做性能优化时,我们关注哪些指标?

技术漫谈

共 7743字,需浏览 16分钟

 · 2021-10-22

大厂技术  爆肝日更  精选好文

前一段时间,我们对 webview 做了一期性能优化,在优化过程中,我们追求的是页面要足够「快」「稳」。那怎么定量地衡量这个「快」「稳」呢?性能指标帮助我们从数据化角度了解页面性能现状,性能瓶颈以及优化完成后,衡量优化效果。

性能指标在日常开发中,大家或多或少的都有接触。比如👇 是一个页面在 Lighthouse 下跑出来的性能数据。

bae5edf37ccd54a33faa49eabec5a31d.webp

这篇文章将回答下面几个问题:

  • 哪些性能指标是需要观测的?它们是什么含义?
  • 这么多指标,我们在什么场景下应该关注哪些?
  • 指标是怎么采集的?
常用的性能指标
  • First Paint
  • First Contentful Paint
  • Largest Contentful Paint
  • First Meaningful Paint
  • First Input Delay
  • Cumulative Layout Shift
  • Time to Interactive
  • DOMContentLoaded
  • Load

First Paint(FP) 首次绘制

首次渲染的时间点,可以视为白屏时间,比如完成背景色渲染的时间点。通常作为时间点最早的一个性能指标。

First Contentful Paint(FCP) 首次内容绘制

首次有内容渲染的时间点,指标测量页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间。对于该指标,"内容"指的是文本、图像、元素或非白色的元素。可以作为首屏时间。

FP vs FCP

FP:从开始加载到第一次渲染

FCP:从开始加载到第一次内容渲染。

FCP 是 FP 的增强版,对用户来说更关键。因为 FCP 带着图像和文字这些内容信息,是用户更关心的。

FP 和 FCP 可能是重合的。

1512d00e3edbfdb1a330e2e0505db781.webp

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

页面的最大内容(通常是比较核心的内容)加载完成的时间,这个最大内容可以是图片/文本块。它是一个 SEO 相关的指标。

LCP vs FCP

FCP:页面加载过程中,比较早期的一个指标,如果一个页面有 loading 态,这个指标表现可能很好,但是实际内容什么时候呈现给用户,这个指标没办法衡量。

LCP:关注页面核心内容呈现时间,这个内容是用户更感兴趣的,更加用户相关。

705261616a40757166f4ad90f7fc32d8.webp

First Meaningful Paint(FMP) 首次有效绘制

首次绘制有意义内容的时间。业界比较认可的方式是在加载和渲染过程中最大布局变动之后的那个绘制时间即为当前页面的 FMP。因为它计算相对复杂,且存在准确性等问题,Lighthouse 6.0 中被废弃。

LCP vs FMP

FMP: 早期比较推荐的性能指标,但是计算更复杂,而且准确性不是很好

LCP: 更新的数据指标,有 API 直接支持,统计简单,且准确,但也存在最大内容是否为最核心内容这样的问题。

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

这个指标的触发是在用户第一次与页面交互的的时候,记录的是是用户第一次与页面交互到浏览器真正能够开始处理事件处理程序以响应该交互的时间,即交互延迟时间。比如发生在用户第一次在页面进行 click, keydown 等交互。

为什么会有这样的延迟呢?一般来说,发生输入延迟是因为浏览器的主线程正忙着执行其他工作(比如解析和执行大型 JS 文件),还不能响应用户。

Cumulative Layout Shift(CLS) 累积布局偏移

在一个页面的生命周期中,会不断的发生布局变化(layout shift),对每一次布局变化做一个累积的记分,其中得分最大布局变化即为 CLS。是衡量页面稳定性的重要指标(visual stability)

糟糕的 CLS 对用户体验的影响👇

f6dd542151d8a82a88cbe0f91ea59320.webp

Core Web Vitals

2020 年 5 月,Google 提出的衡量网站用户体验的核心数据指标,涵盖了页面的加载速度、可交互性和稳定性。是近期生效的会影响 SEO 的重要指标,包含一下三项:

  • LCP
  • FID
  • CLS

