网页的性能优化

20231120235319

从用户在浏览器输入网址,按下回车那一刻起,用户对网页的感受就开始了。

紧接着,用户滚动页面,页面是否流畅?

又或者用户点击按钮,网页是否快速响应?

我们可以简单的认为:

网页性能优化就是用最快的速度响应用户的操作,进而提升用户的体验

发现问题

首先,我们要知道网页慢在哪?我们借助以下工具来查探。

工具一:开发者工具之网络

20231121005202我们需要重点关注上图中底部的三个数据

1、总请求数:越少越好

首屏请求数量最好控制在 80 个以内,最多不超过 110 个

2、资源的总体积:越小越好

首屏请求体积最好控制在 3.5MB 以内,最大不要超过 6MB

3、DOMContentloaded 时间:越小越好

DOM 解析渲染完成时间控制在 1.2 秒以内,最大不要超过 3.5 秒

工具二:开发者工具之性能

20231121013221这张图对刚开始接触的人,会感觉乱,但现在只需关注红框中的内容:

1、第一个红框展示是当前网页执行时经历了一些长任务

W3C 把单个任务(比如执行一个函数)超过 50 毫秒的定义为长任务,红色条出现的越多,说明性能越差

2、接下来是 FP、FCP、LCP,这些表示某些元素渲染的时间节点

FP(First Paint) 第一次像素绘制的时间

FCP(First Contentful Paint) 第一次内容(文本,图片等)绘制的时间

LCP(Largest Contentful Paint) 最大内容块的绘制时间点

这三者,越靠左,即越早绘制,用户就能越早看见内容

工具三:开发者工具之 Lighthouse

上面两个工具,能够发现一些问题,但却没告诉我们衡量标准是什么,以及我们具体该优化哪些地方。

这个时候 Lighthouse 就登场了,它是 Google 推出的一款网页性能测试工具。

初次使用时,为了便于理解,要做一些简单的设置:

20231121170510


测试完成, 你将会看到测试总分和五项指标

20231121172246


再往下拉,你会看到一些优化建议

20231121231310

其实按照它给出的建议,一个个去优化,一般都能优化到一个不错的效果。

关键指标说明

20231121231838

  • FCP(First Contentful Paint):第一次内容(文本、图片等) 绘制的时间
  • LCP(First Contentful Paint): 第一次内容(文本,图片等) 绘制的时间
  • TBT(Total Blocking Time):所有长任务阻塞的时间
  • CLS(Cumulative Layout Shift):累积的布局偏移量
  • SI(Speed Index):内容填充速度

Google Lighthouse 经过多个版本的总结,现在确定将这五个关键指标进行评估计分,接下来我就围绕这几个指标结合实际项目展开说明

解决问题

在解决指标性能问题之前,默认你已经对网页做了最基本的优化:

  • 图片懒加载
  • 原始资源压缩(HTML、CSS、JS、XHR资源、图片、音视频)
  • 资源压缩传输(gzip、deflate、br)
  • 缓存(CDN缓存、强缓存、协商缓存)

FCP(First Contentful Paint)

大致意思就是 第一个内容从发出请求到绘制出现在页面所耗费的时间,这里的内容一般指 文本图片等。

如果你网页中第一个内容是图片,那它需要一定的时间请求到图片资源,然后才是绘制,那这个 FCP 势必会比普通文本的 FCP 时间长。

FCP 是包含了从发出页面请求到返回内容的时间,这就意味着下面的每一条都可能影响最终的结果:

需要排除的物理因素

  • 测试设备 的性能
  • 测试网络 的速度
  • 服务器处理速度
  • 服务器网络速度

作为前端的你,对于这个指标,能做的就是:

FCP 影响着白屏时间,应该控制在 600ms 以内

  • 减小网页文档的体积
  • 移除不必要的 cookie,让网络请求更快
  • 优化第一内容的体积(如果是图片)

LCP(First Contentful Paint)

大致意思就是 最大内容从发出请求到绘制出现在页面所耗费的时间

最大指的是体积还是面积?

为了验证这个猜想,我做了个实验:

<body>
    <!-- 这张图片实际是300*300,但我让它显示成 100*100 -->
    <img src="https://fakeimg.pl/300x300/fdd902/333/" width="100" height="100" />

    <!-- 这张图片实际尺寸是100*100,但我让它显示成300*300 -->
    <img src="https://fakeimg.pl/100x100/cb0000/fff/" width="300" height="300" />
</body>

