浏览器之性能指标-FID(二)

简介: 浏览器之性能指标-FID(二)

7. 优化FID得分

如果我们对FID的得分不满意,通常意味着我们需要改进JavaScript或CSS的使用方式。

以下是我们可以进行FID优化的步骤:

优化CSS代码

对于CSS文件,它们需要尽快下载和解析,以便浏览器可以渲染页面的布局。由于这个原因,我们在减少CSS对FID的影响方面的选择是有限的。然而,我们可以参考CSS最佳实践,例如对文件进行压缩和缩小,或删除未使用的CSS代码。

这里多说一句,其实针对CSS优化也有很多的点,我打算单开一个关于CSS优化的文章,所以在这里就不在展开说明了。

优化JavaScript代码

当存在长时间输入延迟时,通常是JavaScript任务造成的。长时间阻塞浏览器的主线程,导致它无法处理用户输入。

以下是我们可以使用的一些策略,以减少JavaScript执行对主线程阻塞的时间:

创建小的异步任务

长时间的任务会阻塞主线程,不允许其处理用户输入。如果将它们分解为较小的任务,用户输入可以在它们之间被处理。尽量保持任务在50毫秒以下以确保安全。

我们使用setTimeout模拟长任务。

function performTaskPart1() {
  console.log("Part 1");
}
function performTaskPart2() {
  console.log("Part 2");
}
function performTask() {
  // 将长任务分成多个小任务。
  performTaskPart1();
  setTimeout(performTaskPart2, 0);
}
performTask();

生成服务器端内容

尽量减少需要在客户端进行后处理的数据量。这样可以减少浏览器渲染页面时需要执行的工作量。

这点尤其针对SPA项目,大部分都采用的CSR(Client Side Rendering)

页面托管服务器只需要对页面的访问请求响应一个如下的空页面

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- metas -->
    <title></title>
    <link rel="shortcut icon" href="xxx.png" />
    <link rel="stylesheet" href="xxx.css" />
  </head>
  <body>
    <div id="root"><!-- page content --></div>
    <script src="xxx/filterXss.min.js"></script>
    <script src="xxx/x.chunk.js"></script>
    <script src="xxx/main.chunk.js"></script>
  </body>
</html>

