一次前端性能优化的过程(多图)

性能优化听多了,又是各个注意点,又是监控什么的,如《JS性能优化探讨》《前端性能监控》,真正遇到了还是得具体场景具体分析。

一直觉得我的个人网站首页(现在不会了)很慢,进去后背景和熊猫头渲染得特别慢。当然这个慢是相对的,就我做过的 TO B 系统单单是前置接口的获取都要几秒了,只是有 loading 效果看起来不难受。但是我这个网页啥都没有,就那么几个图片和动画,着实不该。那就实际一步步看看吧。

一. 服务器连接速度

直接百度搜网站访问速度测试就有很多工具网站。一看都没啥问题,除了极个别的比较远的省份(我这是买的阿里云深圳的服务器)其他都在一秒以内。再 ping 一下,也可以看到并无延迟。也就是 DNS 解析和访问并没有问题。

二. 减小图片体积

那首先考虑资源图片过大,因为确实用了比较高清的图片,那就在不糊的情况下尽量缩小体积。兴冲冲地更新一看,没啥变化。还是有那个等待过程。那好吧,一不做而不休,我尝试把其他图片都去掉,只留下一个熊猫头,还是很慢,就证明不该是图片背锅。当然缩小体积是没有问题的。省流量嘛。

三. 使用 Devtool Performance

那就直接用工具看吧,打开开发者工具,切到 Performance,启动录制看看效果。找一下,在 Timings 那里看到 FP FCP 等事件,这里就是影响视觉观看的主要节点了。当然前面还有 Parse HTML 或者 Function call 等等事件。主要看 Parse HTML,因为要解析了 html,才会加载一系列资源,可以看出这一步并没有什么问题,也是很快就响应了。但是到 FCP 却隔了好几秒。也就是中间有东西耽搁了。
看了一下,是必要的 js 文件加载。有两个,一个是 app.***.js,基础的业务 js, 一个是 chunk-vendor.***.js,vue 框架的打包产物。毫无疑问,这两者都是不可或缺的。appjs 比较小还好,很快就获取完了。chunk-vendorjs有 300+kb,耗时几秒。可以看到它加载完后,很快就 FCP 了。所以它的加载就是问题关键了。

四. js 文件太大?

讲道理,300+kb 的 js 文件不算小,但也不能叫很大啊。怎么要好几秒才能加载完?当然我的服务器没有加速功能,如果放到 CDN 服务,我想应该效果会好点。但是没有这条件,那只能另外想想办法。我代码里用了 Element-UI,测试了一下确实是按需引入,去掉几个组件后,体积变小了。
但,这些就是要用到的,减不了。路由页面用的也是 import() 动态引入,也就是到了真正访问那个页面组件,才会去加载相应的 js。再说了业务代码也不会打包到 vendor 去。那体积没法减了。然后在自己本地测试了一下,以及线上的缓存状态下,页面出现都很快,更加印证了我的猜测,就是这个 “大 js 文件” 导致了页面渲染慢。(当然最好有个 CDN 什么的加速可以验证…)

事情本到这里就结束了(啥都没干,555…),结果又看到一个神奇的网站,谷歌的性能分析网站,地址:https://pagespeed.web.dev (可能需要科学上网)

五. 使用谷歌分析网站

基础界面长这样,把要分析的网站填上去,点击开始分析。同时可以选择手机或者桌面设备。在测试过程中出现过分析失败,会显示一堆 Error。也不知为啥,尝试多几次就好。

分析后展示了一系列问题和建议。



其实分析结果给了四个方面:性能无障碍性能SEO。由于这里是为了解决性能问题,所以就只截了性能方面的图。里面给了两个很实在的建议:

1. 启用文本压缩

对于这个就不啰嗦介绍了。我只是在想怎么一开始没想到!我是用的 nginx 做的服务代理,结果 nginx 是默认没有开启 gzip 压缩。赶紧给加上。

