“我点击了页面为啥没反应? 😢”
FCP和LCP都是描述页面上内容绘制所需时间的指标。尽管它们很重要,但是绘制时间不包含响应时间(就是页面对用户操作做出响应的事件有多快)
FID作为CWV的关键指标之一,它描述了用户对于网站的互动和友好性的第一印象。它测量了从用户触发第一次交互到页面响应所需要的事件。FID是一个真实指标,并不能在实验室环境下模拟。需要真实的用户交互才能测量响应延迟
image.png
为了在实验室中模拟FID,我们建议采用TBT来替换FID。虽然它们测量的东西不同,但是改善TBT时FID也会一同改善。
造成FID较差的主要原因是大量的JS脚本执行。优化JS的解析,编译和执行可以直接影响到FID。

大量的JS执行任务

浏览器不能立即响应的主要原因是用户交互时主线程上有大量的任务在执行。浏览器在主线程繁忙时无法对用户输入做出响应。为了改善可以采用以下方法:

  • 将大的任务拆分开来
  • 优化页面提前做好交互准备
  • 使用web worker
  • 改善JS的执行时间

    拆分大任务

    如果您已经尝试减少在单个页面上加载的 JavaScript 数量,那么将长时间运行的代码分解为更小的异步任务会很有用。
    长任务是 JavaScript 执行期间,用户可能会发现您的 UI 没有响应。任何阻塞主线程 50 毫秒或更长时间的代码都可以被称为长任务。长任务是不必要的(加载和执行的内容超出用户现在可能需要的范围)。拆分长任务可以减少您网站上的响应延迟。

image.png
Chrome DevTools在性能面板中可视化长任务
随着您采用代码拆分和分解长任务等最佳实践,FID 应该会得到显着改善。虽然 TBT 不是现场指标,但它对于检查最终改进交互时间 (TTI) 和 FID 的进度很有用。
更多细节阅读 Are long JavaScript tasks delaying your Time to Interactive?.

优化页面,提前做好交互的准备

导致FID和TBT较差的主要原因是基本都是JS问题:

自身的脚本执行会延长交互响应时间

  • JavaScript内容过多、执行时间过长和分块效率低下会减慢页面响应,影响用户输入的速度,并最终影响 FID、TBT 和 TTI。代码和功能的渐进式加载可以帮助分散这项工作并提前做好交互准备。
  • 服务端渲染的页面可能看起来还不错,但是要注意到用户的交互可能会被脚本执行所阻塞。如果使用基于路由的代码拆分,这可能需要数百毫秒,有时甚至数秒。考虑在构建期间打包传输更多的内容。

以下是优化应用程序脚本加载之前和之后的 TBT 分数。通过将非必要组件的加载(和执行)移出主线程,用户能够更快地与页面交互。
image.png

数据获取会影响交互准备的许多方面

  • 链式请求(例如 JavaScript 和组件的数据提取)将会影响交互延迟。考虑减少链接接口逻辑的调用。
  • 如果有内联大量的数据相关的内容也会延长 HTML 解析时间并影响绘制和交互指标。旨在最大限度地减少需要在客户端进行后处理的数据量。

    第三方脚本执行也会延迟交互

  • 许多站点包含第三方埋点和分析,它们会导致网络繁忙并使主线程周期性地无响应,从而影响交互延迟。探索一下第三方代码的按需加载(例如,在滚动到视口附近之前,不要加载那些首屏广告)。

  • 在某些情况下,第三方脚本会实行抢占机制,这回延迟页面交互就绪的时间。它尝试优先加载您认为可以为用户提供最大价值的内容。

    使用web worker

    通常主线程的任务阻塞是导致输入延迟的主要原因。Web workers可以使部分JS运行在后台。将不涉及界面操作的任务分离至单独的worker线程可以减少主线程的阻塞事件,并且最终达到改善FID的结果。
    在你的网站上通过使用一下类库可以使得web workers更加简单:

  • Comlink: 一个工具库,它抽象了postMessage,使得它使用起来更加简单

  • Workway: 使用webWorker暴露工具库的一个包
  • Workerize: 将依赖包迁移至webWorker

如果你想阅读更多的关于webWorker怎么样在独立线程运行的内容的话,请阅读 Use Web Workers to run JavaScript off the browser’s main thread

减少JS的运行时间

限制页面上的JS代码量可以减少浏览器执行JS代码的事件。这加快了浏览器对用户交互做出响应的速度。
为了减少页面上执行的JS的代码量,你可以采用:

  • 异步加载没有使用到的代码
  • 压缩没有使用到的依赖

    异步加载没使用的代码

    默认情况下所有的JS代码都会造成渲染阻塞。当浏览器遇到一个script标签的时候,他将会暂停正在做的事情,让后开始下载,解析,编译和执行JS代码。因此你应该仅仅下载页面上需要的代码或者和用户交互相关的内容。
    Coverage标签页的功能会告诉你哪些JS代码在你的页面上没有被使用。
    image.png
    为了减少不必要的JS代码,你需要:

  • 将你的代码拆分成多个依赖

  • 异步加载任何不必要的代码,包括第三方的脚本,使用async和defer

代码拆分是将单个大型 JavaScript 包拆分为可以有条件加载的较小块的概念(也称为延迟加载)。现在的新一些的浏览器基本都支持import,这意味着你可以动态加载脚本:

  1. import('module.js').then((module) => {
  2. // Do something with the module.
  3. });

在用户交互(例如更改路由或显示模式)时动态导入 JavaScript 将确保仅在需要时才获取未用于初始页面加载的代码。
除了一般浏览器支持之外,动态导入语法可用于许多不同的构建系统。

  • 如果您使用webpackRollupParcel作为模块打包器,请利用它们的动态导入支持。
  • 客户端框架,如ReactAngularVue提供了抽象,以便在组件级别更容易延迟加载。

阅读这篇文章了解更多内容 Reduce JavaScript payloads with code splitting

除了使用代码拆分外,你可以使用async和defer来延迟加载不必要的JS代码。

  1. <script defer src="…"></script>
  2. <script async src="…"></script>

除非有特定原因不这样做,否则所有第三方脚本都应加载defer或async默认加载。

最小化未使用的 polyfill

如果您使用现代 JavaScript 语法编写代码并参考现代浏览器 API,您将需要对其进行转译并包含 polyfills 以使其在旧浏览器中工作。
在您的站点中包含 polyfill 和转译代码的主要性能问题之一是,如果较新的浏览器不需要它,则不必下载它。要减少应用程序的 JavaScript 大小,请尽可能减少未使用的 polyfill,并将其使用限制在需要的环境中。
要优化您网站上的 polyfill 使用:

  • 如果您使用Babel作为转译器,请使用@babel/preset-env仅包含您计划定位的浏览器所需的 polyfill 。对于 Babel 7.9,启用bugfixes选项以进一步减少任何不需要的 polyfill
  • 使用 module/nomodule 模式来交付两个单独的包(@babel/preset-env也支持通过target.esmodules)
    1. <script type="module" src="modern.js"></script>
    2. <script nomodule src="legacy.js" defer></script>
    许多使用 Babel 编译的较新的 ECMAScript 特性已经在支持 JavaScript 模块的环境中得到支持。因此,通过这样做,您可以简化确保仅将转译的代码用于实际需要它的浏览器的过程。

开发者工具

许多工具可用于测量和调试 FID:

  • Lighthouse 6.0不支持 FID,因为它是一个字段指标。但是,总阻塞时间(TBT) 可以代理FID。改进 TBT 的同时也会改进真实环境下的 FID。image.png
  • Chrome User Experience Report 提供在原始环境下计算合成的真实 FID 值