20231122113138

试验结论

通过上述实验,我们得知这里的 最大 指的是渲染面积,而不是内容的体积。

当我得知这个结论后,我曾经为了提升一个商品详情页的 LCP 指标,我做了一个内容很简易的图片(为的是让图片体积很小,甚至能用 Base64 代码形式存在),用它来初始占位。 20231122115955

我想象的是这个占位图会被判定为 LCP,那指标自然就上去了。

但现实情况是,虽然我用了占位,但我最终要将真实有效的图片替换这个占位。不仅要替换,而且要快,毕竟用户体验优先。

上线以后,这个 LCP 数据不仅没有变好,反而变差了。

经过一番排查,猜测原因可能是:在短时间内,最大内容渲染时间是累积的。比如,我加载占位图需要 0.5s,那真实的图片,加载并渲染需要 1.5s,那这个 LCP,间隔短的情况下可能会变成 2s。

Contentful 指的是什么?

Contentful一般指 文本图片等。

如果只是定义了一个没有内容,但有背景色的 div 标签,那它不算作是 LCP,因为对于用户来说它并不是可查阅的内容

LCP 成为关键指标的原因分析

对用户来说,当前视口内最大面积的内容的渲染完成,就意味着当前整个区域已经可以查看阅读,所以这个越早渲染,体验越好

LCP 应该怎么去优化(LCP 时间最好能控制在 1.6s 以内)

一旦确定了 LCP 元素,应该去优化这个元素本身:

  • 从源头出发,在不影响质量的前提下压缩体积
  • 使用恰当的尺寸,页面上需要多少尺寸,源图就应该是多少尺寸
  • 使用 CDN 存储这个资源
  • 使用 Webp,Avif 等压缩率更好的图片格式

TBT(Total Blocking Time)

TBT 是前端最需要也最能优化干预的一个指标,同时 Lighthouse 计分规则也将它作为最高比重,足足占了 30%

大致意思就是 阻塞的总时长,更确切的说法应该是:从第1个像素渲染开始,直接可以接收用户操作,所有长任务 超出部分 时间的总和。

TBT 的计算

长任务W3C 给出的一个说法:如果一个任务(比如一段代码的执行又或者一处界面的渲染),它花费的时间大于 50ms,这就算一个长任务。

而 TBT 是将大于 50ms 的时长进行累加(举个例子):

  • 整个 jquery.js 执行完需要 90ms, 这个超出 90 - 50 = 40ms
  • 自己写的 render() 函数执行完需要 40ms,这个没有超出,不加入计算
  • 有一个 for 循环花费了 120ms,这个超出 120 - 50 = 70ms

那把这些累加,这个网页的 TBT 时长就是 40 + 70 = 110ms

浏览器任务调度基本逻辑

在优化之前,我们先简单了解一下浏览器是如何处理任务的。

虽然浏览器应用能够开启多线程处理任务,但操作的事物最终将体现在单个界面上,为了让显示和操作井然有序,现代浏览器采用的是为一个标签页只开启一个单独的渲染主线程,由这一个主线程去处理:

  • 解析HTML
  • 解析CSS
  • 计算样式
  • 样式与布局合成
  • 渲染
  • 处理JS
  • 处理定时器(这个比较特殊)

当主线程遇到了定时器,也就是 setTimeout 这样的计时函数,这个计时可能很长。

为了不阻塞正常的渲染,它会将这个任务交给单独的 计时线程 20231122160237

TBT 优化在代码中的体现

在了解浏览器任务执行逻辑后,我们似乎可以利用这个延时队列机制做一些任务处理的优先级调整。

一个内容很多的界面,尤其是像商品详情页这种,包含了主图概览区、推荐位、详情描述、评论、问答等。

其实对用户来说,刚进到商品详情页,他最迫切的是想看到商品的标题、价格、主图、购买按钮。

20231122164453

那么我们应该将这些主要的功能逻辑调用放在最前面,而其他没那么迫切的模块,可以通过异步操作将它们延后执行。

模块的拆分

为了方便功能调度和维护,我将商品详情页的模块都进行了拆分: 20231122164957

所有的模块,通过主入口 product.js 进行调度分配,我单独将首屏(即大部分用户首次进来看到的界面)所需处理的逻辑单独拎出,而其他不是那么迫切的任务,我用定时器将它们延后执行 20231122165240

首屏优先执行

首屏的处理也很简单,就是调用价格接口并初始化价格功能以及主图的功能 20231122165451

