Web性能优化-性能指标(三)

2,042 阅读7分钟

以往大部分工具仅将加载性能作为web性能衡量指标,但是随时都有可能发生性能不佳的情况,不只限于加载期间。应用无法快速响应点击、无法平滑滚动或动画卡顿都会导致糟糕的体验。用户关心的是总体体验,加载时间或 DOMContentLoaded时间等传统性能指标极不可靠,因为加载发生的时间可能与用户认为的应用加载时间对应,也可能不对应。

需要思考以下问题:

  1. 哪些指标能够最准确地衡量人所感受到的性能?
  2. 如何针对实际用户来衡量这些指标?
  3. 如何解读衡量结果以确定应用是否“速度快”?
  4. 了解应用的实际用户性能之后,如何避免性能下降并在未来提高性能?

以用户为中心的性能指标

当用户导航到网页时,通常会第一时间寻找视觉反馈。

是否发生? 导航是否成功启动?服务器是否有响应?
是否有用? 是否已渲染可以与用户互动的足够内容?
是否可用? 用户可以与页面交互,还是页面仍在忙于加载?
是否令人愉快? 交互是否顺畅而自然,没有滞后和卡顿?

首次绘制(FP)与首次内容绘制(FCP)

Paint Timing API 定义了两个指标:首次绘制 (FP:First Paint) 和 首次内容绘制 (FCP:First Contentful Paint),这些指标用于标记页面加载之后浏览器在屏幕上渲染像素的时间点。 这对于用户来说十分重要,因为它回答了以下问题: 是否发生?

FP和FCP的差别:FP 标记浏览器渲染任何在视觉上不同于导航前屏幕内容之内容的时间点,FCP 标记的是浏览器渲染来自 DOM 第一位内容的时间点,该内容可能是文本、图像、SVG 甚至 <canvas> 元素。

首次有效绘制和主角元素计时(FMP)

首次有效绘制 (FMP:First Meaningful Paint) 指标能够回答“是否有用?”。在网页上,几乎总有一部分内容比其他部分更重要。 如果页面最重要的部分能迅速加载,用户可能不会注意到其余部分是否加载。

耗时较长的任务(long task)

浏览器通过将任务添加到主线程的队列等待逐个执行来响应用户输入。 浏览器执行 JavaScript 时也会这样做,因此从这个角度看,浏览器为单线程。如果某个任务花费较长时间,主线程就会遭到阻止,而队列中的所有其他任务都必须等待。 Long Tasks API可以将任何耗时超过 50 毫秒的任务标示为可能存在问题,选择 50 毫秒的时间是为了让应用满足在 100 毫秒内响应用户输入的,具体查看Web性能评估模型

可交互时间(TTI)

可交互时间 (TTI) 指标用于标记应用已进行视觉渲染并能可靠响应用户输入的时间点。 应用可能会因为多种原因而无法响应用户输入:

  • 页面组件运行所需的 JavaScript 尚未加载。
  • 耗时较长的任务阻塞主线程

TTI 指标可识别页面初始 JavaScript 已加载且主线程处于空闲状态(没有耗时较长的任务)的时间点。

其他指标

  • LCP(Largest Contentful Paint) 最大内容渲染

    代表在viewport中最大的页面元素加载的时间. LCP的数据会通过PerformanceEntry对象记录, 每次出现更大的内容渲染, 则会产生一个新的PerformanceEntry对象。

  • TBT (Total Blocking Time) 页面阻塞总时长

    TBT汇总所有加载过程中阻塞用户操作的时长,在FCP和TTI之间任何long task中阻塞部分都会被汇总。

  • SI (Speed Index)

    该指标用于显示页面可见部分的显示速度, 单位是时间,

指标与视觉反馈对应

体验 指标
是否发生? 首次绘制 (FP)/首次内容绘制 (FCP)
是否有用? 首次有效绘制 (FMP)/主角元素计时
是否可用? 可交互时间 (TTI)
是否令人愉快? 耗时较长的任务(在技术上不存在耗时较长的任务)

衡量指标

在没有api之前,都是通过以下代码检测常任务的