Time to Interactive(TTI) 可交互时间

说到 TTI 首先要介绍下 Long Task

Long Task:如果浏览器主线程执行的一个 task 耗时大于 50ms,那么这个 task 称为 long task。用户的交互操作也是在主线程执行的,所以当发生 Long Task 时,用户的交互操作很可能无法及时执行,这时用户就会体验到卡顿(当页面响应时间超过 100ms 时,用户可以体验到卡顿),进而影响用户体验。

从页面加载开始到页面处于完全可交互状态所花费的时间。通常是发生在页面依赖的资源已经加载完成,此时浏览器可以快速响应用户交互的时间。

DOMContentLoaded(DCL)

DOM 加载完成即触发,不用等页面资源加载。

Load(L)

页面及其依赖的资源全部加载完成的时间,包括所有的资源文件,如样式表和图片等。

常见的性能名词

以用户为中心的性能指标 (user-centric metric)

传统性能指标:

很长时间以来,描述性能是通过 Load/DOMContentLoaded 事件进行测量的,一个网站加载完成的时间是多少秒。虽然 Load/DOMContentLoaded 是页面生命周期中比较明确的时刻,但是它真的能很好的反应实际用户访问页面的真实感受吗?

比如,服务器如果响应一个很小的体积的页面,Load/DOMContentLoaded 会很快触发,然后异步获取内容,之后在页面上显示。这样的页面 Load/DOMContentLoaded 的时间很短,那从用户体验角度讲它的性能表现就是好的吗?

要回答上面问题,也就引出了👇 这个概念。

以用户为中心的性能指标 (user-centric metric):

以用户为中心的性能指标更关注从用户角度看,页面的性能是怎样的?页面呈现的内容是不是满足用户需要,用户交互起来是否流畅等。上面介绍的 FCP、LCP、FMP、FID、CLS、TTI 均是以用户为中心的性能指标

是否正在发生?导航是否成功启动?服务器有响应吗?
是否有用?是否渲染了足够的内容让用户可以深入其中?
是否可用?页面是否繁忙,用户是否可以与页面进行交互?
是否令人愉快?交互是否流畅自然,没有延迟和卡顿?

根据采集方式不同,性能指标的几种形式

根据测量方式的不同,性能数据可以分为:Lab Data 和 Field Data

Lab Data / SYN

SYN 即 synthetic monitoring,收集形式也有叫 in the lab

Lab Data 是在可控的条件下,特定的机型,特定的网络环境,收集的性能数据。一个使用场景是,新页面开发的时候,页面发布到生产环境中之前,是没办法基于真实用户做性能指标测量的,此时,想了解也没性能情况,可以通过 Lab 方式收集和检查。

Field Data / RUM

RUM 即 Real User Monitoring,收集形式也有叫 in the Filed

Lab 的方式测量虽然能反应性能情况,但是真实用户因机型和网络情况各异,页面加载对于不同用户具有很大的不确定性,Lab 数据并不一定是真实用户的实际情况。而 filed 数据很好的解决了这个问题,有一定的代码侵入性,记录真实用户的性能数据,通过 RUM 数据可以发现一些 Lab 数据下很难暴露出来的性能异常。

RAIL Model

我们先来看下 RAIL 这几个字母分别对应什么?

R: response

A: animation

I: idle

L: load

RAIL 是 Google 提出的以用户为中心的一套性能模型,从各个维度反应一个网站的性能情况,同时提供一组性能目标供参考👇

  • Response: 50ms 内对事件做出响应
  • Animation: 动画 10 ms 内生成内一帧(每帧耗时 16ms,用户会感到动画流畅,为什么这里是 10ms?)
  • Idle: 最大化的利用上空闲时间
  • Load: 在 5s 内完成内容传输并达到用户可交互
性能指标是怎么采集的

性能指标相关的 API 及采集方式

Performance Observer[1]

Performance Observer API 下包含一组性能监测相关的 API

  • Paint Timing API
  • Largest Contentful Paint API
  • Event Timing API
  • Navigation Timing API
  • Layout Instability API
  • Long Tasks API
  • Resource Timing API

