阅读 6876

前端性能优化指南[7]--Web 性能指标

本篇是此系列第 7 篇, 上一篇:Web 性能标准
下篇预告:页面呈现过程之网络加载篇

对于 Web 开发人员来说,如何衡量一个 Web 页面的性能一直是一个难题。

最初,我们使用 Time to First Byte、DomContentLoaded 和 Load 这些衡量文档加载进度的指标,但它们不能直接反应用户视觉体验。

为了能衡量用户视觉体验,Web 标准中定义了一些性能指标,这些性能指标被各大浏览器标准化实现,例如 First Paint 和 First Contentful Paint。还有一些由 Web 孵化器社区组(WICG)提出的性能指标,如 Largest Contentful Paint 、Time to Interactive、First Input Delay、First CPU Idle。另外还有 Google 提出的 First Meaningful Paint、Speed Index,百度提出的 First Screen Paint。

这些指标之间并不是毫无关联,而是在以用户为中心的目标中不断演进出来的,有的已经不再建议使用、有的被各种测试工具实现、有的则可以作为通用标准有各大浏览器提供的可用于在生产环境测量的 API。

我将这些指标分为三类:文档加载相关、内容呈现相关、交互响应性相关,并基于这些指标提取出与用户最相关的核心指标。

下面一一介绍这些指标的出处、定义以及测量方式。

🐹 文档加载相关

文档加载过程时间线如图,这里主要介绍三个指标:TTFB、DCL 和 Load 时间。

image.png

https://www.w3.org/TR/navigation-timing-2/timestamp-diagram.svg

Time to First Byte(TTFB)

浏览器从请求页面开始到接收第一字节的时间,这个时间段内包括 DNS 查找、TCP 连接和 SSL 连接。

DomContentLoaded(DCL)

DomContentLoaded 事件触发的时间。当 **HTML 文档被完全加载和解析完成之后,DOMContentLoaded **事件被触发,而无需等待样式表、图像和子框架加载完成。

Load(L)

onLoad 事件触发的时间。页面所有资源都加载完毕后(比如图片,CSS),onLoad 事件才被触发。

🦊 内容呈现相关

像 Load 或 DOMContentLoaded 这样的度量并不能反映用户的视觉体验,因为它们的时间点不一定与用户在屏幕上看到内容的时间点对应。

我们需要一些可以能够体现页面内容呈现速度的指标。

First Paint(FP)

由 Web 性能工作组在 W3C 标准  Paint Timing 中提出。

定义

从开始加载到浏览器首次绘制像素到屏幕上的时间,也就是页面在屏幕上首次发生视觉变化的时间。但此变化可能是简单的背景色更新或不引人注意的内容,它并不表示页面内容完整性,可能会报告没有任何可见的内容被绘制的时间。

Note:First Paint 不包括默认的背景绘制,但包括非默认的背景绘制。


这是开发人员关心页面加载的第一个关键时刻——当浏览器开始呈现页面时。**

测量方式

function getFirstPaint() {
  let firstPaints = {};
  if (typeof performance.getEntriesByType === 'function') {
    let performanceEntries = performance.getEntriesByType('paint') || [];
    performanceEntries.forEach((entry) => {
      if (entry.name === 'first-paint') {
        firstPaints.firstPaint = entry.startTime;
      } else if (entry.name === 'first-contentful-paint') {
        firstPaints.firstContentfulPaint = entry.startTime;
      }
    });
  } else {
    if (chrome && chrome.loadTimes) {
      let loadTimes = window.chrome.loadTimes();
      let {firstPaintTime, startLoadTime} = loadTimes;
      firstPaints.firstPaint = (firstPaintTime - startLoadTime) * 1000;
    } else if (performance.timing && typeof performance.timing.msFirstPaint === 'number') {
      let {msFirstPaint, navigationStart} = performance.timing;
      firstPaints.firstPaint = msFirstPaint - navigationStart;
    }
  }
  return firstPaints;
}
复制代码