页面中留出一个用于填充渲染内容的视图节点 (div#root),并插入指向项目编译压缩后

  • JS Bundle 文件的 script 节点
  • 指向 CSS 文件的 link.stylesheet 节点等。

浏览器接收到这样的文档响应之后,会根据文档内的链接加载脚本与样式资源,并完成以下几方面主要工作:

  1. 执行脚本
  2. 进行网络访问以获取在线数据
  3. 使用 DOM API 更新页面结构
  4. 绑定交互事件
  5. 注入样式

其中,执行脚本就需要安装每个前端框架的内置方法,将JS代码生成对应的Virtual DOM,然后在通过浏览器内置API将其转换为DOM, 然后才会进行事件的绑定,这就大大影响了FID

所以,在CSR项目中存在很大的FID,可以尝试用SSR(Service Side Rendering)。(当然,也可以选择类似Svelte的编译型前端框架,但是这不在本文的讨论范围内)

下面是用ExpressNode端生成页面内容。

const express = require("express");
const app = express();
app.get("/", (req, res) => {
  // 服务端内容生成
  const serverContent = "Hello, 柒八九!";
  res.send(serverContent);
});
app.listen(3000, () => {
  console.log("Server started on port 3000");
});

其实,还有很多的渲染方式,可以参考之前的文章XXR部分。


按需加载第三方代码

第三方代码,如分析工具(sentry)或者嵌入式地图(像Google地图百度地图),通常会阻塞主线程。虽然有时分析代码需要在开始时加载以确保整个访问过程正确跟踪,但我们可能会发现页面上的某些第三方代码不需要立即运行。首先优先加载我们认为对用户提供最大价值的内容。

<!DOCTYPE html>
<html>
<head>
  <title>前端柒八九</title>
</head>
<body>
  <!-- 重要 script 优先加载 -->
  <script src="essential.js" defer></script>
  <!-- 非必要 script 可以延后加载 -->
  <script src="non_essential.js" defer></script>
</body>
</html>

当然,我们还可以利用import()React-Router/React中实现业务层面的按需加载。


使用Web Worker

我们可以将一些主线程工作委托给Web Worker,以减轻主线程的工作负担。Web Worker允许将一些JavaScript代码委托给工作线程运行,这意味着主线程的工作较少,输入延迟较少。

用一个简单的demo来说明一下

main.js

const worker = new Worker("worker.js");
// 向work发送消息
worker.postMessage("Hello from the main thread!");
// 接受来自work的消息
worker.onmessage = (event) => {
  console.log("Message from worker:", event.data);
};

worker.js

下面这段代码,运行在Web Worker线程中。和主线程分离,所以不会阻塞主线程。

self.onmessage = (event) => {
  const messageFromMain = event.data;
  console.log("Message from main thread:", messageFromMain);
  // 执行耗时任务
  const result = doSomeWork();
  //将执行结果发送到主线程
  self.postMessage(result);
};
function doSomeWork() {
  // 模拟耗时任务
  return "耗时任务";
}

如果想了解更多关于Web Worker,可以参考我们之前写的Worker线程


推迟未使用的JavaScript代码

使用asyncdefer,以便仅在需要时执行JavaScript代码。如果我们使用的是高版本JavaScript,可以配置ES6模块以按需加载。

这是一个老生常谈的方式,这里就不展开说明了。

<!DOCTYPE html>
<html>
<head>
  <title>前端柒八九</title>
</head>
<body>
  <!-- 推迟到DOM被渲染完成,再执行加载该脚本 -->
  <script src="script.js" defer></script>
</body>
</html>

减少未使用的Polyfill

Polyfill是在用户使用较旧的浏览器时所需的。开发人员使用它们来使用现代JavaScript构建网站,并仍然向不支持某些现代功能的浏览器提供所有功能。

确保在不需要时不运行Polyfill。使用module/nomodule交付独立的包。

<!DOCTYPE html>
<html>
<head>
  <title>前端柒八九</title>
</head>
<body>
  <!-- 为现代浏览器加载支持 ES 模块的现代 JavaScript。 -->
  <script type="module" src="modern.js"></script>
  <!-- 为不支持 ES 模块的旧版浏览器加载传统 JavaScript。 -->
  <script nomodule src="legacy.js"></script>
</body>
</html>

当然,我们现在项目大部分是基于Webpack/Vite等前端工具开发,我们可以在处理的时候,都是利用Bable进行代码转义处理,在Bable中是可以配置相关的配置,来处理针对不同浏览器和特性的polyfill打包问题。


闲时直至紧急

闲时直至紧急(Idle until urgent)是由谷歌提出的一种代码评估策略

这种策略结合了两种最流行的代码评估方法——急切评估(eager evaluation)和惰性评估(lazy evaluation)。

  • 急切评估(eager evaluation)意味着我们的所有代码都会立即运行。这种方法会导致页面加载时间较长,直到完全可交互,但之后运行顺畅,没有任何中断。
  • 惰性评估(lazy evaluation)则是相反的方法——只有在需要时才运行代码。虽然它有其优点,并且对某些网站可能很有用,但惰性评估意味着一旦需要运行代码,我们就会面临输入延迟的风险。
  • 闲时直至紧急(Idle until urgent)将这两种方法的优点结合起来,以提供一种聪明的代码评估方式,从而使输入延迟最小化。

闲时直至紧急(Idle until urgent)允许我们在空闲时段运行代码,充分利用主线程。同时,它保证了紧急需要的代码立即运行。

采用闲时直至紧急的方法是改进首次输入延迟的好方法。由于代码执行仅在空闲时段进行,可以最小化主线程的阻塞时间。


优化输入延迟

当浏览器在用户与网站进行交互时(如点击按钮或链接)响应时间过长时,长时间的输入延迟就会成为一个问题。为了解决这个问题,我们可以使用HTML属性来控制脚本的下载(重新排序脚本文件和优化代码中的图像)或删除不必要的脚本。

以下是一些可采取的措施来减少长时间输入延迟的问题:

  1. 重新排序脚本:通过将关键脚本放在页面的顶部,使其尽早下载并尽快执行。这样可以确保与用户交互相关的脚本能够快速加载和运行。
  2. 优化图像:通过使用适当的图像格式(如WebP)和压缩图像文件大小,减少图像的加载时间。优化图像可以提高页面的加载速度,减少输入延迟。
  3. 删除不必要的脚本:检查网页中的脚本文件,并删除不必要的脚本。只保留必要的脚本,可以减少下载和执行脚本的时间,从而降低输入延迟。
  4. 使用延迟(defer)加载或异步(async)加载:对于某些脚本,我们可以将其设置为延迟(defer)加载或异步(async)加载,以便在页面加载完成后再加载和执行。这样可以防止脚本的下载和执行阻塞页面的呈现和用户交互。

8. 测量FID

可以使用以下工具的字段数据来分析首次输入延迟(FID):

  1. 使用Chrome用户体验报告
  1. PageSpeed Insights
  2. Search Console
  3. Firebase性能监测

如何使用JavaScript测量FID?

我们还可以通过在页面中添加JavaScript代码来测量FID。

有两种方法可以实现这一点:

  1. 使用web-vitals JavaScript库。

shell

npm install web-vitals
yarn add web-vitals

将以下代码添加到我们的页面中,以在控制台中输出FID值:

import {onLCP, onFID, onCLS} from 'web-vitals';
onCLS(console.log);
onFID(console.log);
onLCP(console.log);

关于web-vitals的更多用法,可以参考其官网

  1. 手动添加PerformanceObserver以跟踪输入。 如果我们不想导入web-vitals库,还可以手动使用Event Timing API来跟踪FID。
let firstHiddenTime = document.visibilityState === 'hidden' ? 0 : Infinity;
document.addEventListener('visibilitychange', (event) => {
  firstHiddenTime = Math.min(firstHiddenTime, event.timeStamp);
}, {once: true});
function sendToAnalytics(data) {
  const body = JSON.stringify(data);
  (navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
      fetch('/analytics', {body, method: 'POST', keepalive: true});
}
try {
  function onFirstInputEntry(entry, po) {
    if (entry.startTime < firstHiddenTime) {
      const fid = entry.processingStart - entry.startTime;
      po.disconnect();
      sendToAnalytics({fid});
    }
  }
  const po = new PerformanceObserver((entryList, po) => {
    entryList.getEntries().forEach((entry) => onFirstInputEntry(entry, po));
  });
  po.observe({
    type: 'first-input',
    buffered: true,
  });
} catch (e) {
}

上面代码用于收集FID数据,并将其发送到分析服务。

  1. 首先,代码初始化一个名为firstHiddenTime的变量。
  • 它通过检查当前页面的 visibilityState 是否为 'hidden' 来判断页面是否已隐藏。
  • 如果页面已隐藏,将 firstHiddenTime 设置为 0,否则设置为无穷大(Infinity)。
  • 这样做是为了记录页面首次隐藏的时间,即用户切换到其他标签页或最小化浏览器的时间。
  1. 通过添加visibilitychange事件监听器,当页面的可见性状态发生变化时,触发回调函数。
  • 这里使用了 { once: true } 参数,使回调函数只执行一次。
  • 在回调函数中,将当前事件的时间戳(event.timeStamp)与 firstHiddenTime 比较,并将较小的值更新为 firstHiddenTime
  • 这样,firstHiddenTime 将记录页面的首次隐藏时间。
  1. 定义了一个名为sendToAnalytics的函数,用于将数据发送到分析服务。
  • 函数接受一个 data 参数,它是要发送的数据。
  • 数据会被序列化为 JSON 字符串,并通过 navigator.sendBeacon 方法异步发送到 '/analytics' 接口。
  • 如果浏览器不支持 sendBeacon 方法,则使用 fetch 方法以 POST 请求的方式发送数据,并通过 keepalive: true 选项保持请求的持久连接。
  1. try块中,定义了一个名为onFirstInputEntry的函数,它用于处理PerformanceObserver观察到的首次输入(FID)性能条目。
  • 首次输入是指用户首次与页面交互(例如点击按钮)时,浏览器开始处理输入事件到实际响应的延迟时间。
  1. 创建了一个 PerformanceObserver 对象 po,用于观察页面性能条目,并设置 type 为 'first-input' 并启用 buffered: true 选项,这样可以缓冲已有的性能条目。这使得我们能够获取到首次输入的性能条目。
  2. PerformanceObserver的回调函数中,使用entryList.getEntries()获取到所有的性能条目,并遍历处理这些条目。
  • 对于每个性能条目,我们检查它的 startTime 是否在页面首次隐藏时间 firstHiddenTime 之前,如果是,则计算首次输入的延迟时间(fid),并调用 sendToAnalytics 函数将其发送到分析服务。

9. 最大潜在首次输入延迟(Max Potential First Input Delay)

根据用户首次与页面进行交互的时间,FID可能会有很大的差异,因为浏览器的主线程在页面的生命周期中并不是均衡占用的。因此,一些用户可能根本不会遇到延迟,而其他用户可能会因此感到非常不满意,这取决于他们首次与页面进行交互的时间。

因此,监测页面的最大潜在首次输入延迟(MPFID)可能会有所帮助。它是在FCP后在主线程上运行的最长任务的持续时间

通过测量该任务的持续时间,可以模拟用户在这个长时间任务开始时与页面进行交互,并等待任务完成以处理输入的潜在情况。

优化MPFID涉及多种策略,可以减少最长任务的运行时间或将其分解为较小的块。

MPFID是Lighthouse中的一个实验室指标。要查看它,可以将我们页面的Lighthouse报告导出为JSON文件。

然后,我们可以通过Lighthouse Viewer对报告进行查看。

image.png


10. 能否在Lighthouse测量 FID ?

答案是不能的,我们不能使用像Lighthouse这样的实验室工具来测量FID这种真实用户指标

要注册输入事件,需要实际用户的交互。然而,我们可以借助与FID强相关的指标进行分析和测量。 起到了,隔山打牛的作用。

总阻塞时间(Total Blocking Time,TBT)是一个在实验室中可以测量的指标示例。如果我们改善了TBT,很可能也会改善FID

TBT测量了从FCP可交互时间(Time To Interactive)期间,主线程因无法响应用户输入而被阻塞的时间。

通过改善TBT,我们很可能也会改善首次输入延迟的表现。

image.png


后记

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

相关文章
|
Web App开发 存储 JavaScript
浏览器之性能指标-TTI
浏览器之性能指标-TTI
215 0
|
Web App开发 存储 JavaScript
浏览器之性能指标-INP
浏览器之性能指标-INP
135 0
|
Web App开发 前端开发 JavaScript
浏览器之性能指标-TBT
浏览器之性能指标-TBT
220 0
|
前端开发 JavaScript 搜索推荐
浏览器之性能指标-FID(一)
浏览器之性能指标-FID(一)
258 0
|
Web App开发 编解码 前端开发
浏览器之性能指标-CLS(二)
浏览器之性能指标-CLS(二)
263 0
|
前端开发 JavaScript UED
浏览器之性能指标-CLS(一)
浏览器之性能指标-CLS
232 0
|
存储 缓存 前端开发
浏览器之性能指标-LCP
浏览器之性能指标-LCP
121 0
|
Web App开发 缓存 前端开发
浏览器之性能指标_FCP(二)
浏览器之性能指标_FCP(二)
213 0
|
Web App开发 存储 前端开发
浏览器之性能指标_FCP(一)
浏览器之性能指标_FCP
150 0
|
25天前
|
JSON 移动开发 JavaScript
在浏览器执行js脚本的两种方式
【10月更文挑战第20天】本文介绍了在浏览器中执行HTTP请求的两种方式:`fetch`和`XMLHttpRequest`。`fetch`支持GET和POST请求,返回Promise对象,可以方便地处理异步操作。`XMLHttpRequest`则通过回调函数处理请求结果,适用于需要兼容旧浏览器的场景。文中还提供了具体的代码示例。
在浏览器执行js脚本的两种方式