下面按照不同指标的收集用到的 API 依次介绍它们是怎么用的。

Paint Timing API[2]

用于收集 FP / FCP

new PerformanceObserver((list) => {

  for (const entry of list.getEntries()) {

    console.log('FCP: ', entry.startTime);

  }

}).observe({

  type'first-contentful-paint',

  buffered: true,

});



new PerformanceObserver((list) => {

  for (const entry of list.getEntries()) {

    console.log('FP: ', entry.startTime);

  }

}).observe({

  type'first-paint',

  buffered: true,

});

Largest Contentful Paint API[3]

用于收集 LCP

largest-contentful-paint 事件会在页面加载过程中根据此时已渲染最大元素的变化,不断的被触发,实际上报中会一直监听这些变化,直到用户与页面发生交互行为(比如 click、keydown)或者页面被隐藏或者页面被 unload 等,取监听到的最后值做上报。

new PerformanceObserver(entryList => {

  for (const entry of entryList.getEntries()) {

    console.log('LCP: ', entry.startTime);

  }

}).observe({

    type'largest-contentful-paint',

    buffered: true

});

3cab1ede84c4b6fe9db13983f6f972db.webp0488772b0a66840b977a3dd7db36c002.webp95f6b8313b83744c513c3601d33feb9d.webp380c77048b9e254b719e4446fb86143b.webp

dce8ad55a0595d715896abd8865d660f.webp

完整版代码参考👉 https://github.com/GoogleChrome/web-vitals/blob/main/src/getLCP.ts

这个指标是 Core Web Vitals,但是兼容性不好,iOS 下都是不支持的🙅

8c14e5fa01d9ce8abe78d3b32c303661.webp

Event Timing API[4]

用于收集 FID

监听用户的第一次输入(first-input)事件,FID = 开始处理 input 的时间 - input 操作的起始时间

new PerformanceObserver(list => {

  for (const entry of list.getEntries()) {

    // 开始处理 input 的时间 - input 操作的起始时间

    const FID = entry.processingStart - entry.startTime; 

    console.log('FID:', FID);

  }

}).observe({

  type'first-input',

  buffered: true,

});

Navigation Timing 1.0[5] 或 Navigation Timing 2.0[6]

用于收集 Load / DOMContentLoaded

ac6c9e99a6ef9b5e4b0bfa64a05749b0.webp
new PerformanceObserver(list => {

  for (const entry of list.getEntries()) {

    const Load = entry.loadEventStart - entry.fetchStart;

    console.log('Load:', Load);

  }

}).observe({

  type'navigation',

  buffered: true,

});



new PerformanceObserver(list => {

  for (const entry of list.getEntries()) {

    const DOMContentLoaded = entry.domContentLoadedEventStart - entry.fetchStart;

    console.log('DOMContentLoaded:', DOMContentLoaded);

  }

}).observe({

  type'navigation',

  buffered: true,

});

Layout Instability API[7]

收集 CLS: 将加载过程分块(session),监听 layout-shift 变化获得每次布局变动的 value 值,统计每个 session 的布局变动分数,最大的布局变动分数时段,即为 CLS。

let sessionEntries = [];

let sessionValue = 0;

let metric = {

  value: 0

}

new PerformanceObserver(entryList => {

  for (const entry of entryList.getEntries()) {

    if (!entry.hadRecentInput) {

      const firstSessionEntry = sessionEntries[0];

      const lastSessionEntry = sessionEntries[sessionEntries.length - 1];



      // 如果时间靠近上一个 session, 将本轮 layout-shift 累加进上一个 session

      if (sessionValue &&

          entry.startTime - lastSessionEntry.startTime < 1000 &&

          entry.startTime - firstSessionEntry.startTime < 5000) {

        sessionValue += entry.value;

        sessionEntries.push(entry);

      } else { // 新起一个 session

        sessionValue = entry.value;

        sessionEntries = [entry];

      }



      // 如果当前 session 的 value 大于之前的最大值,替换为现在这个大的

      if (sessionValue > metric.value) {

        metric.value = sessionValue;

        metric.entries = sessionEntries;

        console.log('CLS: ', metric)

      }

    }

  }

}).observe({

    type'layout-shift',

    buffered: true

});

