Performance API 实战:用 Navigation/Resource Timing 做前端性能监控(Web Vitals)

#PerformanceAPI实战:用Navigation/ResourceTiming做前端性能监控(WebVitals)我第一次正儿八经做前端性能监控,是被用户逼的。那会儿线上有个活动页,运营天天在群里@我:"…

Performance API 实战:用 Navigation/Resource Timing 做前端性能监控(Web Vitals)

我第一次正儿八经做前端性能监控,是被用户逼的。

那会儿线上有个活动页,运营天天在群里 @ 我:"哥,用户说这页怎么这么卡,点进去转半天圈。"我嘴上说着"马上看",心里其实一点底都没有——因为我根本不知道它到底慢在哪。我拍脑袋写了段"监控":在入口文件顶部记一个 Date.now(),在 window.onload 里再记一个 Date.now(),俩一减就是"页面加载时间",然后定时上报。

结果你猜怎么着?上报到监控平台的曲线跟心电图似的上蹿下跳:有时候是负数,有时候是 0,有时候飙到 8000 多毫秒。我被技术主管拎去问话:"你这监控是装饰品吧?负时间是怎么回事?"我当场尬住,回去一查才发现,一是我那两个 Date.now() 打点位置不靠谱,二是有些数据在 onload 之前就被我读出来上报了,拿到一堆 undefined 然后算成了 NaN。那一刻我才意识到,我对浏览器的 Performance API 几乎一无所知,之前那套"监控"纯属自欺欺人。

还有个更丢人的事:我以为 performance.timing 里的 navigationStart 永远是 0,直接拿 loadEventEnd 减它就算总耗时。结果在 SPA 里,路由切换根本不走整页刷新,performance.timing 这套老接口拿到的全是首屏那一次的值,后面切多少页它都不带变的——我盯着一张永远不更新的"加载时间"图表看了半天,还以为是缓存没刷新。

后来我沉下心把 Navigation Timing、Resource Timing、还有现在大家挂在嘴边的 Web Vitals 啃了一遍,才明白浏览器早就把页面从敲下回车到完全加载的每一帧、每一毫秒都记好了,就等你来查。这篇文章就是我把踩过的坑、查过的文档、还有改了又改的代码摊开来讲一遍——不整虚的,全是我自己项目里跑通过的。你会看到怎么用 Navigation Timing 给整页做"体检",怎么用 Resource Timing 揪出拖慢首屏的那张图,怎么用 PerformanceObserver 拿到 FCP、LCP、CLS 这些和用户感知直接挂钩的指标,最后怎么把这一堆东西串成一个不阻塞主线程的轻量监控 SDK。

先把我现在能拍胸脯下的结论摆出来,省得你跟我一样绕弯路:浏览器已经把性能数据记好了,我们做的是"查询"而不是"测量";老接口 performance.timing 早该进博物馆了,新代码一律用 performance.getEntriesByType();Web Vitals 这种动态指标别用轮询去刮,交给 PerformanceObserver 异步监听才靠谱。下面分模块讲,每个模块都给可运行的代码和中文注释。

一、Performance API 到底是什么:它已经替你测完了

先说清楚 Performance API 到底是什么。它其实不是某一个函数,而是 W3C 定义的一整套"性能时间线(Performance Timeline)"标准族,底下挂着 High Resolution Time、Navigation Timing、Resource Timing、User Timing、Paint Timing 等一票子标准。你可以把它理解成浏览器给你装好的一个黑匣子:页面从输入网址回车那一刻起,DNS 查了多久、TCP 握了多久、服务器多久吐出第一个字节、DOM 解析了多久、每张图片每个脚本加载了多久——所有时间点都默默填进一个叫 performance 的全局对象里。

我们要做的不是"去测",而是"去查"。它已经测完了,你只管读。这也是 Performance API 最舒服的地方:零侵入、零额外开销,数据还是真实用户在真实网络下的真实表现(也就是大家常说的 RUM,Real User Monitoring),比你在办公室千兆网速、万元设备上测出来的"假性能"有说服力一万倍。

