页面性能
指标与模型
网页性能的上下游?
选定合适的 API 收集数据,进行简单预处理后,通过 SDK 上报到后端或者云服务。为了展示收集到的数据,可以设置 99 线等图表,在某些特定的业务场景,还可以设置 A/B 图和告警。
性能指标和性能模型的不同?
两个立场不同,性能指标如 Navigation Timing 使用绝对的量来衡量页面各项性能参数,性能模型如 Web Vitals 使用算法定性分析页面加载时的用户体验。
为什么需要性能模型?
用户体验很难用具体或者绝对的量来衡定,比如说在渐进式加载中,只对比白屏时间和 onload 时间只能说明用户提早看到了内容,却不能说明渐进式加载好在哪里,好了多少。

合成监控和真实用户监控是什么?
从技术上来说,可以把监控分为合成监控(Synthetic Monitoring,SYN)和真实用户监控(Real User Monitoring,RUM)。合成监控是在一个模拟的场景中,使用 Lighthouse 等工具, 提取出性能指标并获得审计报告。真实用户监控是在真实用户访问时,通过数据源获取数据,再上报到日志服务器,以用来展示和分析这么一流程。
Web Vitals 有哪些指标?
Core Web Vitals 包含 LCP、INP、 CLS,分别衡量加载性能、交互响应和视觉稳定性。LCP 需在 2.5 秒内完成,INP 需在 200 毫秒内响应,CLS 需保持在 0.1 以下。 LCP、INP、CLS 均处于「稳定」生命周期阶段,定义和阈值每年最多更新一次。
Core Web Vitals 的三阶段生命周期
Google 将 Core Web Vitals 指标按「实验性 → 待处理 → 稳定」三阶段推进。实验性阶段允许根据社区反馈进行重大修改;待处理阶段至少保持六个月,给生态预留适应窗口;稳定阶段每年最多更新一次,当前 LCP、CLS、 INP 均处于稳定阶段。INP 即从实验性指标逐步替换 FID 的典型演进。
第 75 百分位评估标准
Google 建议以 P75(第 75 百分位)作为 Core Web Vitals 的合规阈值,并按移动端与桌面端分别统计。若网页在 LCP、INP、CLS 三项指标的 P75 均达到推荐值,则视为通过评估。 这一标准平衡了大多数用户的体验质量与尾部异常值的干扰。
实验室监控与真实用户监控的互补角色
Core Web Vitals 首先是真实用户指标(RUM),反映设备能力、网络状况和交互方式的实际差异。实验室测量(Lighthouse 等工具)适合开发阶段预防回归。两者互补,不可互相替代。辅助指标 TTFB、 FCP 用于诊断 LCP 问题来源(服务器响应或渲染阻塞),TBT 用于诊断 INP 的潜在交互延迟。
有哪些开发工具?
- Web Vitals,获取 Web Vitals 评分。
- simple-web-perf,Navigation Timing 数据上报封装。
- webpack-lighthouse-plugin,在打包结束后自动运行 Lighthouse 并输出报告。
- lighthouse-parade,递归抓取页面并评分,输出关于网站的完整报告。

使用 web-vitals JS 库采集指标
web-vitals 库提供了 onCLS、onINP、onLCP 等 API,以与 Google 工具一致的计算方式测量指标。接入后通过 navigator.sendBeacon 或 fetch 将数据上报到分析端点,
再按 P75 汇总即可判断页面是否达标。
import {onCLS, onINP, onLCP} from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify(metric);
(navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
fetch('/analytics', {body, method: 'POST', keepalive: true});
}
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
如何测量性能指标
怎么测定页面帧率?
可以使用 requestAnimationFrame,但是不推荐,因为脚本本身就会对性能造成影响。
可以使用哪些 API 测量性能?

Performance Timing 被弃用了?
Performance Timing 被弃用了,取而代之的是 Performance Navigation Timing。不应该再使用 performance.navigation、performance.getEntries 等 API, 这些旧的 API 没有办法肩检测如加载新脚本等情况,不支持新的性能指标如 Long Tasks API,此外可能干扰页面性能,所以推荐使用 Navigation Timing API 获取数据, 并使用 Performance Observer 进行侦测。