完整代码见👉 https://github.com/GoogleChrome/web-vitals/blob/main/src/getCLS.ts

Long Tasks API[8] & Resource Timing API[9]

TTI 的采集依赖这两个 API,计算过程:

  1. 先采集 FCP,作为起点
  2. 沿时间轴正向搜索时长至少为 5 秒的安静窗口(安静窗口:没有 Long Task 且不超过两个正在处理的网络 GET 请求)
  3. 沿时间轴反向搜索安静窗口之前的最后一个长任务,如果没有找到长任务,则在 FCP 步骤停止执行
  4. TTI 是安静窗口之前最后一个长任务的结束时间,如果没有找到长任务,则与 FCP 值相同

—— Time to Interactive (TTI)[10]

54492a08e3c4ad1c10779f82628273cf.webp

Mutation[11][12]Observer[13]

收集 FMP:通过 MutationObserver 对 DOM 变化进行监听,每次回调根据新旧 DOM 数量、种类、深度等,计算出当前 DOM 树的分数,分数变化最剧烈的时刻视为 FMP ,Load 事件触发后 200ms 停止监听,取最大变动的记录做上报。

new MutationObserver(() => {

  // 这里是剧烈程度分的计算

}).observe(document, {

  childList: true,

  subtree: true,

});

业界性能指标采集工具

In the Lab

  • Chrome DevTools[14]
  • Lighthouse[15]
  • WebPageTest[16]

In the Field

  • web-vitals[17]
参考

Web Vitals[18]

Largest Contentful Paint (LCP)[19]

Cumulative Layout Shift (CLS)[20]

First Input Delay (FID)[21]

Measure performance with the RAIL model[22]

参考资料

[1]

Performance Observer: https://www.w3.org/TR/performance-timeline-2/

[2]

Paint Timing API: https://w3c.github.io/paint-timing/#sec-PerformancePaintTiming

[3]

Largest Contentful Paint API: https://wicg.github.io/largest-contentful-paint/

[4]

Event Timing API: https://wicg.github.io/event-timing/

[5]

Navigation Timing 1.0: https://www.w3.org/TR/navigation-timing/

[6]

Navigation Timing 2.0: https://www.w3.org/TR/navigation-timing-2/

[7]

Layout Instability API: https://wicg.github.io/layout-instability/

[8]

Long Tasks API: https://www.w3.org/TR/2017/WD-longtasks-1-20170907/

[9]

Resource Timing API: https://www.w3.org/TR/resource-timing-2/

[10]

Time to Interactive (TTI): https://web.dev/tti/

[11]

Mutation: https://dom.spec.whatwg.org/#mutationobserver

[12]

: https://dom.spec.whatwg.org/#mutationobserver

[13]

Observer: https://dom.spec.whatwg.org/#mutationobserver

[14]

Chrome DevTools: https://developers.google.com/web/tools/chrome-devtools/

[15]

Lighthouse: https://developers.google.com/web/tools/lighthouse/

[16]

WebPageTest: https://www.webpagetest.org/

[17]

web-vitals: https://github.com/GoogleChrome/web-vitals

[18]

Web Vitals: https://web.dev/vitals/

[19]

Largest Contentful Paint (LCP): http://web.dev/lcp

[20]

Cumulative Layout Shift (CLS): https://web.dev/cls/

[21]

First Input Delay (FID): https://web.dev/fid/

[22]

Measure performance with the RAIL model: https://web.dev/rail/


往期推荐

React 全局状态管理的 3 种底层机制

深入对比 eslint 插件 和 babel 插件的异同点

六个问题让你更懂 React Fiber,了解框架底层渲染逻辑

一文速览 TypeScript 装饰器 与 IoC 机制

你不知道 CSS 可以做的 4 件事

点击下方“技术漫谈”,选择“设为星标
第一时间关注技术干货!
浏览 79
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报