Featured image of post Core Web Vitals概念讲解及优化方法简述

Core Web Vitals概念讲解及优化方法简述

本文介绍了Google提出的三个核心网站性能指标(Core Web Vitals),包括LCP(最大内容绘制)、FID(首次输入延迟)和CLS(累计布局偏移),介绍了其概念、测量和优化方法。

众所周知,性能优化对于前端开发来说非常重要,特别是对于 ToC 的 Web 应用来讲,如果你的网页使用起来太过卡顿,用户就会选择离开,去使用别人的网站。虽然网络的速度和计算机的性能不断在提升,但我们传输的 Web 应用体积也在变得越来越大和越来越复杂,更何况不是所有人都能够享受快速稳定的网络连接和高性能的计算机。所以我们必须要掌握性能优化的方法,才能保证大部分用户的体验良好,提高网站的受欢迎程度。

大部分的性能优化教程都是围绕网络、浏览器渲染来进行展开,从开发者的角度分析可以做的事情。本文采用了一种不同的视角,我们从用户的角度来看:什么样的网站是一个性能良好的网站?答案会是加载迅速、交互响应快、浏览体验好等等。那我们就可以从这些方面切入,来看有哪些可以做的性能优化尝试。当然了,作为开发者,我们仅仅知道这些定性描述是不够的,还需要可以量化的指标,才能准确地进行测量和提升。

Google 已经帮我们解决了这个问题,Web Vitals(网页体验指标)是由 Google 提出的一系列前端性能指标,用于衡量一个网站的性能和用户体验。其中最核心的是 Core Web Vitals,其中包括了三个指标,分别是 LCP(Largest Contentful Paint,最大内容绘制),FID(First Input Delay,首次输入延迟),CLS(Cumulative Layout Shift,累计布局偏移)。Google 通过一系列的用户研究提出了这些指标,并把它们用于自家搜索引擎对网站的性能评价中,所以是非常值得参考的。通过针对性地优化这些指标,我们可以更有方向地进行性能优化,从而提升网页的用户体验。

Core Web Vitals 概念讲解

首先我们了解一下各个指标的具体含义。

LCP(Largest Contentful Paint,最大内容绘制)

LCP 测量的是网页中面积最大的一块内容加载完成的时间,它代表着用户感知页面加载(基本上)完成的时间

以掘金的主页为例,从骨架屏中我们可以看出最大的一块内容应该是首页的文章列表。因此 LCP 的时间就是页面开始加载到文章列表显示出来的时间。

FID(First Input Delay,首次输入延迟)

FID 测量的是从用户第一次与页面交互到浏览器对交互作出响应所经过的时间。这里的交互包括了点击链接、按钮,在输入框中输入文字等。例如我在掘金的首页加载时点击了顶部 Tab 的“沸点”,过了一秒钟后网站才响应我的点击进行页面跳转,那么此时页面的 FID 就是1s。FID 反映的是网站对用户交互的响应速度

CLS(Cumulative Layout Shift,累计布局偏移)

CLS 测量的是页面加载过程中各个元素偏移的距离总和,它反映的并不是网页加载的速度和响应时间,而是加载过程中的用户体验

CLS 对用户体验的影响可以通过下面这个例子看出:

图中用户想要点击返回按钮时,页面上方弹出了一段新的文字,导致用户误点为提交订单。类似的情况通常是由异步加载的图片或广告导致,如果没有进行恰当的处理,这些元素的异步加载会导致页面的重新布局,破坏用户体验。

测量方法

没有调查就没有发言权。想要对网页性能进行优化,我们首先得对网页的性能指标进行测量,找出性能瓶颈,对症下药。

Lighthouse

在本地调试时,我们可以通过 Chrome 开发者工具中的 Lighthouse 面板来查看网页的 Core Web Vitals 情况。打开 Chrome 开发者工具,选择 LightHouse 面板。如果看不到 Lighthouse 这个 Tab 的话就点右侧的 ⏩,下拉框中可以找到 Lighthouse。

注意在使用 Lighthouse 分析网站性能时最好是将开发者工具单独为一个窗口,不要和测试页面的窗口挤在一起,因为这样才能反映正常屏幕宽度下网站性能的情况。默认 Lighthouse 会对 Accessibility,PWA 等都进行检测,这些我们并不需要,所以取消勾选即可。最后点击右上方的蓝色按钮开始分析网页性能。

以掘金的分析结果为例,我们可以看到最上面的是网页的总体分数,接下来的一栏就是网页的性能指标情况。其中出现了我们提到的 LCP 和 CLS,而 FID 实际上就相当于 First Contentful Paint + Total Blocking Time。报告中显示为绿色的说明该指标情况良好,如果是黄色说明有待提高,红色则表明需要多加注意。

再往下滚动可以看到报告中还给出了一些具体的优化建议,我们可以在右上角通过相关的指标来筛选建议。

点击其中的某个建议,例如“给图片添加heightwidth“,可以看到开发者工具已经贴心地告诉我们是哪些图片有这个问题,以及改进这一点所能够改善的 Web Vitals 指标。

代码测量