Navigation Timing 中不同的阶段是连续的吗?
可能不是。如浏览器有同域下最多 6 个请求并行的限制,那么 domainLoopupEnd 到 requestStart 之间可能会出现较长的等待时间(Stalled Time)。此外,不一定每个阶段都会有数据,如:未发生跳转时, redirectCount 为 0;如果页面没有 service worker,那么 workerStart 为 0;DNS 从缓存中获取时,domainLoopupStart 和 domainLoopupEnd 可能相等。
PerformanceObserver 是什么?
"有效的性能测量的第一条规则是确保性能测量技术本身不会导致性能问题",使用 Performance Observer 可以获取某个具体类型的指标的同时不会干扰或影响页面性能,因为它会在浏览器空闲时期执行。
try {
// "element"、"event"、"first-input"、
// "largest-contentful-paint"、"layout-shift"、
// "longtask"、"mark"、"measure"、
// "navigation"、"paint"、"resource"
PerformanceObserver.supportedEntryTypes.map(type => {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.toJSON())
}
}).observe({
type,
// 使用 buffered 可以在第一次回调时获取缓存区中的历史记录
buffered: true
})
})
} catch {
// nothing
}
Long Tasks API 是什么?
Long Tasks API 将会汇报使主线程阻塞超过 50ms 的任何任务。通过观测阻塞时长,可以获得诸如 TTI(Time To Interactive)、TBT(Total Blocking Time)等数据。
如何测量函数执行的时长?
可以使用 Date.now()、performance.now(),但是如果不考虑兼容性的话,使用 User Timing API 要更好一些,它可以和相关套件很好的结合:比如说在 Performance 面板中可视化, 或通过 Performance Observer 进行观测。
performance.mark('myTask:start')
await task()
performance.mark('myTask:end')
performance.measure('myTask', 'myTask:start', 'myTask:end')
如何测量元素的渲染时间?
可以使用 Element Timing API。通过给 element 增加 elementtiming 属性,可以在注册 Performance Observer 后监听 element 类型的事件并获得元素渲染时间。 LCP 指标就是建立在 ET API 基础上的,只是汇报的是最大内容元素的渲染时间。
<img elementtiming="hero-image" />
<script>
new PerformanceObserver(/* ... */)
.observe({ type: 'element', buffered: true })
</script>
Event Timing API 是什么?
Resource Timing API 是什么?
Navigation Timing API 是什么?
类似于 Resource Timing API,但不同的地方在于它会在导航时被触发,还附带了 DOMContentLoaded 和 load 事件的触发事件。
除了规范定义数据,还可以上报哪些?
5W:时间、地理位置、页面 URL、浏览器、系统、账号 ID、现场还原。网络:页面加载方式、Service Worker、HTTP 协议版本、资源压缩方式。其它:页面在前台还是后台。
时间到交互性 (Time to Interactivity)
Zach Leatherman 提出的指标,关注代码已落地浏览器但交互尚未就绪的"死亡区域"。这一指 标帮助开发者量化从可看到可用的间隔,为性能优化谈判提供具体依据。
见:Web of State of the Browser Day Out:Remy Sharp 的会议回顾
Links
性能优化
做性能优化的基本法则是什么?
编译层参见 webpack 优化;网络层减少请求;客户端预加载、预渲染、预执行。以及两句名言:"空间换时间"、"串行改并行"。
如何优化复杂动画的性能?
复杂的 DOM 动画(如多个独立元素同时运动)在低端设备上可能造成性能瓶颈。
CSS Sprite 动画是一种高性能替代方案,通过 object-position 切换单张 Sprite 图的不同区域来实现帧动画,相比多元素方案大幅减少了渲染开销。
第二页加载优化的陷阱
Harry Roberts 指出一个反直觉现象:当跳出率很高时,用大量 JavaScript"优化"第二页加载 是过早优化,反而带来负面成本。这提醒性能优化需要结合实际业务数据,而非盲目追求指标。
见:Web of State of the Browser Day Out:Remy Sharp 的会议回顾
SSR 及混合应用优化
Speculation Rules API 是什么?
Chrome 推出的推测规则 API,允许开发者在用户实际导航到网页之前就开始加载网页。目前支持两种推测类型:
- 预提取 (prefetch):仅获取 HTML 文档,不加载子资源
- 预渲染 (prerender):获取子资源并执行渲染,几乎像在隐藏标签页中打开网页
Prerender Until Script 是什么?
完全预渲染虽然性能提升显著,但存在实现复杂度和副作用风险(如分析脚本提前触发、状态管理复杂等)。Prerender Until Script 是介于预提取和预渲染之间的中间方案:
- 预提取 HTML 文档 + 渲染页面 + 获取子资源
- 关键区别:遇到
<script>元素时暂停解析,等待用户导航后再继续 - 无脚本的页面将完全预渲染,有阻塞脚本的页面至少能提前开始渲染和资源预加载
使用方法:
{
"prerender_until_script": [{
"where": { "href_matches": "/*" }
}],
"prefetch": [{
"where": { "href_matches": "/*" }
}]
}
降级策略:同时配置 prefetch 可实现对不支持浏览器的优雅降级。
CSR 是什么?
Client Side Rendering,和 SSR 相对应,指页面渲染、逻辑、路由、请求都是在浏览器发生的。想要提高 CSR 项目的用户体验,可以考虑在编译时通过预渲染首屏、骨架屏等形式对性能指标进行优化,或者,上 SSR。
从 CSR 到 SSR 有哪些过渡方案?
CSP -> CSP with prerender -> SSR with hydration -> static SSR -> SSR