http {
  gzip on;
  gzip_comp_level 6;    # 压缩比例,比例越大,压缩时间越长。默认是1
  gzip_types text/xml text/plain text/css application/javascript application/x-javascript application/rss+xml;     # 哪些文件可以被压缩
}

默认 nginx.confgzip on 是注释掉的,给打开,然后要注意的是 gzip_types 要声明好 css js 都要压缩。要不然还是默认只有 html 压缩。
然后重启服务,看看效果,嘿,体感上果然是快了。300+kb 直接给压缩到 100+kb,加载速度从原来的 5s 快到 1.5s!当然这个是有波动的,试了好几次也有 2s 多的。但总之是快多了。可以看到 FCP 提前了很多。

要注意的是:这里的时间线对的是最上面的那个白色时间间隙,蓝色遮罩的其实是下面事件时间轴的视角外。另外查看文件的响应头有 Content-Encoding 字段,即是有开启文本压缩。

2. 优化星星跳动动画

因为这个动画方法,其实是当时刚学前端看的教学视频抄的,而我看的还是几年前的过期视频。那时候可能还没有 css3 的 transform 啊,will-change 什么的,用的是 定时器 + left 位移。一直没理,这次也一起优化下吧。当然这其实和首屏展示慢没影响,不过确实也是性能指标的一环了。


接着看下优化后的数据,看得出分数是高了。但是 Speed Index 却更多了,应该是网络波动问题,看下这个指标是啥意思。先看下官方介绍。再看下相关中文文章。里面提到一句:当用户视口中的所有内容都完全可见时。那我理解应该还是那几张图片拖了后腿。

新的建议里也提到了优化图片资源,换成更加省空间的 WebP 和 AVIF 等图片格式。而且动画除了开销低的 opacitytransform,其他还是对性能有较大影响。

六. 还能继续优化吗?

有的,”建议“里面也提到了,请减少未使用的 JavaScript,并等到需要使用时再加载脚本,以减少网络活动耗用的字节数。里面提到的未使用到的 js,应该就是那部分 Element-UI 的代码,首页确实没有用到。这里可以:

  1. 改成 CDN 引入插件代码,加上 prefetch 属性(可参阅《HTML几个资源异步属性》)让浏览器有空下载就行,反正首页也用不到。不过也得考虑直接进入其他页面时的情况。
  2. 把使用到的组件的引入放在具体的页面里,就是分薄了 chunk-vendorjs,不过这个太麻烦了,少数几个也就算了,多了还要关注引入组件太麻烦。

七. 继续优化,来波狠的处理第三方插件

1. 启用 CDN?不要了

顺着上面的想法,继续尝试下能否优化到极致。首先是 CDN 方案,结果发现如果 Element-UI 使用 CDN,同时 vue 也要用 CDN 引入,具体可看安装文档。也不是说不行,但,还是算了。

2. 动态引入

既然首页不需要用到 Element-UI 的东西,那么就摘出来,改成动态引入。放在哪里好呢,除了首页其他都用到了。思来想去,就放在全局路由钩子吧,代码如下:

let hasInstall = false;
router.beforeEach((to, from, next) => {
  if (hasInstall || ['home'].includes(to.name)) {
    return next();
  }
  import('element-ui/lib/theme-chalk/index.css');
  import(/* webpackChunkName: "elementui" */'element-ui').then((elementui) => {
    Vue.use(elementui);
    hasInstall = true;
  }).finally(() => {
    next();
  });
});

用一个变量和页面名字做判断,如果是非首页,那么就加载后再进入页面。然后我发现就算这样写,在进入首页时候也会在空闲的时候去加载 elementui.js,这挺好的,不阻塞。不过还是会有两个小问题:

  1. 如果不经过首页且首次直接进入其他页面,那么就要等待加载 elementui.js 后再进入,必然会有点慢。相当于让其他页面给首页负重前行了。不过也不对,首页本来也用不上,不该它背。
  2. 变成动态引入之后,就不能按需加载了。所以代码直接全部安装: Vue.use(elementui)。这是 webpack 的策略,也好理解。资源分析只能在打包静态处理。都动态引入了,哪知道什么按需引入,先全部加载了再看情况使用。