单靠本地调试的方法来测试网页性能显然是不够的,因为本地测试只能反映应用在开发者的电脑上运行的情况,而开发者的电脑性能和网络状况往往比普通的用户要好,所以我们更应该关注的是用户访问网站时的性能指标情况。我们可以利用浏览器给我们提供的PerformanceObserverAPI 来收集用户访问页面时的 Web Vitals 数据,然后进行上报。

对于 LCP,我们可以通过如下的代码进行测量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const metrics = {
  lcp: 0,
  fid: 0,
  cls: 0,
};

new PerformanceObserver((entryList) => {
  let entries = entryList.getEntries() || [];
  entries.forEach((entry) => {
    // 比对每个阶段的LCP和当前记录的LCP,记录最大的LCP值
    if (entry.startTime > metrics.lcp) {
      payload.lcp = entry.startTime;
      console.log(`LCP: ${metrics.lcp}`);
    }
  });
}).observe({ type: "largest-contentful-paint", buffered: true });

我们调用new PerformanceObserver(cb).observe(options)函数,在options中告诉它我们要观察largest-contentful-paint,也就是 LCP。buffed: true表示即使相应的性能指标在PerformanceObserver创建就已经发生,也会被缓存下来。

要注意的是 LCP 有可能会是一系列不同的值,原因是PerformanceObserver记录的是每个时刻网页中最大的内容完成渲染的时间,以上文提到的掘金主页为例:网页中最快渲染出来的是顶部的 tab 栏,我们假设它在0.5s完成了渲染,这时网页中面积最大的元素也是它,所以此时的 LCP 是0.5s;随后1.2s时两侧的侧边栏模块也渲染完成了,但此时网页中面积最大的元素还是 tab 栏,那么 LCP 就依然是0.5s;最后在2s时文章列表也渲染完成了,此时网页中最大的元素就变成了文章列表,相应地,LCP 也变成了2s

这就是为什么在PerformanceObserver的回调中我们要比对每个entrystartTime(也就是每个阶段记录的 LCP)和当前记录的 LCP,这样我们才能找到最大的 LCP 值,这个值就是真正的网页 LCP。

对于 FID 的测量,我们使用的代码也非常类似:

1
2
3
4
5
6
7
new PerformanceObserver((entryList) => {
  let entries = entryList.getEntries() || [];
  entries.forEach((entry) => {
    metrics.fid = entry.processingStart - entry.startTime;
    console.log(`FID: ${metrics.fid}`);
  });
}).observe({ type: "first-input", buffered: true });

我们没有办法直接获取到 FID 的值,而是观察first-input的情况,通过将其触发时间entry.startTime与真正的处理开始时间entry.processingStart进行相减,可以得到 FID 的值。

CLS 的测量代码也跟上面两个指标差不多:

1
2
3
4
5
6
7
8
9
new PerformanceObserver((entryList) => {
  let entries = entryList.getEntries() || [];
  entries.forEach((entry) => {
    if (!entry.hadRecentInput) {
      metrics.cls += entry.value;
      console.log(`CLS: ${metrics.cls}`);
    }
  });
}).observe({ type: "layout-shift", buffered: true });

CLS 的含义是页面加载过程中各个元素偏移的距离总和,所以我们观察页面的layout-shift事件,将每一次布局偏移的值都累加到 CLS 的值中,最终就可以算出用户使用过程中的累计布局偏移量。

不难发现这种写法还是很繁琐的,所以 Google 为我们封装了web-vitals库,利用这个库我们可以更方便地获取 Web Vitals 的值。其使用示例如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { onCLS, onFID, onLCP } from "web-vitals";

function sendToAnalytics(metric) {
  const body = JSON.stringify(metric);
  // 如果`navigator.sendBeacon()`可以使用就用, 否则使用`fetch()`将性能数据发送至服务器。
  (navigator.sendBeacon && navigator.sendBeacon("/analytics", body)) ||
    fetch("/analytics", { body, method: "POST", keepalive: true });
}

onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);

其中 sendBeacon 和普通的fetch请求区别在于:sendBeacon 适用于在页面卸载或关闭时需要发送数据的场景,例如发送统计数据或日志等。这是因为 sendBeacon 只能使用 POST 方法发送数据,且没有回调函数,但它不会阻塞页面的卸载,所以可以提高浏览器重新导航的速度。

优化方法

LCP(最大内容绘制)优化