其他模块按需执行

至于其他的一些模块,诸如 评论、推荐位,很多新用户可能压根不会去查看,那我最初便不会去执行,只有当用户到达那片区域才会加载。

我们利用了图片懒加载的思想,用 IntersectionObserver 封装了一个通用的视口触达方法 lazyObserver

20231122171224

TBT的瓶颈可能出在第三方库

其实正常写业务代码,绘制页面,以现代浏览器的性能,都不太会产生太大的长任务。

很多情况下是出在庞大的第三方库,因为大部分情况,一整个第三方库可能就一个大任务去执行,这点我们通过 Lighthouse 建议面板可查探到: 20231122180123

站内公共脚本的 TBT 问题

自已站内抽离的一些 common.js,因为任务比较集中,也会出现这样的问题。

但这些至少我们还能去优化,比如排查一些循环,把长任务进行拆分,那么拆分长任务除了我上述用的 setTimeout 或 其他异步任务之外,还能用 Google 官方建议的用 async await 给主线程让步的方式:

function delay(duration) {
    var start = Date.now();
    while (Date.now() - start < duration) { }
}

function func1() {
    delay(1000);
}

function func2() {
    delay(2000);
}

// 创建一个即时完成的 promise
function forMain() {
    return new Promise(resolve => {
        setTimeout(resolve, 0);
    });
}

async function taskQueue(tasks) {
    while (tasks.length > 0) {
        const task = tasks.shift();

        task();

        await forMain();
    }
}

除此之外,将任务拆分在不同的script 标签中,不论是行内还是外链形式,它们都将以独立的任务运行

<script>
    function delay(duration) {
        var start = Date.now();
        while (Date.now() - start < duration) { }
    }

    function func1() {
        delay(1000);
    }

    function func2() {
        delay(1500);
    }

    // 任务1
    func1();
</script>

<script>
    // 任务2,这和任务1不会合并成一个任务
    func2();
</script>

CLS(Cumulative Layout Shift)

大致意思就是布局元素整体偏移量的累积。 这么说还是有点抽象,再简单点理解就是比如有一个 div 元素,最初是显示在最顶部,过了一会,在 div 的上方,又插入了新内容,把 div 往下推了一段距离,造成页面的重排。

这个推的距离和推的面积,有一个计算方式,具体查看 计分规则open in new window

再说的直白些,就是要减少我们的页面元素的重排。

我在网页中的实践

由于我的商品详情页基本价格区,是由异步请求出来的结果,如果网络状态比较差的情况下,等待会比较久。

所以在一开始,用户看到的下面简介文本内容 20231122182923

当异步请求完成以后,价格+营销信息+购买按钮+物流信息+优惠券信息出现后,就会将简介整个版块往下推,那就造成了布局重排,这样CLS分数较,对用户的体验就比较差。 为了解决这个问题,我们想过让服务端直接输出,不采用异步的方式加载。但招到服务端的拒绝,理由是价格变化比较频繁,物流信息也跟每个用户相关,很难做到服务端直出。 改版服务输出方式不行的情况,我只好换成了现在的骨架屏方案,骨架屏可以占住将来要显示内容的区域,不会造成简介内容的重排: 20231122183431

过渡与动画对 CLS 的影响

我们在做一些对 margin,padding,top,left 改变的位移动画时,可能会造成 CLS 偏移 比较好的做法是采用 CSS3 的 transform,因为它不会造成重排,也不需要经过光栅化。

SI(Speed Index)

大致意思就是可见元素内容在当前可视区的一个填充时间,这个比较受网络加载的影响,如果网络保证的情况下,LCP 也优化好,这个参数一般是水到渠成。

要注意的一点是,我们在使用懒加载图片时,应该排除首屏。

也就是说首屏没必要做懒加载,而应该直出图片,这样才能提高 SI 的速度,这也符合用户的观感,最快地看到需要的内容。

这是我在首页大图的处理方式: 20231122185100

这是我在列表商品卡片时做的处理: 20231122185243

总结

性能优化主要就是在将内容最快地呈现给用户,无论你是通过缩小资源体积,还是保证服务网络带宽,又或者按需加载。

说来说去就一句话,给用户所需,让用户指哪打哪就是好的体验

不要为了参数去做优化,因为有些参数可以作假,但损失了用户体验

请,永远不要为了 KPI,或指标参数去做优化,如果你是一个用户体验优先的前端开发者。

你就只有一个目标:让内容最快地呈现给用户