Vue项目中如何准确获取首屏加载时间并生成性能报告?
我在用 Vue 3 做一个后台管理系统,想监控首页的首屏加载性能,但不确定该在哪个生命周期钩子里记录时间点。试过在 onMounted 里打点,但发现这时候图片还没加载完,和 Lighthouse 报告对不上。
有没有推荐的做法?比如结合 Performance API 或者 Web Vitals?下面是我目前的写法:
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
const fcp = performance.getEntriesByName('first-contentful-paint')[0]
console.log('FCP:', fcp ? fcp.startTime : 'not available')
})
</script>
<template>
<div>Dashboard Content</div>
</template>
onMounted里拿first-contentful-paint是不靠谱的,因为 FCP 是浏览器自动上报的,它可能在onMounted触发前早就完成了,尤其首屏有图片或异步组件时。效率更高的做法是用
performance.getEntriesByType('paint')直接取所有 paint 类型的 entry,FCP 和 LCP 都能拿到,而且能避开 timing 差异问题。另外,Lighthouse 的 FCP 和你手动埋点的逻辑可能不一致——它用的是浏览器的 Performance Paint Timing API,但如果你首屏有图片,它会等首张图片渲染完才算 FCP,而
onMounted里 DOM 已经挂载但图片不一定加载完。下面这个是真实项目里用的方案,兼容性好、开销低:
如果要生成报告,建议用
web-vitals这个库,它已经帮你处理了各种 edge case,比如跨帧、iframe、重复触发等,直接:npm install web-vitals然后:
注意
web-vitals的 FCP/LCP 是单次测量,它会等待浏览器稳定后才触发回调,比你自己用getEntriesByName更准,尤其是对图片多的页面。Lighthouse 的报告是多次采样 + 加权,你本地单次埋点肯定没法完全对齐,但用
web-vitals能逼近真实用户感知。首先得明确:首屏加载时间不是单靠一个生命周期钩子就能搞定的事,因为 Vue 的
onMounted只能保证 DOM 挂载完成,但图片、字体这些资源可能还在下载中,而 Lighthouse 的 FCP(First Contentful Paint)是浏览器渲染层面的指标,它只关心“第一个文本/图片/SVG 等内容渲染出来的时间”,不关心 JS 是否执行完、也不管图片有没有加载完(除非是 background-image,但那又是另一回事了)。你现在的写法:
这个 API 确实能拿到 FCP 时间,但有两个坑:
1.
performance.getEntriesByName('first-contentful-paint')在 Chrome 里实际返回的是PerformancePaintTiming类型的 entry,但这个 entry 的 name 字段是'first-contentful-paint',不是'first-paint',而且它可能为undefined(比如页面还没完成渲染、或者某些浏览器不支持)2. 你只在
onMounted里取了一次,但 FCP 有可能在onMounted之前早就发生了,尤其是首屏内容比较简单的后台系统,JS 加载完一执行,DOM 就渲染完了,FCP 早就过去了,你再取的时候可能拿不到 entry(因为浏览器的 performance 缓存是有限的,或者你取晚了)所以正确的做法是:在页面初始化阶段就注册监听 FCP 的 timing 事件,而不是事后去查。
具体来说,推荐用
PerformanceObserver来监听 paint 类型的 entry,它能在浏览器真正发生 paint 的时候实时触发回调,比事后查getEntriesByName准确多了。另外,FCP 还有个细节:它不包含图片的渲染时间(除非图片是 inline 的 SVG 或 background-image),所以如果你首屏有大量
![]()
标签,Lighthouse 的 FCP 和你肉眼看到的“首屏内容出现时间”可能差得挺远——这时候你可能更关心 LCP(Largest Contentful Paint),它才是更贴近用户感知的指标。下面给你一个完整可用的方案,既包括 FCP,也顺便加了 LCP 和 CLS,直接复制就能用:
上面这段代码有几个关键点:
-
PerformanceObserver是实时监听,不会错过 FCP/LCP 触发的时机,哪怕它们发生在onMounted之前(比如页面 SSR 渲染后立刻触发了 FCP)- 只取最后一次 LCP,因为一个页面可能有多个 LCP(比如图片懒加载、异步组件加载后替换内容)
- CLS 的计算要排除 hadRecentInput 的情况,避免用户操作引起的布局偏移被算进去(Lighthouse 也是这么算的)
如果你只想做一个最简版的“首屏加载时间”(比如你项目里首屏就是几个文字+一个 logo,没有大图),那 FCP 就够用了;但如果你首屏有张大图,建议直接看 LCP,因为那才是用户真正“看到内容”的时间。
另外提醒一句:Lighthouse 的报告里还有 TTI(Time to Interactive),那个更复杂,涉及主线程空闲时间,一般前端监控不会精确测这个,除非你有自定义的“首屏交互完成”事件(比如点击某个按钮能操作了),那可以自己打个
performance.mark('interactive'),再用performance.measure('tti', 'navigationStart', 'interactive')来算。最后再吐槽一句:你如果在
localhost:3000开发模式下测,性能数据基本不能看——因为 dev 模式下 Vue 的响应式系统、热更新、sourcemap 都会拖慢速度,一定要在 production 构建后(npm run build+vite preview)再测,不然你写的监控逻辑再准,数据也是假的。如果你需要把数据上报到后端,可以封装成一个
sendPerformanceReport函数,在beforeUnmount或onBeforeRouteLeave里调用,或者配合navigator.sendBeacon做异步上报(避免页面关闭时请求丢失)——不过这个就看你项目需求了。