对于大部分的网站首页来说,Largest Content 都是 banner 图之类的图片。那么我们就应该想办法让这张图片加载出来的速度更快,要做到这一点,我们有三个努力的方向:

  1. 推迟不必要的资源加载,让主要图片的请求优先进行。一个网站在初始化时需要请求的东西是非常多的,包括 css 文件、js 文件、字体文件以及大量的图片。我们不妨看一看掘金首页加载时请求了多少东西。

    由于屏幕大小的限制,我并没有把所有的请求都截到图里面,但已经不难看出请求的文件个数非常的多。而一个页面通常一次性只能并发 6-8 个请求,所以如果我们没有做好处理,关键图片的请求可能排队就要排很久。

    在上面提到的初始化需要的文件中,字体文件和 CSS 文件肯定是要优先加载的,因为它和图片一样都是渲染页面所必须的。但对于一些次要的 js 文件和图片文件,就可以考虑延迟其加载,来给关键图片加载让路。

    js 文件的延迟加载可以通过defer属性来实现,当浏览器遇到带有defer属性的 js 文件时,它会将该文件的加载推迟到文档解析完成后再进行。

    图片文件可以通过在其元素中添加loading="lazy"属性来实现懒加载。对于添加了loading="lazy"属性的图片,浏览器会在其进入可视区域后再进行加载。

  2. 优化图片大小。由于决定 LCP 的大部分是图片,所以我们可以对主要图片的大小进行优化,有以下几种具体方法:

    a. 使用体积更小的图片格式,比如webp

    b. 利用工具对图片进行无损压缩,例如 node 中的imagemin库。

    c. 使用响应式图片,根据屏幕的大小对请求的图片分辨率进行调整。小屏幕下并不需要看太高分辨率的图片,所以可以传输更小的低分辩率图片。对此 HTML 也是有原生的支持的:

    1
    2
    3
    4
    5
    6
    7
    8
    
    <!-- srcset告诉浏览器在不同的宽度下对应的图片链接,sizes则是用于告诉浏览器我们对于不同宽度的定义 -->
    <img
      src="picture-1200.jpg"
      srcset="picture-600.jpg 600w, picture-900.jpg 900w, picture-1200.jpg 1200w"
      sizes="(max-width: 600px) 600px,
        (max-width: 900px) 900px,
        1200px"
    />
    
  3. 提高关键图片的网络请求速度。对于如何提高网络请求的速度相信大家应该都比较熟悉了,这里简单列举几种方法:

    a. 使用HTTP/2,其二进制传输和多路复用机制能够提高网络请求的速度。

    b. 利用CDN将用户请求的资源分发到距离用户最近的节点,避免长距离的数据传输。

    c. 利用HTTP 缓存来避免不必要的重复请求。

    这篇文章中对于网络请求的优化有更详细的论述,大家有需要的话可以参考。

FID(首次输入延迟)优化

FID 对应的其实就是相关 JS 文件解析完成的时间,因为只有 JS 解析完了页面才能对用户输入做出反应。因此优化 FID 也就意味着优化 JS 文件的加载和解析速度,说到底就是要传输尽量少的 JS 文件,同时提高 JS 文件的传输速度。

  1. 要想传输更少的 JS 文件,我们首先可以利用打包工具的split chunks(代码分包)和tree shaking功能来删除重复和无用的代码。然后再利用 UglifyJS 之类的代码压缩工具对代码进行进一步的压缩。

  2. 提高 JS 文件的传输速度其实就是提高网络请求的速度,相关的方法在上面已经列举了一些,这里就不再赘述了。

CLS(累计布局偏移)优化

布局偏移的解决方法比较直观,就是给那些可能会延迟加载出来的元素设定占位符/骨架屏。如果不清楚页面中有哪些元素导致了布局偏移,也可以通过上文所说的 Lighthouse 进行查看,再对症下药。除了使用骨架屏,也可以直接通过 HTML 的heightwidth属性,来让浏览器事先了解相关元素的大小(这被称为“Layout Hints” - 布局提示),这样在布局页面的时候浏览器就会把元素的宽高计算在内,元素加载出来时就不需要重新布局了。

1
2
<!-- 通过width和height告诉浏览器这是一张640*480的图片 -->
<img width="640" height="480" src="..." />

除此之外,一些页面中的动态元素可能也会影响 CLS。对于这些动态元素,可以通过绝对定位等方式让其脱离文档流,避免对其他元素的影响,造成浏览器大规模的重新布局。

对于元素尺寸、位置的改变尽量通过transform实现,因为transform不会改变元素在文档流中的位置,也就不会影响其他元素的布局。

总结

本文介绍了 Google 提出的三个核心网站性能指标(Core Web Vitals),分别是 LCP、FID 和 CLS,介绍了其概念、测量和优化方法:

  1. LCP(Largest Contentful Paint,最大内容绘制)指的是网页中面积最大的一块内容加载完成的时间,可以通过推迟不必要的资源加载、优化图片和提高图片请求速度来优化 LCP。
  2. FID(First Input Delay,首次输入延迟)指的是用户第一次与页面交互到浏览器对交互作出响应所经过的时间,通过压缩 JS 文件和提高 JS 文件传输速度可以优化 LCP。
  3. CLS(Cumulative Layout Shift,累计布局偏移)指的是页面加载过程中各个元素偏移的距离总和,利用骨架屏、布局提示、绝对定位以及transform等技术可以有效减少页面的 CLS。

上述的三个指标都可以通过浏览器提供的PerformanceObserver API 来进行测量,web-vitals等工具库对这个 API 进行了封装,更方便使用。在实际开发中我们可以利用 Lighthouse 来分析潜藏的性能问题,从而对症下药。

本文作者 wzkMaster,码字不易,有帮助的话欢迎点赞收藏~

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy