前端性能指标详解:FP, FMP, LCP, 与 CLS
今天,我们来深入探讨前端性能指标,重点关注 FP (First Paint - 首次绘制)、FMP (First Meaningful Paint - 首次有意义绘制)、LCP (Largest Contentful Paint - 最大内容绘制) 和 CLS (Cumulative Layout Shift - 累积布局偏移)。这些指标对于衡量网页加载体验至关重要,它们直接决定了用户是觉得你的网站“快”还是“稳定”。
我们将逐步解析如何测量、分析和应用这些指标。我们的目标是提供实用、无冗余的技术细节!
为什么要在乎性能指标?
前端性能直接影响用户体验。加载缓慢或内容跳跃的页面会赶走用户。谷歌的 核心网页指标 (Core Web Vitals) 将 LCP、CLS 和 FID (First Input Delay - 首次输入延迟,本文不深入探讨) 作为关键指标,它们甚至会影响搜索引擎排名。
虽然 FP 和 FMP 已不是官方核心指标,但它们在分析渲染过程中仍然至关重要。让我们从 FP 开始,逐一揭开这些指标的神秘面纱。
FP (First Paint - 首次绘制): 页面开始呈现内容的时刻
FP (First Paint) 标志着浏览器在屏幕上绘制第一个像素的时刻。这个像素可以是背景颜色、边框或任何视觉元素——不一定是用户关心的“主要内容”。FP 是一个基础指标,它表明页面从白屏到开始显示内容花费了多长时间。
如何测量 FP?
FP 是第一个渲染里程碑,通常通过 Performance api 来捕获。它发生在浏览器解析完 dom 和 cssOM 并开始绘制像素时。以下是测量方法:
// 监听页面加载完成事件
window.addEventListener("load", () => {
// 使用 Performance API 获取所有“paint”类型的性能条目
const performanceEntries = window.performance.getEntriesByType("paint");
// 找到名为 'first-paint' 的条目
const fpEntry = performanceEntries.find((entry) => entry.name === "first-paint");
if (fpEntry) {
// fpEntry.startTime 是从页面导航开始到首次绘制发生的时间(毫秒)
console.log(`首次绘制 (FP): ${fpEntry.startTime.toFixed(2)}ms`);
} else {
console.log("浏览器不支持 FP 或无法获取");
}
});
这段代码使用 performance.getEntriesByType('paint') 来获取与绘制相关的性能条目,找到 first-paint 条目并记录其 startTime (以毫秒为单位)。大多数现代浏览器(Chrome、Edge、Firefox)都支持此 API。
FP 代表什么?
FP 标志着渲染的开始,表明浏览器已经开始工作。例如,背景颜色的变化或加载指示器的出现都会触发 FP。它通常发生在:
浏览器解析 html <head> 并加载了 CSS。 浏览器开始渲染第一个 DOM 元素(例如 <body> 的背景)。
真实世界的例子
思考一个简单的页面:
<!DOCTYPE html>
<html>
<head>
<title>我的页面</title>
<link
rel="stylesheet"
href="styles.css"
/>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
/* styles.css */
body {
background-color: #f0f0f0; /* 设置背景色 */
}
h1 {
color: navy;
}
当浏览器加载 styles.css 并应用 body 的背景色时,屏幕从白色变为 #f0f0f0,这便触发了 FP。此时 <h1> 文本可能尚未出现,因为 DOM 和 CSSOM 可能仍在构建中。
FP 的局限性
FP 只追踪第一个像素的绘制,而不管内容是否有意义。例如,即使用户想看的文章或图片还未加载,一个背景色的变化也会触发 FP。这正是 FMP 和 LCP 发挥作用的地方。
FMP (First Meaningful Paint - 首次有意义绘制): 页面变得有意义的时刻
注意: FMP 是一个已被 Lighthouse 废弃的指标,因为它定义模糊且难以标准化。现在官方推荐使用 LCP。但了解 FMP 有助于理解性能演进历史。
FMP (First Meaningful Paint) 捕获页面主要内容(如文章标题、主图或导航栏)首次渲染的时刻。它不是一个标准化的浏览器指标,定义可能很模糊,但像 Lighthouse 这样的工具曾用它来分析加载体验。
如何测量 FMP?
FMP 无法通过浏览器 API 直接获得,因为“有意义”的内容因页面而异。Lighthouse 通过分析渲染时间线,寻找“视觉上显著”的元素(如大块文本或图片)出现的时间点来估算 FMP。
以下是如何在 Node.js 中使用 Lighthouse 测量 FMP:
// 引入 lighthouse 和 chrome-launcher 库
const lighthouse = require("lighthouse");
const chromeLauncher = require("chrome-launcher");
asyncfunction runLighthouse(url) {
// 启动一个无头 Chrome 实例
const chrome = await chromeLauncher.launch({ chromeFlags: ["--headless"] });
const options = {
output: "json", // 输出格式为 JSON
onlyCategories: ["performance"], // 只运行性能审计
};
// 运行 Lighthouse 分析
const runnerResult = await lighthouse(url, options, chrome);
// 从报告中获取 FMP 的估算值(毫秒)
console.log("首次有意义绘制 (FMP):", runnerResult.lhr.audits["first-meaningful-paint"].numericValue, "ms");
// 关闭 Chrome 实例
await chrome.kill();
}
// 运行分析
runLighthouse("http://localhost:3000");
首先,需要安装依赖:
npm install lighthouse chrome-launcher
这段代码运行 Lighthouse,分析页面,并返回估算的 FMP 时间。Lighthouse 通过观察 DOM 变化和渲染事件来推断 FMP。
FMP 的挑战
FMP 的定义因页面而异,Lighthouse 的估算也未必总是准确。例如:
**单页应用 (SPA)**:使用 react/vue 构建的应用通过 JavaScript 渲染内容,这会将 FMP 推迟到 JS 执行之后。 图片密集型页面:FMP 可能与图片加载绑定,而 Lighthouse 可能会错误判断。
这促使我们转向 LCP,这是 Google 提出的官方指标,旨在解决 FMP 的模糊性问题。
LCP (Largest Contentful Paint - 最大内容绘制): 最大内容块渲染的时刻
LCP (Largest Contentful Paint) 是核心网页指标之一,用于测量页面上最大的内容元素(如图片、视频或文本块)完全渲染出来所需的时间。它比 FMP 更精确,因为它只关注视口内最大的可见元素。
如何测量 LCP?
LCP 可以通过 Performance API 的 LargestContentfulPaint 条目捕获。浏览器会持续追踪视口中最大的内容元素,并记录其渲染时间。
// 创建一个 PerformanceObserver 实例来监听性能事件
const observer = new PerformanceObserver((entryList) => {
// getEntries() 返回一个性能条目数组
const entries = entryList.getEntries();
for (const entry of entries) {
// entry.startTime 是 LCP 时间
console.log(`最大内容绘制 (LCP): ${entry.startTime.toFixed(2)}ms`);
// entry.element 是触发 LCP 的 DOM 元素
console.log("LCP 元素:", entry.element);
}
});
// 开始监听 'largest-contentful-paint' 类型的事件
// buffered: true 确保在观察者创建之前发生的事件也能被捕获
observer.observe({ type: "largest-contentful-paint", buffered: true });
你也可以在 Chrome DevTools 的 Performance 面板中查看 LCP。打开 DevTools (F12),转到 "Performance" 标签页,录制页面加载过程,LCP 会在时间线上被明确标记出来。
LCP 的定义
LCP 追踪的元素包括:
<img> 元素 (包括 <picture> 内的 <img>)。 <video> 元素的封面图片(poster image)。 具有 background-image 的元素 (通过 url() 加载)。 文本块(如 <p>, <h1>, <h2>)。 包含文本或图片的容器,如 <p>。
浏览器会:
追踪视口中最大的内容元素。 记录它完全渲染的时间(例如,图片加载完成或文本绘制完成)。 如果稍后出现一个更大的元素(例如,滚动后加载了一张新图片),LCP 会被更新。
代码示例
这是一个突出 LCP 的页面:
<!DOCTYPE html>
<html>
<head>
<title>LCP 演示</title>
<style>
.hero {
width: 100%;
height: 400px;
/* 一个大的背景图通常是 LCP 元素 */
background: url("hero.jpg") no-repeat center/cover;
}
h1 {
font-size: 48px;
}
</style>
</head>
<body>
<p class="hero"></p>
<h1>欢迎来到我的网站</h1>
<p>一些内容...</p>
</body>
</html>
假设 hero.jpg 是一张大图(例如 1920x1080),渲染顺序可能是:
FP: 页面背景色(默认为白色)绘制。 FMP (估算): <h1> 标题渲染(可能是主要内容)。 LCP: .hero 的背景图完全加载并绘制(成为视口中最大的元素)。
LCP 优化注意事项
图片加载:如果 LCP 元素是图片,使用现代格式(如 WebP, AVIF)并压缩文件大小。 字体加载:对于基于文本的 LCP,缓慢的字体文件(WOFF2)会延迟渲染。 动态内容:在 SPA 中,LCP 可能在 React/Vue 渲染主组件后才发生。
CLS (Cumulative Layout Shift - 累积布局偏移): 页面有多稳定?
CLS (Cumulative Layout Shift) 衡量页面内容在加载过程中“跳动”或“偏移”的程度。用户最讨厌的就是,正要点击一个按钮,页面突然移动,导致误点。CLS 是核心网页指标之一,旨在确保页面稳定性。
如何测量 CLS?
CLS 是通过 LayoutShift 性能条目计算的。浏览器会追踪所有非用户交互触发的布局变化,并计算一个“偏移分数”。
let clsScore = 0;
// 创建 PerformanceObserver 来监听布局偏移事件
const observer = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
// 忽略由用户输入(如点击、滚动)在 500ms 内触发的布局偏移
if (!entry.hadRecentInput) {
// entry.value 是单次布局偏移的分数
clsScore += entry.value;
console.log(`单次布局偏移分数: ${entry.value.toFixed(4)}`);
}
}
// 累加所有分数得到最终的 CLS
console.log(`累积布局偏移 (CLS): ${clsScore.toFixed(4)}`);
});
// 开始监听 'layout-shift' 类型的事件
observer.observe({ type: "layout-shift", buffered: true });
根据 Google 的标准,CLS 分数低于 0.1 是理想的。你可以使用 Chrome DevTools 的 Performance 面板来可视化 CLS,发生偏移的元素会被高亮显示。
CLS 的计算方式
CLS 是所有单个布局偏移分数的总和,每个分数按以下方式计算:
偏移分数 = 影响因子 (Impact Fraction) × 距离因子 (Distance Fraction)
影响因子:受偏移影响的视口区域比例。例如,一个元素从顶部移动到底部,可能会影响 50% 的视口。 距离因子:元素移动的距离相对于视口高度(或宽度)的比例。例如,移动了视口高度的 10%,则距离因子为 0.1。
代码示例
这是一个会导致 CLS 的页面:
<!DOCTYPE html>
<html>
<head>
<title>CLS 演示</title>
<style>
.banner {
width: 100%;
height: 100px;
background: lightblue;
}
.content {
margin-top: 10px;
}
</style>
</head>
<body>
<p class="content">
<h1>主要内容</h1>
<p>这部分内容可能会移动...</p>
</p>
<script>
// 模拟异步加载广告
setTimeout(() => {
const banner = document.createElement("p");
banner.className = "banner";
// 将广告横幅插入到 body 的最前面,导致下面的内容移动
document.body.insertBefore(banner, document.body.firstChild);
}, 2000);
</script>
</body>
</html>
这个页面:
首先渲染 <h1> 和 <p>。 2 秒后,JavaScript 插入一个 100px 高的 .banner,导致 .content 向下移动。
修复 CLS 的常见方法
为图片设置尺寸:给 <img> 标签添加 width 和 height 属性,以便浏览器提前预留空间。
<img
src="image.jpg"
width="800"
height="600"
alt="示例图片"
/>使用 CSS 占位符:为即将加载的动态内容预留空间。
.banner-placeholder {
width: 100%;
min-height: 100px; /* 预留空间 */
}避免在现有内容上方插入新内容。
综合分析:一个真实场景
让我们分析一个包含 FP、FMP、LCP 和 CLS 的真实页面:
<!DOCTYPE html>
<html>
<head>
<title>新闻网站</title>
<link
rel="stylesheet"
href="styles.css"
/>
<!-- 加载外部字体 -->
<link
href="https://fonts.googleapis.com/css2?family=Roboto"
rel="stylesheet"
/>
</head>
<body>
<header>
<h1>新闻网站</h1>
</header>
<main>
<!-- LCP 候选元素 -->
<img
src="hero.jpg"
alt="主图"
/>
<h2>头条新闻</h2>
<p>突发新闻内容...</p>
</main>
<!-- 异步加载的 JS 可能导致 CLS -->
<script
src="ads.js"
async
></script>
</body>
</html>
/* styles.css */
body {
background-color: #f0f0f0;
}
header {
background: navy;
color: white;
padding: 10px;
}
/* 图片未设置高度,可能导致 CLS */
img {
width: 100%;
height: auto;
}
main {
margin: 20px;
}
h2,
p {
font-family: "Roboto", sans-serif;
}
// ads.js
// 模拟一个广告脚本,在 3 秒后插入一个横幅
setTimeout(() => {
const ad = document.createElement("p");
ad.style.height = "100px";
ad.style.background = "lightgreen";
document.body.insertBefore(ad, document.body.firstChild);
}, 3000);
性能分析与优化:
FP: 很快,因为只是 body 的背景色(约 200ms)。 FMP (估算): <h2> 和 <p> 的渲染(约 500ms),受字体加载影响。 优化: 使用 font-display: swap; 让系统字体先显示。 LCP: <img> 完全加载(约 1000ms+),取决于图片大小和网络。 优化: 压缩图片,使用 WebP 格式,并为视口外的图片添加 loading="lazy"。 CLS: 优化: 为广告位设置一个 min-height: 100px 的占位容器。 优化: 为 <img> 设置 width 和 height 属性,或使用 CSS 的 aspect-ratio 属性。 原因1: 图片加载完成前没有尺寸,导致 height: auto 生效后推开下方内容。 原因2: 广告插入导致 <main> 下移。
单页应用 (SPA) 场景:React 应用性能
使用 React/Vue 等框架的 SPA 由于其动态渲染机制,使性能指标分析变得复杂。
// src/App.js
import React, { useEffect, useState } from"react";
import"./App.css";
function App() {
const [adVisible, setAdVisible] = useState(false);
useEffect(() => {
// 模拟 3 秒后显示广告
const timer = setTimeout(() => {
setAdVisible(true);
}, 3000);
return() => clearTimeout(timer);
}, []);
return (
<p>
{/* 广告位,会导致布局偏移 */}
{adVisible && <p className="ad-banner">广告</p>}
<header>
<h1>React 新闻</h1>
</header>
<main>
<img
src="hero.jpg"
alt="主图"
style={{ width: "100%" }}
/>
<h2>头条新闻</h2>
<p>突发新闻内容...</p>
</main>
</p>
);
}
exportdefault App;
SPA 性能挑战与改进
JS 阻塞:大型 JS 包会因为解析和执行而延迟 FMP 和 LCP。
改进: 使用代码分割 (React.lazy 和 Suspense)。 动态渲染: 通过 useEffect 或异步请求加载的内容会延迟 LCP。
CLS 风险: 动态组件(如广告、评论)会增加布局偏移风险。
// 使用占位符修复 CLS
.ad-placeholder {
min-height: 100px;
}
{/* 在广告加载前显示占位符 */}
{adVisible ? <p className="ad-banner">广告</p> : <p className="ad-placeholder"></p>}改进: 为动态内容预留空间。
工具支持
Chrome DevTools: Performance 面板可以直观地显示 FP、LCP 和 CLS。
Lighthouse: 运行全面的性能审计,提供 FMP、LCP 等指标的得分和建议。
web-vitals 库: Google 官方的轻量级库,用于在真实用户环境中监控 LCP 和 CLS。
npm install web-vitalsimport { getLCP, getCLS } from "web-vitals";
// 监控 LCP
getLCP((metric) => console.log("LCP:", metric.value.toFixed(2), "ms"));
// 监控 CLS
getCLS((metric) => console.log("CLS:", metric.value.toFixed(4)));
结论
FP、FMP、LCP 和 CLS 是衡量前端性能的关键指标:
FP (首次绘制): 标志着渲染的开始。 FMP (首次有意义绘制): 估算有用内容出现的时间点(已废弃)。 LCP (最大内容绘制): 精确测量主要元素加载速度,代表加载性能。 CLS (累积布局偏移): 衡量页面稳定性,代表视觉稳定性。
通过使用 Performance API、Lighthouse 和 web-vitals 库,您可以准确地测量这些指标。本文中的代码示例演示了如何进行监控和分析,并通过真实世界的案例(静态页面、SPA、复杂页面)阐明了每个指标的触发条件和影响。
现在,就动手试试这些脚本,打开 DevTools,享受性能分析的乐趣吧!
来自公众号:OTT前端技术
本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!