这里得区分两种性能数据,否则你很容易陷入"本地测着挺快、一上线就被用户骂"的怪圈。一种是"实验室数据"(Lab Data),比如你本地跑 Lighthouse、在 DevTools 里录一段,它干净、可控、可复现,但只代表你那一台机器的那一次加载;另一种是"现场数据"(Field Data),也就是 Performance API 在真实用户设备上采集到的,它杂乱、有长尾、但真实。Google 的 Web Vitals 评级看的是现场数据,所以你 Lighthouse 跑出绿色,绝不代表用户不卡——真正要长期盯的,是这一章后面讲的全量 RUM 上报,而不是某一次本地自测的漂亮分数。

我刚接触时犯过一个低级错误:以为这玩意儿得自己埋点才能用。后来才发现完全相反——你啥都不用干,浏览器自己就在记录。你只需要知道"去哪个抽屉拿哪个指标"。下面三章,就是三张抽屉的使用说明书。

二、Navigation Timing:整页加载的"体检报告"

第一个抽屉,是页面整体加载的明细。这是排查"首屏慢"的入口。

很多人(包括当年的我)只知道一个老接口 performance.timing,那是 Navigation Timing Level 1 的产物。我得吐槽一句:这玩意儿就像你前任——早该放下了,但老项目里还躺着。W3C 已经明确把它标记为废弃,新代码请一律用 Navigation Timing Level 2 的写法:performance.getEntriesByType('navigation')。它返回的是PerformanceNavigationTiming 对象,字段比老的多了不止一星半点,还顺手修正了老接口里几个反人类的设计(比如老接口所有时间都是相对 navigationStart 的绝对毫秒数,新接口统一用 DOMHighResTimeStamp,精度更高还自带基准)。

字段多得能让人犯密集恐惧症,但真正天天用的就那几个。我习惯把它们的含义画成一条流水线记在脑子里:navigationStart(你按回车那一下)→ fetchStart(浏览器准备发请求)→ domainLookupStart/End(DNS 查询)→ connectStart/End(TCP 握手)→ requestStart(请求发出)→ responseStart(收到第一个字节,也就是 TTFB 时刻)→ responseEnd(收完)→ domInteractive(DOM 解析完能交互)→ domContentLoadedEventEnd(DOMContentLoaded 跑完)→ loadEventEnd(整页 onload 完成)。

有了这条流水线,各阶段耗时就是两个时间戳相减。下面这段代码是我现在每个项目都会丢进去的"起手式":

javascript
复制
// 现代写法:从 Performance Timeline 里取 Navigation Timing 条目// 返回的是 PerformanceNavigationTiming,字段比老 performance.timing 多得多const nav = performance.getEntriesByType('navigation')[0];// 各阶段耗时(单位:毫秒,DOMHighResTimeStamp)const metrics = { // DNS 查询:从开始查域名到查完 dns: nav.domainLookupEnd - nav.domainLookupStart, // TCP 三次握手:从开始连到连上 tcp: nav.connectEnd - nav.connectStart, // 首字节时间 TTFB:从请求发出到收到第一个响应字节 ttfb: nav.responseStart - nav.requestStart, // DOM 解析:从响应结束到可以交互 domParse: nav.domInteractive - nav.responseEnd, // 资源加载完成(不含异步资源) domReady: nav.domContentLoadedEventEnd - nav.startTime, // 整页完全加载 load: nav.loadEventEnd - nav.startTime, // 总时长 duration: nav.duration,};console.table(metrics);// 现代浏览器还能拿到这些"老 timing 没有"的字段console.log('使用的协议:', nav.nextHopProtocol); // 比如 'h2' / 'http/1.1' / 'h3'console.log('Service Worker 启动耗时:', nav.workerStart); // 大于 0 说明走了 SWconsole.log('是否重定向:', nav.redirectCount);

上面这段代码是地基。跑完之后你得到一张表,但光看数字没用,得会"读病历"。我给自己定了条规矩:TTFB 长期高于 800ms,基本是后端或 CDN 的锅(PHP、DB 慢、没上缓存);DOM 解析耗时超过 500ms,那是前端自己的锅(HTML 太大、同步脚本挡道);TCP 连接动辄一两百毫秒,多半是跨机房没上 CDN。分清楚锅在谁身上,你跟后端对线的时侯才不会被反杀。

我还踩过一个有意思的坑:有次 TTFB 看着正常,可整页就是慢。翻 nav 的字段才发现 nextHopProtocol 是 'http/1.1',secureConnectionStart 是 0——也就是说这个请求既没用 HTTP/2 多路复用,又没启用 TLS 1.3。在一堆小资源并发的场景下,HTTP/1.1 的队头阻塞能把首屏拖死。这种问题,你光看"加载时间"永远发现不了,得钻进 nextHopProtocol 这种细粒度字段里才看得到。

顺带提一句,老的 performance.timing 里那个 navigation 类型判断(是不是刷新、是不是前进后退),新接口也挪到了 nav.type 字段上(值为 'navigate' / 'reload' / 'back_forward' / 'prerender')。别再去碰已经废弃的 performance.navigation 了,那玩意儿连 MDN 自己都在页面顶上挂了"Deprecated"的灰条。

三、Resource Timing:每个资源的"作案时间线"

整页体检做完了,但"首屏慢"常常是某一张图、某一个脚本在作妖。这时候就得拉开第二个抽屉:Resource Timing。它干的事跟 Navigation Timing 一模一样,只不过对象从"整页"换成了"页面上每一个资源"——JS、CSS、图片、字体、fetch 请求,每个都有自己的那套时间线。

取数方式也对称得让人舒服:

javascript
复制
// 取页面上所有静态资源的加载明细const resources = performance.getEntriesByType('resource');// 把耗时长、可能拖慢首屏的资源挑出来const SLOW_THRESHOLD = 1000; // 1 秒以上算慢const slowOnes = resources .filter((r) => r.duration > SLOW_THRESHOLD) .map((r) => ({ name: r.name, type: r.initiatorType, // script / css / img / fetch / xmlhttprequest ... duration: Math.round(r.duration), protocol: r.nextHopProtocol, }));console.table(slowOnes);// 顺着慢资源往下查"卡在哪个阶段"const worst = resources.reduce((a, b) => (b.duration > a.duration ? b : a));console.log('最慢资源:', worst.name);console.log(' DNS 耗时:', worst.domainLookupEnd - worst.domainLookupStart);console.log(' TCP 耗时:', worst.connectEnd - worst.connectStart);console.log(' 请求到首字节:', worst.responseStart - worst.requestStart);console.log(' 下载耗时:', worst.responseEnd - worst.responseStart);console.log(' 协议:', worst.nextHopProtocol);

我真实遇到过的案子:某活动页 FCP 高达 3.2 秒,运营已经准备写事故复盘了。我拉出 Resource Timing 一查,发现首页顶部那张 banner 大图的 responseStart 比同批资源滞后了 2.1 秒,再一看它的 nextHopProtocol 是 'http/1.1'、secureConnectionStart 是 0——破案了:HTTP/1.1 队头阻塞 + 没开 TLS 1.3,这张图把后面一串请求全堵在后面了。改成因 CDN 的 HTTP/2 域名分发之后,首屏直接掉到 1.1 秒。你看,不靠猜,靠数据。

Resource Timing 还有个被严重低估的能力:它不费吹灰之力就能告诉你每个资源"有没有命中缓存"。这事儿如果你自己写逻辑去判断,得发 HEAD 请求或者读响应头,麻烦得要死;而浏览器早就在 timing 里记好了:

javascript
复制
// Resource Timing 里藏着缓存信息,不用发额外请求就能判断performance.getEntriesByType('resource').forEach((r) => { let cache = '未命中(实打实下载)'; if (r.transferSize === 0) { // transferSize 为 0:没有任何字节在网络上传输 → 强制缓存(from disk/memory cache) cache = '强制缓存(transferSize=0)'; } else if (r.encodedBodySize === 0) { // 有传输字节,但响应体编码大小为 0 → 304 协商缓存 cache = '协商缓存(304,encodedBodySize=0)'; } console.log(r.name, '→', cache, '| 协议', r.nextHopProtocol);});

transferSize 是"实际在网络上传输的字节数(含响应头)",encodedBodySize 是"响应体压缩后的字节数"。这俩组合能区分出是 Disk/Memory Cache(transferSize=0)、还是 304 协商缓存(有传输但响应体为空)、还是真·从服务器下载。我之前一直以为我们网站的静态资源缓存配得挺好,结果用这段脚本一跑,发现一半的 JS 居然每次都在实打实下载——原来是前端构建没给带 hash 的文件配长缓存,后端还偷偷加了个 no-cache。这种"以为优化好了其实没优化"的错觉,最容易被 Resource Timing 打脸。

四、Web Vitals:和用户感知直接挂钩的指标

前两章讲的是"技术视角的耗时",但老板和用户根本不关心你 DNS 查了 20ms 还是 30ms,他们只关心:页面是不是秒开?点按钮卡不卡?滚着滚着字会不会乱跳?这一章就是回答这些"人话"指标的抽屉,也就是大家现在天天念叨的 Web Vitals。

Web Vitals 是 Google 推的一套"用户体验标尺",核心三件套是 LCP(最大内容绘制,衡量加载)、CLS(累积布局偏移,衡量视觉稳定性)、还有 INP(Interaction to Next Paint,衡量交互响应,它已经在 2024 年正式取代了老的 FID)。再加上 FCP(首次内容绘制)、TTFB,基本就覆盖了用户对"快不快"的全部体感。

我早期最大的误区,是想着用 setInterval 去轮询 performance 拿这些指标。被现实狠狠教育了:LCP 的值是会变的——页面加载过程中"最大的那个内容元素"可能换了好几个,你得等它稳定;CLS 是累积的,得把每次位移加起来;这些都不是"某一刻读一下"能拿到的,而是"持续观察直到结束"。所以标准做法是用 PerformanceObserver 异步监听,它跟 ResizeObserver 与 MutationObserver 实战:监听元素尺寸与 DOM 变化(告别轮询)里讲的是一个套路:别轮询,让浏览器主动推数据给你。

javascript
复制
// 用 PerformanceObserver 异步监听,不轮询、不漏数据const vitals = {};const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { switch (entry.entryType) { case 'paint': // FCP:首次绘制有内容的时间 if (entry.name === 'first-contentful-paint') { vitals.fcp = entry.startTime; } break; case 'largest-contentful-paint': // LCP:取"最后一次"出现的,因为最大内容元素会变 vitals.lcp = entry.startTime; break; case 'layout-shift': // CLS:把每次意外位移累加起来(跳过用户主动交互造成的位移) if (!entry.hadRecentInput) { vitals.cls = (vitals.cls || 0) + entry.value; } break; case 'first-input': // INP 的前身 FID:首次交互到浏览器开始处理的时间差 vitals.fid = entry.processingStart - entry.startTime; break; } }});// 一次性订阅多种指标observer.observe({ entryTypes: ['paint', 'largest-contentful-paint', 'layout-shift', 'first-input'],});// 记得在合适的时机停止,避免内存泄漏// observer.disconnect();