(function detectLongFrame() {
  var lastFrameTime = Date.now();
  requestAnimationFrame(function() {
    var currentFrameTime = Date.now();

    if (currentFrameTime - lastFrameTime > 50) {
      // 记录长任务
    }

    detectLongFrame(currentFrameTime);
  });
}());

虽然大部分情况下此代码都行得通,但其也有不少缺点:

  1. 此代码会给每个帧增加开销。
  2. 此代码会阻止空闲块。
  3. 此代码会严重消耗电池续航时间。

性能测量代码最重要的规则是不应降低性能。

得益于新增的几个浏览器 API,可以在实际设备上衡量这些指标,而无需使用大量可能降低性能的变通方法。 这些新增的 API 是 PerformanceObserver、PerformanceEntry 和 DOMHighResTimeStamp

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.entryType);
    console.log(entry.startTime); // DOMHighResTimeStamp
    console.log(entry.duration); // DOMHighResTimeStamp
  }
});

observer.observe({entryTypes: ['resource', 'paint']});

PerformanceObserver 为我们提供的功能是,能够在性能事件发生时订阅这些事件,并以异步方式响应事件。

衡量FP和FCP

const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      // `name` 可能为 'first-paint''first-contentful-paint'.
      const metricName = entry.name;
      const time = Math.round(entry.startTime + entry.duration);

      console.log({
        eventCategory:'Performance Metrics',
        eventAction: metricName,
        eventValue: time,
        nonInteraction: true,
      });
    }
});
observer.observe({entryTypes: ['paint']});

// or

performance.getEntriesByType('paint')

注意:必须确保 PerformanceObserver 在任何样式表之前于文档的 <head> 中注册,以使其在 FP/FCP 发生前运行。

衡量 FMP(此指标暂无标准)

确定页面上的主角元素之后,可以跟踪为用户呈现这些元素的时间点。目前尚无标准化的 FMP 定义,因此也没有性能条目类型。

衡量 TTI

可以使用TTI Polyfill检测TTI,polyfill 公开 getFirstConsistentlyInteractive() 方法, 返回使用 TTI 值进行解析的 promise。

import ttiPolyfill from './path/to/tti-polyfill.js';

ttiPolyfill.getFirstConsistentlyInteractive().then((tti) => {
  console.log({
    eventCategory:'Performance Metrics',
    eventAction:'TTI',
    eventValue: tti,
    nonInteraction: true,
  });
});

衡量输入延迟

阻塞主线程的耗时较长任务可能会导致事件侦听器无法及时执行。为提供流畅的界面体验,界面应在用户执行输入后的 100 毫秒内作出响应。

若要在代码中检测输入延迟,可将事件时间戳与当前时间作比较,判断两者相差是否超过 100 毫秒。

const subscribeBtn = document.querySelector('#subscribe');

subscribeBtn.addEventListener('click', (event) => {
  // 处理事件逻辑

  const lag = performance.now() - event.timeStamp;
  if (lag > 100) {
    console.log({
      eventCategory: 'Performance Metric'
      eventAction: 'input-latency',
      eventLabel: '#subscribe:click',
      eventValue: Math.round(lag),
      nonInteraction: true,
    });
  }
});

由于事件延迟通常是由耗时较长的任务所致,因此可将事件延迟检测逻辑与耗时较长任务检测逻辑相结合:判断某个耗时较长的任务在 event.timeStamp 所示的时间阻塞主线程。

优化策略

优化 FP/FCP

从文档的 <head> 中移除任何阻塞渲染的脚本或样式表,可以减少首次绘制和首次内容绘制前的等待时间。

确认最小的样式集并将其内联到 <head> 中(或者使用 HTTP/2 服务器推送)),即可实现极短的首次绘制时间。

优化 FMP/TTI

确定页面上最关键的界面元素(主角元素)之后,应确保初始脚本加载仅包含渲染这些元素并使其可交互所需的代码。

避免出现耗时较长的任务

拆分代码并按照优先顺序排列要加载的代码,不仅可以缩短页面可交互时间,还可以减少耗时较长的任务。

除了将代码拆分为多个单独的文件之外,还可将大型同步代码块拆分为较小的块,以便以异步方式执行,或者推迟到下一空闲点。以异步方式在较小的块中执行此逻辑,可在主线程中留出空间,供浏览器响应用户输入。