这个方法呢,感觉有点极端 hack,生产来讲没必要这样做,这里自己小网站就无所谓。但是这一招确实大大减轻了首页的体积,在没压缩下,300+kb 降到 200+ kb,少了 100kb,当然压缩后差别没那么大,只有 30kb。

八. http2 & 资源分流 & 缓存

1. 启用 http2

继续看 Devtool Network 面板,觉得一口气请求的东西还是有点多(自己觉得,其实不多)。想了一下都啥年代了,不得上个 http2 试试多路复用?马上更新了一下 nginx,这里启用 http2,还费了一番周章,过程记录在《nginx 的基础用法 & linux(centos)下支持 https 和 http2》。但是很遗憾这一招没看到多大优化,可能是数量不算真正的多,之前的 http1.1 keep-alive 已经效果不错了,http2 的多路复用提升不明显。
这里说一下,Network 面板的 Protocol 可能没显示,在表头那里右键菜单勾上就可以了。

2. 图片分流

接着说到多路复用,必然想到请求分流啊。之前整个文件服务器花了大半天研究 nginx,在《记录下 nginx 使用配置》里,还提到一句:

而且“一个域名同时请求的资源有限,chrome是六个,文件请求可以配置成不一样的域名,不要带cookie等请求头,有助于性能优化”。当然在我这有点扯淡,远远不到要用这个性能优化的手段。

没想到还真的用到了。也就是把图片资源放到文件服务器上去,这里就达到和 js css 文件们域名不同,不用挤到一个赛道去。看一下操作前后对比:

图片分流优化前

图片分流优化后

网络是有波动的,这是我刷新多次截的比较普遍的数据。还是很明显可以看出每张图片都普遍快了几百毫秒。那这一招就算生效了

3. js css 分流

在处理上面的图片分流时,突然又想到,之前公司是这样处理的:前端的东西全部放在一个容器,后端的东西放在一个容器,然后配置好 HAProxy 接口转发规则。也就是前端只有接口和后端打交道,没有所谓后端路由概念。前端也是 HAProxy 配置好静态资源访问路径,前端资源的访问和后端没有任何关系。
也就是:之前没有用 nginx,用的 express.static 来做静态资源容器。现在不要这样用了啊,完全可以改成前端由 nginx 来承载。然后配一条 /api 规则打到后端服务就可以了。

js css 分流优化前

js css 分流优化后

可以看到,分流后的加载速度也是大小不一的加快了:其中最猛的是 elementui.js,直接快了几秒。那这一招也是有效了
不过这里我有个疑问:是分流减轻了 express 本身的服务压力所以响应快呢?还是说 nginx 作为专业的服务容器就是比 express.static 快?

4. 上缓存

要不是“建议”里提到,我还没注意到我都没加缓存,赶紧给 nginx 加上。

location / {
  expires 365d;
}

其实一开始我是写 14d 的,也就是两周。因为之前想起的公司老大说缓存时间跟着发版间隔就好,我觉得有道理,没必要那么长。但是设置 14d,“建议”里具体还是有 warning,然后在 chrome 文档《Serve static assets with an efficient cache policy》(可能需要科学上网)里示例也是一年的样子,当然里面也提到按照实际来。实际就是我改成一年后,“建议”没有 warning 了。其实在之前的缓存策略文章《关于缓存的几个关键词》里提到:

设定一个期限很大的强缓存,如 max-age=31536000 (一年)。文件通过打包工具生成出来都带上哈希。只要文件内容不改哈希就不改,请求的地址就不改,命中强缓存。改了内容自然连请求地址都改了,就会返回新文件。

也是说一年的。按照实际情况设置即可。

注意:命中强缓存(也就是在 Cache-Control:max-age 缓存时间期限内)的状态码是 200,命中协商缓存的状态码是 304。