这里有个极易翻车的点:LCP 一定要取"最后一次" entry 的 startTime,因为我前面说了,最大内容元素会变,取第一次你就拿到个假的低值,跟 Lighthouse 测出来的对不上,被质疑"你这数据哪来的"就很尴尬。CLS 那个 hadRecentInput 判断也别省——用户自己点了按钮导致布局动一下,这种"主动位移"不能算进 CLS 里,否则你量出来的分数永远偏高。

官方给的健康线我贴这儿方便你对照:LCP ≤ 2.5s 算好,> 4s 就得挨批;CLS ≤ 0.1 算好,> 0.25 就是"页面在蹦迪";FID ≤ 100ms 算好,INP ≤ 200ms 算好。这些阈值的来龙去脉建议直接看 web.dev 的 Web Vitals 说明和MDN 的 PerformanceObserver 文档,比我在这转述权威得多。

说到"绘制",有个容易混的点:FCP 这种 Paint Timing 是浏览器渲染流水线的产物,而 CSS Houdini 实战:用 Paint/Animation Worklet 突破原生 CSS 限制(自定义绘制) 里讲的自绘(Paint Worklet)是你在 JS 里接管"画什么"的过程——一个是"测出什么时候画完了",一个是"决定画成什么样",两个不在一层,别搞串了。

五、performance.now() 与 User Timing:给自己的代码打点

前面都是浏览器替我们记的,但有时候我们想量的是"我这一段业务逻辑到底跑了几毫秒",比如"用户点下单到下单成功花了多久""这个排序算法在真实数据上要多少时间"。这时候就轮到 High Resolution Time 和 User Timing 这两兄弟出场了。

先说performance.now()。它返回的是相对页面导航开始的毫秒数,精度到亚毫秒,而且最关键的一点:它不受系统时间调整的影响。你用 Date.now() 去算代码耗时,万一用户机器上的系统时间被 NTP 校准了一下、或者被某个软件回拨了,你算出来的差值能变成负数(别问我怎么知道的,我就是开头那个写出负时间监控的憨憨)。performance.now() 基于硬件高精度时钟,恒定速率递增,测代码耗时它才是正解。

javascript
复制
// performance.now():相对页面导航开始的毫秒数,不受系统时间调整影响// 比 Date.now() 更适合测代码耗时(Date.now 会被系统校准、被阻塞干扰)function measureSort(n) { const start = performance.now(); const arr = Array.from({ length: n }, () => Math.random()); arr.sort((a, b) => a - b); const end = performance.now(); console.log(`排序 ${n} 个数耗时`, (end - start).toFixed(3), 'ms'); return arr;}measureSort(100000);// User Timing:给业务代码任意打点,名字自定义performance.mark('checkout:start');// ... 一堆下单逻辑 ...performance.mark('checkout:end');performance.measure('checkout:duration', 'checkout:start', 'checkout:end');// 打完点从 Performance Timeline 里取出来const m = performance.getEntriesByName('checkout:duration')[0];console.log('下单总耗时:', m.duration, 'ms');performance.clearMarks(); // 用完清理,别让条目无限堆积performance.clearMeasures();

performance.mark 是在时间线上插一面叫得出名字的小旗子,performance.measure 是量两面旗子之间的距离,名字你随便起。这套机制特别适合给核心链路打点,比如"加购""下单""支付"每一步各花多久,全量上报后就能画出漏斗。

我后来把打点封装成了装饰器,直接在方法上 @measure 一下就自动记耗时,省得每个函数里手动 mark/measure——如果你也对这种"无侵入埋点"感兴趣,可以看 JavaScript Decorators 装饰器实战:用 @ 重构类与方法(TC39 Stage 3),思路是一样的:用更优雅的语法把重复的劳动消掉。不过提醒一句,装饰器现在还是 TC39 Stage 3,生产环境记得配好编译,别直接裸跑。