First Contentful Paint(FCP)

由 Web 性能工作组在 W3C 标准  Paint Timing 中提出。

定义

浏览器首次绘制来自 DOM 的内容的时间,内容必须是文本、图片(包含背景图)、非白色的 canvas 或 SVG,也包括带有正在加载中的 Web 字体的文本。

这是用户第一次开始看到页面内容,但仅仅有内容,并不意味着它是有用的内容(例如 Header、导航栏等),也不意味着有用户要消费的内容。

另外,字体加载是影响 FCP 的一个重要因素,字体通常是需要一段时间才能加载的大文件,有些浏览器在加载字体之前会隐藏文本。为了确保在 webfont 加载期间文本保持可见,可以临时显示系统字体。

如下所示,font-display: swap; 告诉浏览器使用该字体的文本应立即使用系统字体显示。一旦自定义字体就绪,将替换掉系统字体。

@font-face {
  font-family: 'Pacifico';
  font-style: normal;
  font-weight: 400;
  src: local('Pacifico Regular'), local('Pacifico-Regular'), url(https://fonts.gstatic.com/s/pacifico/v12/FwZY7-Qmy14u9lezJ-6H6MmBp0u-.woff2) format('woff2');
  font-display: swap;
}
复制代码

测量方式

见 First Paint 的计算方式。

First Meaningful Paint(FMP)

由 Google 在 Time to First Meaningful Paint: a layout-based approach  中提出。

定义

页面的主要内容绘制到屏幕上的时间,这是一个更好的衡量用户感知加载体验的指标,但仍然不理想。

主要内容的定义因页面而异,例如对于博客文章,它的主要内容是标题和摘要,对于搜索页面,它的主要内容是搜索结果,对于电商的页面,图片则是主要内容。

所以采用 布局数量最大并且 Web 字体已加载 的时刻作为主要内容绘制的近似时间。


测量方式

随着网页的加载与解析,布局对象(Layout Object)被逐步添加到布局树(Layout Tree)中。对于主要内容绘制到屏幕上的时间点,则是通过一种计算布局对象数量的方式来估算的,该算法将(添加到布局树的布局对象数 / max(1, 页面高度/屏幕高度))最大的时刻作为 FMP 的时间点,如果在布局时正在加载字体,则布局变动时间将推迟到显示字体为止,以此来猜测页面的主要内容绘制到屏幕中的时间。

这种计算方式对页面加载的微小差异过于敏感,容易导致结果不一致。此外,度量的定义依赖于特定于浏览器的实现细节,这意味着它不能标准化,也不能在所有 Web 浏览器中实现。

在 Lighthouse 6.0 中已不推荐使用 FMP,建议使用 Largest Contentful Paint 代替。

Largest Contentful Paint(LCP)

由 Web 孵化器社区组(WICG)在 Largest Contentful Paint API 中提出,是一个非标准化的 Web 性能度量。

定义

可视区域中最大的内容元素呈现到屏幕上的时间,用以估算页面的主要内容对用户可见时间。

关于最大内容元素的计算可以查阅 Largest Contentful Paint 规范,此规范提供了 API 可以获取 LCP 时间(如果浏览器实现了此 API 的话,Chrome 浏览器是实现了的)。

测量方式

try {
  const po = new PerformanceObserver((entryList) => {
    const entries = entryList.getEntries();
    const lastEntry = entries[entries.length - 1];

    // 优先取 renderTime,如果没有则取 loadTime
    let lcp = lastEntry.renderTime || lastEntry.loadTime;
    window.perfData.push({
      'LCP', lcp
    });
  });
  po.observe({type: 'largest-contentful-paint'});
} catch (e) {
  // Do nothing 
}
复制代码

Speed Index(SI)

由 Google 在 webpagetest.org 中提出。

定义 

这是一个表示页面可视区域中内容的填充速度的指标,可以通过计算页面可见区域内容显示的平均时间来衡量。

测量方式

首先在浏览器中捕获页面加载的视频,然后对每 100 毫秒间隔的页面截图计算页面填充的百分比,可以得到这样一个曲线(纵轴是页面可视区域内容填充完成度,横轴是时间)。

image.png


图中的 Example 1 和 Example 2 都是在 10s 时页面填充完成,但 Example 1 在 2s 是就已经填充了 80% 的内容,而 Example 2 在 8s 时才填充 80%。

图中阴影部分的面积(即时间-内容填充百分比曲线以上部分)的大小即可表示可视区域内页面内容的填充速度,面积越小,填充速度越快。

如果用时间来衡量,可以这样计算,以此来表示页面可见区域内容显示的平均时间。

Example 1:Speed Index = (80% * 2) + (20% * 10)= 3.6
Example 2:Speed Index = (80% * 8) + (20% * 10)= 8.4
复制代码

这个平均时间可以用来比较首屏内容完整呈现给用户的性能体验,但它计算的不是首屏内容完整呈现这一时刻,不能算是一个用时间来度量的指标。

First Screen Paint(FSP)


由百度在 W3C 标准提案 First Screen Paint 中提出。

定义

页面从开始加载到首屏内容全部绘制完成的时间,用户可以看到首屏的全部内容。

如果说 LCP 是用户看到有效内容的最近似的时间,那么在 FSP 这个时间点用户已经看到了可视区域内完整的内容,可以说是衡量用户视觉体验最合适的指标。

另外,影响首屏内容完整绘制的主要问题是要避免横向屏幕外和纵向屏幕外元素的绘制阻塞首屏内容的渲染,例如在开发过程中,把内容列表代码放在导航代码前面,浏览器会先渲染完列表内容再渲染导航,如果超过屏幕外的列表内容很长,会严重影响首屏内导航显示到屏幕的时间。


测量方式

此指标的计算方式目前还没有非常一致的官方标准,阿里巴巴内部对于 Weex 页面通过采集首屏屏幕内最后一个View 稳定的时间来作为首屏内容全部呈现绘制时间。

🐷 交互响应性相关

Time to Interactive(TTI)

由 Web 孵化器社区组(WICG)在 Time To Interactive 中提出,是一个非标准化的性能度量指标。

定义

表示网页第一次 完全达到可交互状态 的时间点,浏览器已经可以持续性的响应用户的输入。完全达到可交互状态的时间点是在最后一个长任务(Long Task)完成的时间, 并且在随后的 5 秒内网络和主线程是空闲的。

从定义上来看,中文名称叫可持续交互时间或可流畅交互时间更合适。

长任务是需要 50 毫秒以上才能完成的任务

image.png

图片来源于 web.dev

测量方式

TTI 的计算依赖于对 Long Task 和主线程是否空闲的观察,目前并不在 Web 性能标准规范中,但在一些性能监视工具中实现了。

另外,Google 提供了采集 TTI 的 API:tti-polyfill

const ttiPolyfill = require('tti-polyfill');
ttiPolyfill.getFirstConsistentlyInteractive().then((tti) => {
  window.perfData.push({
    'tti': tti
  });
});
复制代码

First CPU Idle(FCI)

由网络孵化器社区小组提出的 First Interactive 指标,并已被用于各种工具中。这个指标在 LightHouse 中称为 First CPU Idle(FCI)。

定义

页面第一次可以响应用户输入的时间。

FCI 和 TTI 都是页面可以响应用户输入的时间。FCI 发生在用户可以开始与页面交互时;TTI 发生在 用户完全能够(可持续) 与页面交互时。第一次可交互与可流畅交互的时间点如何确定可以在 Google 的 First Interactive and Consistently Interactive中查阅。

测量方式

这不是一个在 Web 标准中的指标,在 Lighthouse 中实现了该指标的计算方法。

但在 Lighthouse 6.0 中,已不推荐使用 FCI,原因是虽然 FCI 提供了比 TTI 更有意义的度量,但这种差异还不足以证明维护两个类似度量的合理性。建议考虑使用 Total Blocking Time 和 TTI。

First Input Delay(FID)

此指标由网络孵化器社区小组(WICG)提出,并已被用于各种工具中。

定义

从用户第一次与页面交互(例如单击链接、点击按钮等)到浏览器实际能够响应该交互的时间。

输入延迟是因为浏览器的主线程正忙于做其他事情,所以不能响应用户。发生这种情况的一个常见原因是浏览器正忙于解析和执行应用程序加载的大型 JavaScript 文件。

第一次输入延迟通常发生在第一次内容绘制(FCP)和可持续交互时间(TTI)之间,因为页面已经呈现了一些内容,但还不能可靠地交互。

image.png

如果用户刚好在主线程最忙时与页面交互,延迟响应的时间就会较长,如果与页面交互是在主线程空闲期间,浏览器可能会立即响应。所以对于 FID 这个指标,我们需要关注的是整体的 FID 值分布,而不是单一值。


测量方式

在 WICG 小组制定的 Event Timing 规范(非 Web 标准)中给出了采集 FID 的 API,能不能用于生产取决于各大浏览器是否实现。

另外,Google 提供了一个 JS 库 github.com/GoogleChrom… 用于测量 FID。

Frames Per Second(FPS)

定义

帧率是视频设备产生图像(或帧)的速率,用每秒可以重新绘制的帧数(Frames Per Second,FPS)表示。

重新绘制可能需要重新计算样式、布局和绘制,如果每帧绘制到屏幕的时间在 16.7 ms 以上,每秒绘制的帧数就会小于 60 帧,人眼就能感受到页面出现卡顿,所以 FPS 是衡量应用流畅度的一个非常重要的指标,60fps 是页面流畅的目标,可以为每次绘制提供 16.7ms 的时间预算。

既然帧率与页面重新绘制有关,那我们可以思考两个问题:

  • 哪些情况下会触发重新绘制?

FPS 在电影和游戏中最为常见,但现在被广泛用作衡量网站和网络应用程序性能的指标。

在 Web 性能中,FPS 最常用于衡量动画的性能:如果 FPS 太低,动画会卡顿。

FPS 也可以作为用户与页面交互时页面响应性的一般度量。例如,如果将鼠标移到某个页面元素上会触发执行 JavaScript 来更新页面,这可能会触发回流和重绘,这需要在帧中完成,如果浏览器处理帧的时间过长,将会出现卡顿现象。

再例如,如果在滚动页面时会触发很多复杂的页面更新,并且浏览器无法保持可接受的帧率,那么滚动页面时会显得迟缓或卡顿。

  • 如何降低重新绘制的时间?

重新绘制到屏幕可能需要从构建 DOM 树开始、重新计算样式、布局、绘制等,我们需要尽可能的避免触发这些流程,例如使用 CSS 修改 opacity 属性就不会触发重新布局,可以减少绘制时间。

所以在实现动画时,建议使用性能成本低的  CSS 属性,而不要使用 JavaScript 设置元素

测量方式

FPS 的测量方式可以参考阿里淘系技术部的这篇 无线性能优化:FPS 测试。其中讲到了最佳的方式是使用 Frame Timing API 由浏览器来实现对 FPS 的测量,但由于此 API 的定义规范还是草案,没有浏览器实现。目前大多线上系统采集都是使用 requestAnimationFrame 来测量 FPS。

在页面重绘前,浏览器会执行传入 requestAnimationFrame 的入参函数,此函数一般用来实现连贯的逐帧动画。 但我们可以在此函数中通过计算获得页面的绘制频率,从而你计算出 FPS。

一个示例如下:

// 代码示例来自:《无线性能优化:FPS 测试》
var lastTime = performance.now();
var frame = 0;
var lastFameTime = performance.now();

var loop = function(time) {
    var now =  performance.now();
    var fs = (now - lastFameTime);
    lastFameTime = now;
    var fps = Math.round(1000/fs);
    frame++;
    if (now > 1000 + lastTime){
        var fps = Math.round( ( frame * 1000 ) / ( now - lastTime ) );
        frame = 0;    
        lastTime = now;    
    };           
    window.requestAnimFrame(loop);   
}
复制代码

🎯 核心指标

当打开一个页面,用户可能会看到这样一个变化过程:白屏 → 打底图 → 出现部分内容 → 首屏内容全部出现,但图片还在加载中 → 首屏内容全部出现,图片也已经加载完成。

核心指标.png

一般在首屏大部分内容呈现时,用户才会开始与页面交互, 如点击、向下滑动等。

在首屏内容全部出现之前的这些变化过程,如果时间很长,用户体验会很差。我们需要优化这些过程,让首屏内容尽快呈现,当然,没有这些过程立马呈现首屏内容的用户体验是最佳的。

我们可以用几个核心指标来衡量这些关键变化点,这些核心指标可以用我们前面介绍的一些性能指标来近似。

用户体验核心指标 定义 衡量指标
白屏时间 页面开始有内容的时间,在没有内容之前是白屏 FP 或 FCP
首屏时间 可视区域内容已完全呈现的时间 FSP
可交互时间 用户第一次可以与页面交互的时间 FCI
可流畅交互时间 用户第一次可以持续与页面交互的时间 TTI

关于可交互时间和可流畅交互时间的英文名称,我认为 Time To First Interactive(TTFI) 和 Time To Consistently Interactive(TTCI) 更合适。 FCI 和 TTI 的定义是第一次可交互时间和可持续交互时间,但英文名不容易理解。

但在 Google 的 First Interactive and First Consistently Interactive  这篇文章开篇有强调: 👉更新[ 2018 年 7 月 25 日]:我们已重命名这些指标,以简化与外部开发人员的消息传递。 First Interactive 现在是 First CPU Idle,Time to Consistently Interactive 称为 Time to Interactive(TTI)。这篇文章仍然使用旧名称。


FSP 和 FCI 目前在线上生产环境还没有统一的测量和采集方式,但在线下实验室环境我们可以测量。一般的做法是访问一个页面,当页面打开时在页面中点击或滑动操作,并录制这个过程的视频,将视频以每秒切成 n 张(例如 10 张,每 100 ms 取 1 帧)图片,然后用算法计算页面内容变化,例如:

  • 1、用 OCR(Optical Character Recognition) 算法计算图片中文字的字节数变化,当字节数很少时,可以定义为在白屏状态,当字节数稳定时说明首屏内容已稳定。来回滑动到字节数有变化时刻。当字节数有变化时说明页面是可滑动的,表明可交互。来回滑动三次字节数有变化的时刻,表明页面交互流畅。

image.png

  • 2、查看图像的每个像素,将其与最终图像进行比较,然后计算每个帧匹配像素的百分比。
  • 3、获取图像中颜色的直方图(红色、绿色和蓝色各一个),然后查看页面上颜色的总体分布。我们计算开始直方图(对于第一个视频帧)和结束直方图(最后一个视频帧)之间的差异,并使用该差异作为基线。将视频中每个帧的直方图与第一个直方图的差异与基线进行比较,以确定视频帧的“完整性”。

三种方式各有优劣:

  • 第 1 种方式,不能监控到屏幕内没有文字的图片的变化。
  • 第 2 种方式,对于加载过程中网页会流动的情况不适合。
  • 第 3 种方式,对最终状态非常敏感,是根据最终图像计算进度,不适合页面有视频播放、轮播图的情况。


对于这些衡量用户体验的关键时间点,谷歌也给出了 4 种反映用户体验的指标,详细描述可以查看 以用户为中心的性能指标

用户体验 页面给用户的反馈 衡量指标
是否发生? 导航是否成功启动?服务器是否有响应? 首次绘制 (FP)/首次内容绘制 (FCP)
是否有用? 是否已渲染可以与用户互动的足够内容? 首次有效绘制 (FMP)/主角元素计时
是否可用? 用户可以与页面交互,还是页面仍在忙于加载? 可交互时间 (TTI)
是否令人愉快? 交互是否顺畅而自然,没有滞后和卡顿? 耗时较长的任务

📝 小结

我们从一开始介绍了文档加载相关的指标,但像 Load 或 DOMContentLoaded 这样的度量并不能反映用户的视觉体验,因为它们的时间点不一定与用户在屏幕上看到内容的时间点对应。

First Paint 和 First Contentful Paint 这些以用户为中心的性能指标关注的是初始绘制时间,可以用来衡量页面的白屏时间。但是没有考虑绘制的内容的重要性,例如如果页面显示一个加载指示器,那么 FP 和 FCP 记录的时间点并不是用户最关心的,用户仍然会认为页面不可用。

First Meaningful Paint 和 Largest Contentful Paint 可以帮助我们衡量初始绘制后的用户看到页面一部分内容或主要内容的加载体验。

而 Speed Index 和 First Screen Paint 可以帮助我们衡量用户看到首屏完整内容的加载体验。

First CPU Idle 和 Time to Interactive 可以帮助我们衡量在页面刚加载后的第一次可交互和可持续交互时间, 还可以通过测量 First Input Delay 指标获得用户输入延迟时间,以判断用户操作时的响应性体验。

而 Frames Per Second(FPS)可以帮助我们衡量与页面交互的平滑性(流畅性)体验。

最后,以用户为中心,提出了 4 种衡量用户体验的核心指标:白屏时间、首屏时间、可交互时间和可流畅交互时间(也可称为可持续交互时间)。

image.png

那么如何优化页面的性能,让页面更快的呈现呢?我们就需要了解页面呈现的过程,我将在下一节介绍。

💫 思考

百度在 2014 年向 Web 性能工作组提交了一份关于首屏渲染优化的提案,该规范用于加快移动端用户实际感知到首屏内容展现的速度。规范的主要内容如下:

在 Web 页面代码解析(parse)后,还需要经过布局(layout)与绘制(paint)阶段,才能在屏幕上展示给用户。移动设备屏幕很小,通常很短的代码就能够充满这个屏幕,而这部分内容就是用户首先实际感知到的内容区域。

当一个页面代码由 A、B 两段组成,代码 A 能够表示首屏的所有内容,当 A 完成 Web 内容解析(parse)后,并不能立刻完成布局(layout)与绘制上屏操作(paint)。

内核从 parse 状态转化成 layout 状态,存在若干触发条件,它们包括了解析的 token 数目,解析的时间以及延迟(delay)时间。

内核从 layout 状态转化成 paint 状态,同样存在若干必要条件,这些条件使得内核无法提早退出 layout 流程,进入实际上屏绘制阶段。

所有这些限制条件并没有充分考虑到手机首屏内容的大小,以及实际用户感知内容展现的重要优先级。在复杂的移动网络环境下这种限制对浏览速度的影响更大。

通过定义首屏渲染优化规范,Web 开发者可以指定浏览器进行合适的首屏内容提前绘制(内核可自行判断首屏位置或是由开发者指定首屏位置),从而加快首屏展现速度,显著缩短用户首次看见非白屏页面时间。

<!--指示浏览器自行决定首屏判断机制,进行首屏内容提前绘制优化。-->
<meta name="render-optimize-policy" content="first-screen-advance [;enable]">
复制代码

具体方案可以查阅:


嗯,这个思考题的背景有点长。思考问题如下:

  • 这份提案最终没有成为 W3C 标准, 你认为原因是什么?
  • 首屏渲染完成时间是一个重要指标,但还没有统一的测量方案,你有什么方案来测量首屏时间吗?


欢迎大家在评论区讨论。

😇 系列篇