见:TODO,Rendering on the Web、CSR、SSR、NSR、ESR傻傻分不清楚
ESR 是什么?
经典直出方案是什么?
直出指在后端渲染好 HTML 后再回传给客户端这种方式,直出可以节约许多 HTML 渲染及 AJAX 请求的时间,对 Web 前端的侵入性最小。
离线包缓存是什么?
直出方案仍需要把时间花在请求、加载 HTML 上,离线包缓存意味着把 HTML、CSS、JS、数据提前缓存在本地,只有部分需要异步更新的页面才通过请求的方式加载新数据并重新渲染页面。

容器化方案是什么?
TODO,http://www.alloyteam.com/2020/06/fast-open-h5/
Chrome 144 Web App 更新机制有哪些改进?
Chrome 144 对可安装 Web 应用(PWA)的更新流程进行了四项关键优化:
- 名称和图标更新改为可选操作:不再强制弹出干扰性对话框,用户可通过三点菜单的"查看应用更新"选项自主选择是否应用这些身份更改,也可以选择完全忽略
- 智能图标下载:如果清单中的
icons字段 URL 未变化,则直接认为图标未改变,避免不必要的下载(视为Cache-Control: immutable) - 移除更新频率限制:由于不再每次都下载图标,每天一次的限制被取消,非安全敏感型成员可立即更新
- 视觉差异阈值:像素级差异小于 10% 的图标更新会自动应用,避免 CDN 重新编码等微小变化触发用户确认对话框(此豁免每天限一次,防止滥用)
PWA 怎么配合 SSR 做优化?
PWA 能够通过 Service Worker 对缓存进行精细化控制,在客户端使用无头浏览器提前加载 HTML,等到实际加载时请求的就是由 Service Worker 缓存下来的资源了。

NSR 是什么?
Native Side Render,GMTC 2019 UC 团队提到的一种"前端 SSR"方案,它借助浏览器启动一个额外的 JS Runtime,将提前下载好的 HTML 模板和数据预渲染出来。 这种方案的瓶颈在于他会带来额外的流量和性能开销,所以如何预测用户行为,提高命中率是非常重要的事。
Links
阅读更多
《React 16 加载性能优化指南》
作者把页面加载状态划分了四个步骤:
- 打开页面到首屏渲染(浏览器首次渲染可见的内容)
- 首屏渲染到首次内容渲染(相关业务的内容)
- 内容渲染到页面可交互
- 页面逐渐加载(诸如图片等元素)直到页面内容加载完毕
在很多应用中,首屏渲染约等于首次内容渲染,这是因为加载 HTML 和 CSS 之后,要等待业务 JS 加载完毕才能看到有意义的内容。其实可以通过几种方法完善首屏加载的 HTML 和 CSS,增加加载状态,减少用户等待焦虑。
- 在 HTML 模板中使用 SVG 图案(比如通过 html-webpack-plugin 自动插入)
- 使用 pupeteer 等工具预渲染首屏
业务内容加载的过程中,JS 的影响最大。业务 JS 代码可以大致划分为:基础框架、垫片、业务基础库、业务代码这四大类,所以讲到优化 JS 其实是说如何通过分缓存去优化几种不同类型的 JS。
- 基础框架需要长时间缓存,适合强缓存
- 使用 polyfill.io,根据 UA 自动垫片
- 使用 SplitChunksPlugin 替代 CommonChunksPlugin
- 使用 TreeShaking
- 如果打包出来的 Bundle 体积巨大,可以考虑使用代码分割功能,比如 React Loadable 动态载入代码。
- 为绝大多数用户提供 ES6+ 的代码,使用 nomodule 标志为老的浏览器保留 ES5 版本代码
最后,在页面可交互到页面加载完毕这个阶段,可以使用图片懒加载或者骨架屏等形式提高体验。
关于性能优化的9大策略和6大指标
- 网络层面
- 构建工具优化
- include/exclude 避免不必要的查找,比如用在 babel-loader 可以避免不必要的转译
- babel-loader 可配置 cache,只编译修改过的文件
- alias 优化文件查找性能
- thread-loader 多进程编译
- BundleAnalyzer 分析文件体积
- SplitChunks 替换 CommonChunks,渐进式加载
- 摇树优化
- 按需加载
- 动态垫片
- 变量提升
- 压缩,比如 terser
- 图像策略
- 内容分发
- 域名分开,避免 Cookie
- 静态资源走 CDN
- 构建工具优化
前端内存泄漏实证:86% 代码库存在未清理模式
Stack Insight 对 500 个开源仓库的静态分析显示,86% 的代码库至少存在一种缺失清理模式。 扫描发现 55,864 个潜在泄漏实例,分布于 714,217 个文件中。
泄漏模式分布:
- 定时器未清理:43.9%(24,538 例)
- 事件监听器未移除:19.0%(10,616 例)
- 订阅未取消:13.9%(7,740 例)
- Effect 未返回清理函数:9.3%(5,200 例)
基准测试结果:100 次挂载/卸载循环 × 50 次独立运行,强制 GC 后测量。 每个未处理模式每周期保留约 8 KB 堆内存,正确清理时增长接近零。