如何准确监控前端页面的首屏加载时间?

司空桂霞 阅读 28

我在用 Performance API 监控首屏时间,但发现不同设备差异很大,有时候取不到正确的 FCP 值,是不是我用的方法有问题?

目前我是这样获取的:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.name === 'first-contentful-paint') {
      console.log('FCP:', entry.startTime);
    }
  }
});
observer.observe({ entryTypes: ['paint'] });

但在一些低端安卓机上根本没触发回调,是不是还得结合其他指标一起算?

我来解答 赞 6 收藏
二维码
手机扫码查看
2 条解答
UX-景叶
UX-景叶 Lv1
你的代码本身没问题,FCP 在低端安卓机不触发主要有两个原因:一是某些浏览器根本不触发 FCP(页面内容没变化时),二是 PerformanceObserver 本身在老版本浏览器不兼容。

我的做法是加一个回退方案,同时用 LCP 作为补充指标,因为 LCP 比 FCP 更稳定:

// 先尝试用 PerformanceObserver 监听 FCP
let fcpTime = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
fcpTime = entry.startTime;
console.log('FCP:', fcpTime); }
}
});

// 如果浏览器不支持 PerformanceObserver,用 timing 回退
if (PerformanceObserver.supportedEntryTypes && PerformanceObserver.supportedEntryTypes.includes('paint')) {
observer.observe({ entryTypes: ['paint'] });
} else {
// 兼容老浏览器,用 performance.timing
const timing = performance.timing;
fcpTime = timing.navigationStart + timing.domContentLoadedEventEnd - timing.navigationStart;
console.log('FCP (fallback):', fcpTime);
}

// 再加一个 LCP 监听,更可靠
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.startTime);
});
if (PerformanceObserver.supportedEntryTypes && PerformanceObserver.supportedEntryTypes.includes('largest-contentful-paint')) {
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
}

实际项目中我的经验是:移动端用 LCP 当主要指标更靠谱,FCP 经常因为页面结构简单或者懒加载导致取不到值。另外记得在页面加载完成后主动断开 observer,避免内存泄漏:

// 页面加载完成后断开
window.addEventListener('load', () => {
setTimeout(() => {
observer.disconnect();
lcpObserver.disconnect();
}, 2000);
});
点赞
2026-03-20 08:22
UE丶长永
你这个问题挺常见的,我来给你捋一捋。

首先,你的代码写法本身没问题,但 PerformanceObserver 有一个坑:它只能监听到注册之后发生的性能事件。也就是说如果你在页面加载后才去创建 observer,那些已经发生的 paint 事件就错过了。

这就是为什么低端机容易出问题——那些机器渲染快,可能 FCP 在你的 observer 注册之前就已经发生了。

正确的做法是用 PerformanceEntry 配合页面加载时机,我给你一个更完善的方案:

// 方法1:使用 PerformanceObserver 监听(推荐)
function getFCP() {
return new Promise((resolve) => {
// 如果已经获取过就直接返回
const paintEntries = performance.getEntriesByType('paint');
const fcpEntry = paintEntries.find(entry => entry.name === 'first-contentful-paint');
if (fcpEntry) {
resolve(fcpEntry.startTime);
return;
}

// 如果没有,监听未来的 paint 事件
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
observer.disconnect(); // 拿到就断开监听
resolve(entry.startTime);
}
}
});

observer.observe({ entryTypes: ['paint'] });
});
}

// 方法2:兼容方案,取不到 FCP 时用 LCP 作为兜底
async function getFirstScreenTime() {
// 优先用 FCP
let fcp = 0;
const paintEntries = performance.getEntriesByType('paint');
const fcpEntry = paintEntries.find(e => e.name === 'first-contentful-paint');
if (fcpEntry) {
fcp = fcpEntry.startTime;
}

// 如果没有 FCP,用 LCP 兜底
if (!fcp) {
const lcpEntry = await new Promise(resolve => {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
observer.disconnect();
resolve(lastEntry);
});
observer.observe({ entryTypes: ['largest-contentful-paint'], buffered: true });
});
fcp = lcpEntry.startTime;
}

// 还可以加上 DOMReady 作为最终兜底
if (!fcp) {
const navigationEntry = performance.getEntriesByType('navigation')[0];
fcp = navigationEntry ? navigationEntry.domContentLoadedEventStart : 0;
}

console.log('首屏时间:', fcp);
return fcp;
}

// 在页面加载早期调用
if (document.readyState === 'complete') {
getFirstScreenTime();
} else {
window.addEventListener('load', getFirstScreenTime);
}


再说一下你代码里的另一个问题:你用的是 entry.name === 'first-contentful-paint',这个在大部分浏览器是 OK 的,但有些浏览器可能 entry.name 返回的是完整 URL 或者其他格式。更稳妥的方式是直接判断 entry 的类型,因为 PerformanceObserver 已经限定了 entryTypes 是 ['paint'],所以直接拿第一个 FCP 就行:

const observer = new PerformanceObserver((list) => {
// paint 类型只会包含 'first-contentful-paint' 和 'first-paint'
const firstContentfulPaint = list.getEntries().find(
entry => entry.name === 'first-contentful-paint'
);
if (firstContentfulPaint) {
console.log('FCP:', firstContentfulPaint.startTime);
observer.disconnect();
}
});
observer.observe({ entryTypes: ['paint'], buffered: true }); // 加 buffered: true


这里关键点是 buffered: true 这个参数,加上它可以获取到 observer 注册之前已经发生的性能事件,相当于一个缓冲读取。

总结一下:
1. 加 buffered: true 解决低端机漏掉的问题
2. 用 performance.getEntriesByType('paint') 做兜底,双重保障
3. 取不到 FCP 时用 LCP(最大内容绘制)作为兜底,这个在低端机上也更稳定

这样一套组合拳下来,基本能覆盖大多数场景了。你先试试看还有什么问题再问。
点赞
2026-03-18 14:02