九. 继续处理图片

1. 转换图片格式,减小体积

那现在基本就是剩下图片大小问题了。那就跟随建议,把图片转为 webp 格式,不得不说,确实图片质量看起来没变化,或者说肉眼看不出来变化,但是体积变小了不少。

这里只放一张图,因为是在已经分流的基础上改的,和上面的 图片分流优化后 图片作对比就行
可以看出,体积变小了,又快了不少,小的几十毫秒,大的几百毫秒。

2. 预加载图片

这个就是障眼法了,全体加载完肯定还是要那么多时间,但是有些图片先渲染出来效果会好一点,比如我期望熊猫头先渲染出来。那就可以先做预加载:

(() => {
  const sunImage = isMobile ? 'https://file.cchealthier.com/file/home/mobile/m-sun.webp' : 'https://file.cchealthier.com/file/home/pc/sun.webp';
  const moonImage = isMobile ? 'https://file.cchealthier.com/file/home/mobile/m-moon.webp' : 'https://file.cchealthier.com/file/home/pc/moon.webp';
  const runImage = theme === 'day' ? sunImage : moonImage
  const preloadImages = isMobile ? [
    'https://file.cchealthier.com/file/home/mobile/m-alert.webp',
    runImage,
    'https://file.cchealthier.com/file/home/mobile/m-yun.webp',
    'https://file.cchealthier.com/file/home/mobile/m-xing.webp',
  ] : [
    'https://file.cchealthier.com/file/home/pc/alert.webp',
    runImage,
    'https://file.cchealthier.com/file/home/pc/yun.webp',
    'https://file.cchealthier.com/file/home/pc/xing.webp',
  ];
  preloadImages.forEach(src => new Image().src = src);
})();

我在首页 vue 文件的 script 里一开始就调用,其实还有更早的地方可以放,但我觉得还是这个页面用的,就放在这个页面吧。

没有预加载时的图片请求

预加载后的图片请求

熊猫头是 alert.png,预加载后,提前很多加载完了。这样就可以在一进去就看到熊猫头出来,然后几个大的背景图,让它们逐个有层次的出来就行了,体感好很多。

十. 总结

最终优化效果

啪啪啪一顿组合拳操作猛如虎,效果终于可以了。量化数据的话就是:优化前的 FCP 经常是 5s~6s,优化后的 FCP 都在 1s 内了。

还有点瑕疵就是 LCP 那里有点慢,不过我觉得可以了,至少没有明显的等待感,一进去感觉好多了。证明所采取的措施有效的。虽然小网站页面简单,但是见微知著,实际线上网页的优化思路和措施也是大同小异。

再归纳总结一下

这里面也是用了各种性能打点数据查看,不过这种都是用来查看是否有问题的,具体问题在哪还得具体分析。基本思路:

  1. 资源文件(图片,css,js 等)体积是否可以缩小,请求是否分流。
  2. 服务器有没有启用文本压缩、加速、缓存,网络有没有启用 http2。
  3. 第三点我觉得才是前端代码的问题,比如有没有动态引入啊,接口请求是否合理,有没有保证最小单位展示。先有东西吸引眼球,然后那点视觉延迟就足够加载完其他资源了。

当然上面说的,其实都是对于加载速度,也就是平常说的首页展示速度。平时前端 coding 的时候也要注意各类影响性能的做法,比如防抖节流懒加载虚拟加载(《简单实现图片懒加载、预加载、分批加载》《懒加载-虚拟列表-下拉菜单选择器》)等等。这就是另外的性能问题了:页面卡顿。

随想

这篇文章引用了很多别的文章,包括很多我自己以前写的文章,也算是有了真实用武之地的实践。所以除去功能不讲,很多时候我们都在寻求更好的网站体验。一开始还只是到第五点就结束了,几乎啥都没优化到。没想到最后效果能到这样。所以细细琢磨,从多个角度思考,还是很多点可以挖。