lighthouse性能优化实战

4,276 阅读7分钟

前言

作为电商基础链路的开发,除了要考虑功能是否能快速上线外,更多的是要注重页面的性能和用户的体验。

当前详情页面的架构较旧(后端java&vm+前端React), 重要数据都伴随页面一起从后端同步返回,本文章记录的是在不大改的前提下,沉淀了一系列对详情页面的优化,希望能对有需要的小伙伴有所帮助。 其实更好的策略是动静分离:cdn缓存静态页面,利用service worker发送并缓存首屏请求,获取首屏数据。这样,每次请求页面不必回源,可极大减少TTFB。此外,详情页不是独立存在的,除了本身页面的优化外,其他影响详情页面的脚本的优化也很重要。

心得是:性能的优化因不同场景而异,希望大家能灵活应用,找到自己页面性能卡点,对症优化 最后,若有不足,请各位大神多多包涵。

正文大纲

  1. Lighthouse vs Lighthouse6
  2. 6大测量指标及其优化策略排序
  3. 页面现状分析
  4. 针对各个测量指标的优化措施
  5. 优化后的结果对比

Lighthouse5 vs Lighthouse6

lighthouse是google开源的一个有助于提升页面质量的工具。它能够测量页面的性能,可访问性,渐进式web app,SEO等等方面。本篇文章中我们主要是在PageSpeed Insights测量页面的performance。当然,你也可以在chrome的开发控制台使用它。

结论: 评分标准有变化,评分比重较大倾斜到 largest contentful paint 和 total blocking time。所以优化重点在减低最大内容渲染和减小总阻塞时间。

6大测量指标优化策略排序

  1. Largest Contentful Paint 【简称LCP: 最大内容渲染】 LCP官方详细说明

    FCP最大内容渲染时间标记了渲染出最大文本或图片的时间。

  2. Total Blocking Time 【简称TBT: 总阻塞时间】TBT官方详细说明

    TBT测量了FCP(首次内容渲染)和TTI(可交互时间)之间的总耗时。TTI可能会被主线程阻塞以至于无法及时响应用户。大于50ms的任务称为长任务,当任意长任务出现时,主线程则称为被阻塞状态。由于浏览器不会打断正在进行中的长任务,所以,如果用户在执行长任务时和页面有交互事件时,浏览器必须等到该长任务完成才能响应。TBT计算的是在FCP到TTI之间所有长任务时间内总和。 如下图所示,我们有5个任务,其中有3个是长任务,因为它们的时长超过了50ms。

虽然运行任务的总时长是560ms但是只有345ms被认为是总阻塞时长。

  1. First Contentful Paint 【简称FCP: 首次内容渲染】FCP官方详细说明

    FCP测量了从页面开始加载到页面任意部分的内容渲染到屏幕上。

  2. Speed Index 【简称SI: 速度指数】 SI官方详细说明

    SI速度指数表明了网页内容的可见填充速度。lighthouse首先捕获页面加载的视屏,然后对比帧与帧之间视觉效果变化(通过计算结构相似指数SSMI来比较)。 如何提升SI呢?

    • 最小化主线程工作量
    • 减小JS的执行时间
    • 确保在字体加载过程中文本可见
  3. Time to Interactive 【简称TTI: 可交互时间】TTI官方详细说明

    可交互时间是指网页需要多长时间才能提供完整交互功能。TTI测量了从页面开始加载到页面的主要附属资源加载完毕,并且可以足够快速回应用户输入的所用时间。

  4. Cumulative Layout Shift 【简称CLS: 累积布局偏移】CLS官方详细说明

    CLS累积布局偏移旨在测量可见元素在视口内的移动情况。CLS值越小越好。

页面现状分析 (已做首屏代码分片和图片懒加载)

performance总分: 33分

问题1: LCP检测到的是不是商品主图而是弹框中的蒙层图片,该图片未压缩,加载慢,size大。导致FCP过大。

问题2:TBT 440ms,说明长任务过多,需要拆分js,策略加载异步脚本,或者对一些重要的第三方资源host做预链接。