还有个内存注意事项:mark 和 measure 产生的条目会一直躺在 Performance Timeline 里,如果你在高频函数里疯狂打点又不清理,Timeline 会越堆越大。记得用 clearMarks / clearMeasures 及时收尸。另外,老文章里常提的 performance.memory(看 JS 堆内存、判断内存泄漏),在 Chrome 里早已是 deprecated 状态,而且只有 Chromium 系支持,别在你的监控里依赖它,真要查内存去 DevTools 的 Memory 面板。

六、把上面串起来:自己撸一个轻量性能监控 SDK

讲了这么多抽屉,最后得把它们焊成一个能用的东西。我特别反感那种"文章看完还是不会用"的教程,所以这一章给你一个我线上在跑的极简版监控器——它没有 Sentry 那种重量级 SDK 的包袱,就做一件事:页面加载完、趁浏览器空闲,把 Navigation Timing、慢资源、Web Vitals 悄悄发到你的后端。

为什么强调"趁空闲发"?因为性能监控本身是辅助功能,绝不能抢主线程、不能拖慢页面——那可就讽刺了,监控自己成了性能瓶颈。所以上报时机我放在 window.onload 之后,再用 requestIdleCallback 兜底,数据通道优先用 navigator.sendBeacon(它专为"页面卸载前也要把最后一点数据发出去"这种场景设计,不阻塞、还能在页面关掉时送达)。上报的传输本身我一般走 fetch 或 sendBeacon,如果你想把"取消上报""带进度上报大体积诊断包"玩出花,可以接着看 Fetch + AbortController + Streams 实战:现代请求的中断与流式读取(进度/取消)。

如果一次性要上报的数据量很大、或者在端上要做聚合压缩,我建议把这段逻辑挪到 Web Worker 里跑,主线程只负责采集、Worker 负责计算和发送,彻底零打扰——这块可以参考 Web Workers 实战:把计算密集型任务丢给后台线程(SharedWorker 与多线程通信)。下面先给一个最朴素、能直接抄的版本:

javascript
复制
// 一个极简性能监控上报器:页面加载完、空闲时悄悄把数据发走const pMonitor = (() => { const reportUrl = '/api/perf'; const TIMEOUT = 10000; // 资源超过 10s 算异常 // 收集 Navigation Timing 关键指标 function getNavMetrics() { const nav = performance.getEntriesByType('navigation')[0]; return { dns: nav.domainLookupEnd - nav.domainLookupStart, tcp: nav.connectEnd - nav.connectStart, ttfb: nav.responseStart - nav.requestStart, domReady: nav.domContentLoadedEventEnd - nav.startTime, load: nav.loadEventEnd - nav.startTime, protocol: nav.nextHopProtocol, }; } // 收集超时的资源 function getSlowResources() { return performance .getEntriesByType('resource') .filter((r) => r.duration > TIMEOUT) .map((r) => r.name); } function report() { const payload = { nav: getNavMetrics(), slow: getSlowResources(), t: Date.now() }; // sendBeacon 不阻塞主线程,页面关掉也能把最后数据发出去 if (navigator.sendBeacon) { navigator.sendBeacon(reportUrl, JSON.stringify(payload)); } else { fetch(reportUrl, { method: 'POST', body: JSON.stringify(payload), keepalive: true }); } } // 等页面真正加载完,再在空闲时上报,绝不抢主线程 function init() { const oldOnload = window.onload; window.onload = (e) => { oldOnload && oldOnload(e); if (window.requestIdleCallback) { requestIdleCallback(report); } else { setTimeout(report, 0); } }; } return { init, report };})();pMonitor.init();

这段不到 40 行,已经能覆盖"整页耗时 + 慢资源告警"两大核心诉求。把它做成独立文件、在入口底部异步 import 进来,主线程的解析时间一点不受影响。至于 FCP/LCP/CLS 那些,你照着第四章的 observer 代码,在 report 里把 vitals 一起塞进 payload 就行了。

