配了三年性能优化,今天才发现 FP 时间戳根本不是「用户看到内容」的时间——Chrome 145 把渲染管线的最后一公里彻底拆开了

很多前端工程师已经掌握了用 PerformancePaintTiming 抓 FP/FCP 的能力,也知道用 Critical CSS 和 defer 来减少 render-blocking,但测量盲区在于——FP 的 startTime 其实是渲染阶段结束的时间,不是像素真正出现在屏幕上的时间。Chrome 145 刚刚把这两个阶段拆成了 paintTime 和 presentationTime,这两个值之间的 gap 才是真正的「最后一公里」。

先说规范背景。W3C 的 Paint Timing 规范自 2021 年就定义了 FP/FCP,但当时 startTime 的语义在 Chromium 实现里其实是「渲染阶段结束的 VSync 时间」,而不是「像素实际到达屏幕的时间」。这两个时间点在慢设备上可以差到 100ms 以上,对于 INP 指标来说这个差距直接决定了用户的交互等待是否被算作「卡」。

Chrome 145(2026 年初)正式把 paintTime 和 presentationTime 暴露出来了。paintTime 是渲染阶段结束、浏览器开始真正绘制像素的时间戳;presentationTime 是像素实际到达屏幕的时间戳。两者的差值就是「渲染管线末端」到「用户看到内容」之间的物理延迟。这个差值在高性能设备上可能只有几毫秒,但在低端 Android 机型或 CPU 节流状态下可以轻易超过 150ms。

实测怎么用?直接上代码:

“`javascript
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType !== “paint”) return;
const paintDone = entry.startTime;
const paintBegan = entry.paintTime;
const pixelsOnScreen = entry.presentationTime;
if (paintBegan && pixelsOnScreen) {
const pipelineDelay = pixelsOnScreen – paintBegan;
console.log(“Paint pipeline gap: ” + pipelineDelay.toFixed(2) + “ms”);
console.log(” render→paint: ” + (paintBegan – paintDone).toFixed(2) + “ms”);
console.log(” paint→screen: ” + (pixelsOnScreen – paintBegan).toFixed(2) + “ms”);
}
}
});
observer.observe({ type: “paint”, buffered: true });
“`

这段代码能告诉你两件事:paintBegan – paintDone 是渲染引擎到开始绘制的时间,如果这里卡住说明 JS 同步任务或 CSS 复杂选择器在作妖;pixelsOnScreen – paintBegan 是 GPU 提交帧到显示器刷新的时间,如果这里卡住说明合成层太复杂或者触发了重绘。

知道了 gap,怎么优化?webuild.community 做过对照实验:同一个页面,3 个 CSS 文件全放 head 里,First Paint 要等 6 秒才出现内容;把首屏只需要的那一个 CSS 内联进 HTML,其余两个加 media=”print” onload=”this.media=all” 异步加载,用户 2 秒就能看到内容,差了整整 4 秒。这 4 秒大部分就卡在 paintTime 之前的渲染阶段——CSSOM 构建把整个渲染管线拦腰截断了。

优化路径按效果排列:

第一步,内联 Critical CSS

只把首屏需要的样式(通常 5-15KB)直接写在 head 的 style 里,这能把 FCP 时间减少 500ms 到 1.5 秒。

第二步,非关键 CSS 异步加载

用 rel=”preload” as=”style” 配合 onload 切换成正式样式表:

“`html
<link rel=”preload” as=”style” href=”non-critical.css” onload=”this.rel=stylesheet”>
“`

第三步,用 media 属性拆分

“`html
<link rel=”stylesheet” href=”mobile.css” media=”(max-width: 600px)”>
“`

不匹配的屏幕宽度下浏览器直接跳过下载,不阻塞渲染。

第四步,检查长任务

如果 paintBegan – paintDone 值很大,用 Long Tasks API 找出来自哪个 JS 任务:

“`javascript
const ltObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn(“Long task: ” + entry.duration + “ms”);
entry.attribution?.forEach(a => console.log(” culprit:”, a.name));
}
});
ltObserver.observe({ type: “longtask”, buffered: true });
“`

最后给一个完整生产监控片段:

“`javascript
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType !== “paint”) return;
const report = { name: entry.name, startTime: Math.round(entry.startTime 100) / 100 };
if (“paintTime” in entry) {
report.paintTime = Math.round(entry.paintTime
100) / 100;
report.presentationTime = entry.presentationTime ? Math.round(entry.presentationTime 100) / 100 : null;
if (report.paintTime && report.presentationTime) {
report.pipelineGap = Math.round((report.presentationTime – report.paintTime)
100) / 100;
if (report.pipelineGap > 50) console.warn(“[paint-timing] Large pipeline gap: ” + report.pipelineGap + “ms”);
}
}
if (typeof sendMetric === “function”) sendMetric(“paint_timing”, report);
}
});
observer.observe({ type: “paint”, buffered: true });
“`

下一步:打开 Chrome DevTools → Performance 面板,先用这个代码片段跑一遍你自己的线上页面,看 pipelineGap 有没有超过 50ms。如果有,先别急着加 CDN 或者换框架,从 Critical CSS 内联开始,那里是大多数团队 ROI 最高的地方。

评论区

0 条评论

登录后可评论。

阿速·性能优化 13 阅读