问题3:FCP耗时过长。

问题4:TTI过长,长任务过多会影响TTI。

问题5:SI过大。一方面可能是白屏时间太久,另一方面,可能是其他资源阻塞了主资源加载,拖慢渲染。

开发调试工具里测的,惨不忍睹

针对各个测量指标的优化措施

措施1:优化LCP

优化弹出框,替换过大背景图片。商品图片会被识别为最大,对图片做preload。加快图片下载。

措施2:减少无用JS,减小TBT,提升SI

彻底code split。之前文章里讲到只是对非首屏的js做了拆分。现在对所有未用到的代码都做了拆分。比如有些组件特定商品才会有,比如尺码表,车型库,都可以只在命中该商品,需要下载时再下载。有些组件,可以置后加载,只有在用户点击了,才会下载,比如优惠券组件。这里需要额外做了下载失败后的多次尝试下载,力保该组件可加载成功。再次尝试加载代码如下:

// 重新额外加载3次
export function reLoadChunk(fn, reloadTimes = 3, interval = 300) {
    return new Promise((resolve, reject) => {
        fn().then(resolve)
            .catch((error) => {
                if (reloadTimes === 0) {
                    reject(error);
                    return;
                }
                setTimeout(() => {
                    reLoadChunk(fn, reloadTimes - 1);
                }, interval);
            });
    });
}

// 异步加载的模块
// 就这样包裹一层,就可以实现对一些重要的异步模块的再次加载
const asyncModule = lazy(() => reLoadChunk(() => import(/* webpackChunkName: "async-module" */ '@async/module/npm')));

措施3:首图webp处理,减小自定义首屏时间

由于详情页的自定义首屏时间是按照商品主图渲染完成来上报的。所以压缩图片对减少自定义首屏时间很重要。有时候,原图大小可能800KB+,webp可将其压缩至200KB+。效果还是很明显的。

措施4:内联骨架屏css,减小FCP耗时,提升SI

之前骨架屏的css是写在主css里的。需要解析到主css之后才能显示出骨架屏。现在直接将它内联到后端vm模板。避免被其他资源阻塞。可稍微加快骨架屏的渲染。

措施5:资源预加载
<!-- preload js -->
<link rel="preload" as="script" href="{js地址}" crossorigin />
<!-- preload css -->
<link rel="preload" as="style" href="{css地址}" crossorigin />
<!-- preload 图片 -->
<link rel="preload" as="image" href="{图片地址}" crossorigin />
措施6:提前建立第三方链接

注意:只对一些立即用到的第三方站点做这种preconnect处理,要省着用preconnect。 用preconnect给所有的第三方站点都做提前链接很低效,可以为重要的第三方站点做preconnect,其余的都做dns-prefetch. 可节约20-120ms。

<!-- preconnect 3rd party -->
<link rel="preconnect" href="https://example.com">
<link rel="dns-prefetch" href="https://www.google-analytics.com" />
<link rel="dns-prefetch" href="https://cm.g.doubleclick.net" />

措施7:减少CLS

其实布局有偏移还是很常见的。比如我们投放的广告顶通,在页面最上方,是异步接口获取的。可能有也可能没有广告,所以看到可能会是页面先加载了一部分,然后被广告顶下来了。这种异步的内容减少CLS的方式:

  • 可以想办法把这个异步接口合并到主接口里来解决
  • 或者设置一个默认的图片占位。等数据来了再替换掉。
其他措施:当把lighthouse给出的opportunities和diagnosis都做到了以后,可考虑
  • 减少服务端TTFB,需要后端优化主接口。
  • 第三方脚本治理:能优化就优化,不能优化的想办法置后加载,避免阻塞主js。

优化后的结果对比

lighthouse测量对比

自定义首屏对比

自定义首屏时间为首张主图大图渲染完毕的时间。 经历了三波优化和方案试错,从一个月的平均数据来看,全球主站点自定义首屏从2.2s降低到了1.76s左右。✿✿ヽ(°▽°)ノ✿目前暂时不能动了,之后再继续。