Vue项目中如何准确获取首屏加载时间并生成性能报告?

端木照涵 阅读 15

我在用 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>
我来解答 赞 3 收藏
二维码
手机扫码查看
2 条解答
司空英瑞
你这写法有个坑: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

然后:

import { onMounted } from 'vue'
import { getFCP, getLCP } from 'web-vitals'

onMounted(() => {
getFCP(console.log)
getLCP(console.log)
})


注意 web-vitals 的 FCP/LCP 是单次测量,它会等待浏览器稳定后才触发回调,比你自己用 getEntriesByName 更准,尤其是对图片多的页面。
Lighthouse 的报告是多次采样 + 加权,你本地单次埋点肯定没法完全对齐,但用 web-vitals 能逼近真实用户感知。
点赞 4
2026-02-25 21:17
程序员逸翔
你这个思路方向是对的,但有几个关键点没踩准,我来拆开说说为啥你现在的写法和 Lighthouse 对不上,以及怎么改才靠谱。

首先得明确:首屏加载时间不是单靠一个生命周期钩子就能搞定的事,因为 Vue 的 onMounted 只能保证 DOM 挂载完成,但图片、字体这些资源可能还在下载中,而 Lighthouse 的 FCP(First Contentful Paint)是浏览器渲染层面的指标,它只关心“第一个文本/图片/SVG 等内容渲染出来的时间”,不关心 JS 是否执行完、也不管图片有没有加载完(除非是 background-image,但那又是另一回事了)。

你现在的写法:
const fcp = performance.getEntriesByName('first-contentful-paint')[0]

这个 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,直接复制就能用:

<script setup>
import { onMounted, onUnmounted, ref } from 'vue'

// 用 ref 存结果,方便后续导出或上报
const performanceData = ref({
fcp: null,
lcp: null,
cls: null
})

// 注册 PerformanceObserver 监听 paint 和 largest-contentful-paint
let paintObserver = null
let lcpObserver = null
let clsObserver = null

onMounted(() => {
// 监听 FCP / FP
try {
paintObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.name 可能是 'first-paint' 或 'first-contentful-paint'
if (entry.name === 'first-contentful-paint') {
performanceData.value.fcp = entry.startTime
console.log('FCP:', entry.startTime, 'ms')
}
}
})
paintObserver.observe({ entryTypes: ['paint'] })
} catch (e) {
console.warn('PerformanceObserver not supported for paint')
}

// 监听 LCP(Largest Contentful Paint)
try {
lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
performanceData.value.lcp = lastEntry.startTime
console.log('LCP:', lastEntry.startTime, 'ms')
})
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] })
} catch (e) {
console.warn('PerformanceObserver not supported for LCP')
}

// 监听 CLS(Cumulative Layout Shift)
try {
clsObserver = new PerformanceObserver((list) => {
let clsValue = 0
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value
}
}
performanceData.value.cls = clsValue
console.log('CLS:', clsValue)
})
clsObserver.observe({ entryTypes: ['layout-shift'] })
} catch (e) {
console.warn('PerformanceObserver not supported for CLS')
}
})

onUnmounted(() => {
// 记得清理 observer,避免内存泄漏
paintObserver?.disconnect()
lcpObserver?.disconnect()
clsObserver?.disconnect()
})

// 如果你想在控制台直接输出结果,比如调试时
console.log('performanceData:', performanceData.value)
</script>


上面这段代码有几个关键点:

- 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 函数,在 beforeUnmountonBeforeRouteLeave 里调用,或者配合 navigator.sendBeacon 做异步上报(避免页面关闭时请求丢失)——不过这个就看你项目需求了。
点赞 3
2026-02-25 18:05