我得泼盆冷水:监控做到"上报"只是上半场,"告警"才是下半场。我见过太多团队,前端吭哧吭哧把数据传上去了,后端却只存不分析,超时资源列表堆在那儿没人看,直到用户又来投诉才发现某个 CDN 节点挂了三天。光有"报"没有"警",你的监控就是个高级点的摆设。真正的闭环是:上报 → 按版本/地区/网络类型聚合 → 超过阈值(比如某资源超时次数 5 分钟内破百)就邮件/短信炸相关负责人。这部分得服务端配合,前端单方面努力没用——这点我踩过坑,必须提醒你。

还有两个上线前极易忽略的细节,都是我交过学费的。一是采样率,别傻乎乎地把每个用户的每次交互、每个资源都全量上报,量一大你自己的后端先被监控打爆,一般按 10% 到 30% 随机抽样就足够定位问题了;二是隐私合规,上报 payload 里千万别带 URL 完整路径里的敏感参数、别带用户输入内容、别带能定位到个人的设备指纹,性能数据够做聚合分析用就行,这根弦必须绷着,不然哪天被安全团队找上门,你连监控代码都保不住。

七、避坑清单:我把能踩的坑都替你踩了

最后给自己、也给你留一份速查清单,都是血泪换来的:

第一,拿数据一定要等到 window.onload 之后,或者干脆用 PerformanceObserver 异步监听。我开头那个负时间、0 值、NaN 的惨案,根因就是数据读早了。Navigation Timing 的 loadEventEnd、Resource Timing 的异步资源,都得等页面真正稳定才有准数,绑在 onload 里最稳。

第二,千万别再用 performance.timing 和 performance.navigation 了,这俩在 MDN 上都是明晃晃的 Deprecated。新代码统一 performance.getEntriesByType('navigation' / 'resource' / 'paint'),类型判断用 nav.type 字段。老项目里看到了,能重构就重构,实在动不了至少心里有数它哪天会被浏览器砍掉。

第三,performance.now() 测代码耗时,Date.now() 做时间戳展示,各司其职。别再用 Date.now() 去量耗时,系统时间一调你就翻车。

第四,LCP 取最后一次 entry,CLS 跳过 hadRecentInput,这两条漏了,你上报的指标就是假的,拿去跟 Lighthouse 对线必输。

第五,mark / measure / observer 用完要 clear 和 disconnect,不然 Performance Timeline 这个"黑匣子"会被你自己的打点撑爆,反而拖累性能——监控把自己搞成瓶颈,是我最不愿看到的黑色幽默。

第六,Resource Timing 的 transferSize / encodedBodySize 是白嫖来的缓存探针,不花一分钱流量就能判断命中了哪种缓存,做静态资源优化时比你自己猜靠谱一百倍。

把这几条记牢,再配合上面六个代码块,你已经具备在一个真实项目里把前端性能监控从零搭起来的能力了。它不神秘,浏览器早就替你把数据准备好了,你要做的只是"懂规矩地去拿、讲道理地去读、低打扰地上报"。

标签:javascript, 前端, 开发语言描述:用第一人称踩坑叙事讲透浏览器 Performance API 如何做前端性能监控:从 Navigation Timing 给整页做加载体检(DNS/TCP/TTFB/DOM 各阶段耗时)、Resource Timing 揪出拖慢首屏的慢资源并用 transferSize/encodedBodySize 白嫖缓存命中判断,到用 PerformanceObserver 异步拿到 FCP/LCP/CLS 等 Web Vitals(告别轮询),再到 performance.now() 与 User Timing 给业务代码打点,最后串成一个基于 sendBeacon + requestIdleCallback 的轻量监控 SDK,并附 6 条工程避坑清单。全是作者线上跑通过的真实代码与踩坑。

2my557gbkr2my557gbkr
评论

评论列表共有0条评论

发表评论发布评论后审核成功,即可显示在列表中...

文章2
粉丝0
获赞0
私信