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
节点等。
浏览器接收到这样的文档响应之后,会根据文档内的链接加载脚本与样式资源,并完成以下几方面主要工作:
- 执行脚本
- 进行网络访问以获取在线数据
- 使用 DOM API 更新页面结构
- 绑定交互事件
- 注入样式
其中,执行脚本就需要安装每个前端框架的内置方法,将JS代码生成对应的Virtual DOM
,然后在通过浏览器内置API将其转换为DOM
, 然后才会进行事件的绑定,这就大大影响了FID
。
所以,在CSR
项目中存在很大的FID
,可以尝试用SSR
(Service Side Rendering)。(当然,也可以选择类似Svelte
的编译型前端框架,但是这不在本文的讨论范围内)
下面是用Express
在Node
端生成页面内容。
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代码
使用async
或defer
,以便仅在需要时执行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属性
来控制脚本的下载(重新排序脚本文件和优化代码中的图像)或删除不必要的脚本。
以下是一些可采取的措施来减少长时间输入延迟的问题:
- 重新排序脚本:通过将关键脚本放在页面的顶部,使其尽早下载并尽快执行。这样可以确保与用户交互相关的脚本能够快速加载和运行。
- 优化图像:通过使用适当的图像格式(如WebP)和压缩图像文件大小,减少图像的加载时间。优化图像可以提高页面的加载速度,减少输入延迟。
- 删除不必要的脚本:检查网页中的脚本文件,并删除不必要的脚本。只保留必要的脚本,可以减少下载和执行脚本的时间,从而降低输入延迟。
- 使用延迟(
defer
)加载或异步(async
)加载:对于某些脚本,我们可以将其设置为延迟(defer
)加载或异步(async
)加载,以便在页面加载完成后再加载和执行。这样可以防止脚本的下载和执行阻塞页面的呈现和用户交互。
8. 测量FID
可以使用以下工具的字段数据来分析首次输入延迟(FID):
如何使用JavaScript测量FID?
我们还可以通过在页面中添加JavaScript代码来测量FID。
有两种方法可以实现这一点:
- 使用
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
的更多用法,可以参考其官网
- 手动添加
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
数据,并将其发送到分析服务。
- 首先,代码初始化一个名为
firstHiddenTime
的变量。
- 它通过检查当前页面的
visibilityState
是否为 'hidden' 来判断页面是否已隐藏。 - 如果页面已隐藏,将
firstHiddenTime
设置为 0,否则设置为无穷大(Infinity
)。 - 这样做是为了记录页面首次隐藏的时间,即用户切换到其他标签页或最小化浏览器的时间。
- 通过添加
visibilitychange
事件监听器,当页面的可见性状态发生变化时,触发回调函数。
- 这里使用了
{ once: true }
参数,使回调函数只执行一次。 - 在回调函数中,将当前事件的时间戳(
event.timeStamp
)与firstHiddenTime
比较,并将较小的值更新为firstHiddenTime
。 - 这样,
firstHiddenTime
将记录页面的首次隐藏时间。
- 定义了一个名为
sendToAnalytics
的函数,用于将数据发送到分析服务。
- 函数接受一个
data
参数,它是要发送的数据。 - 数据会被序列化为 JSON 字符串,并通过
navigator.sendBeacon
方法异步发送到'/analytics'
接口。 - 如果浏览器不支持
sendBeacon
方法,则使用fetch
方法以 POST 请求的方式发送数据,并通过keepalive: true
选项保持请求的持久连接。
- 在
try
块中,定义了一个名为onFirstInputEntry
的函数,它用于处理PerformanceObserver
观察到的首次输入(FID)性能条目。
- 首次输入是指用户首次与页面交互(例如点击按钮)时,浏览器开始处理输入事件到实际响应的延迟时间。
- 创建了一个
PerformanceObserver
对象po
,用于观察页面性能条目,并设置type
为 'first-input' 并启用buffered: true
选项,这样可以缓冲已有的性能条目。这使得我们能够获取到首次输入的性能条目。 - 在
PerformanceObserver
的回调函数中,使用entryList.getEntries()
获取到所有的性能条目,并遍历处理这些条目。
- 对于每个性能条目,我们检查它的
startTime
是否在页面首次隐藏时间firstHiddenTime
之前,如果是,则计算首次输入的延迟时间(fid
),并调用sendToAnalytics
函数将其发送到分析服务。
9. 最大潜在首次输入延迟(Max Potential First Input Delay)
根据用户首次与页面进行交互的时间,FID
可能会有很大的差异,因为浏览器的主线程在页面的生命周期中并不是均衡占用的。因此,一些用户可能根本不会遇到延迟,而其他用户可能会因此感到非常不满意,这取决于他们首次与页面进行交互的时间。
因此,监测页面的最大潜在首次输入延迟
(MPFID)可能会有所帮助。它是在FCP
后在主线程上运行的最长任务的持续时间。
通过测量该任务的持续时间,可以模拟用户在这个长时间任务开始时与页面进行交互,并等待任务完成以处理输入的潜在情况。
优化MPFID涉及多种策略,可以减少最长任务的运行时间或将其分解为较小的块。
MPFID
是Lighthouse中的一个实验室指标
。要查看它,可以将我们页面的Lighthouse
报告导出为JSON文件。
然后,我们可以通过Lighthouse Viewer对报告进行查看。
10. 能否在Lighthouse测量 FID ?
答案是不能的,我们不能使用像Lighthouse
这样的实验室工具来测量FID
这种真实用户指标。
要注册输入事件,需要实际用户的交互。然而,我们可以借助与FID强相关的指标进行分析和测量。 起到了,隔山打牛的作用。
总阻塞时间
(Total Blocking Time,TBT)是一个在实验室
中可以测量的指标示例。如果我们改善了TBT,很可能也会改善FID
。
TBT测量了从FCP
到可交互时间
(Time To Interactive)期间,主线程因无法响应用户输入而被阻塞的时间。
通过改善TBT,我们很可能也会改善首次输入延迟的表现。
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。