我曾遇到这样的困惑:花了很多时间优化代码,减少了回流次数,设置了合理的缓存策略,但如何证明页面"变快了"?用户说"感觉还是有点慢",但我不知道具体慢在哪里。这让我开始思考:性能到底应该如何度量?哪些指标真正影响用户体验?
这篇文章是我对 Web 性能指标的学习总结,重点关注 Google 推出的 Core Web Vitals,以及如何将这些指标应用到实际优化中。
问题的起源
为什么需要性能指标?最直接的原因是:性能影响业务。
根据 Google 的研究:
- 页面加载时间从 1 秒增加到 3 秒,跳出率增加 32%
- 页面加载时间从 1 秒增加到 5 秒,跳出率增加 90%
- 移动端加载时间超过 3 秒,53% 的用户会放弃访问
但"快"是一个很主观的概念。同样的页面,不同用户的感受可能完全不同。性能指标的作用,就是把主观的"快慢"转化为客观的、可度量的数字。
核心概念探索
1. Core Web Vitals:Google 的三大核心指标
2020 年,Google 推出了 Core Web Vitals,包含三个核心指标:
LCP(Largest Contentful Paint):最大内容绘制
定义:页面主要内容(最大的图片或文本块)完成渲染的时间。
javascript
// 环境:浏览器
// 场景:监测 LCP
// 使用 PerformanceObserver API
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime);
console.log('LCP element:', lastEntry.element);
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
// LCP 测量的是什么?
// - <img> 元素
// - <svg> 内的 <image> 元素
// - <video> 元素的封面图
// - 带背景图的块级元素
// - 包含文本的块级元素
评分标准:
- 优秀(Good):< 2.5 秒
- 需要改进(Needs Improvement):2.5 - 4.0 秒
- 差(Poor):> 4.0 秒
常见问题与优化:
html
// 问题 1:大图片加载慢
// 解决方案:
// 1. 图片压缩
// 使用 WebP/AVIF 格式,压缩率更高
<img src="hero.webp" alt="Hero image">
// 2. 响应式图片
<img
srcset="hero-320w.jpg 320w,
hero-640w.jpg 640w,
hero-1280w.jpg 1280w"
sizes="(max-width: 640px) 100vw, 50vw"
src="hero-1280w.jpg"
alt="Hero image"
>
// 3. 预加载关键图片
<link rel="preload" as="image" href="hero.jpg">
// 4. 使用 CDN
<img src="https://cdn.example.com/hero.jpg" alt="Hero image">
css
/* 问题 2:自定义字体阻塞文本渲染 */
/* 解决方案:使用 font-display */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap; /* 先显示系统字体,字体加载完再替换 */
}
/* font-display 选项:
- auto: 浏览器默认行为
- block: 阻塞渲染(不推荐)
- swap: 立即显示后备字体,加载完替换(推荐)
- fallback: 短暂阻塞(100ms),超时显示后备字体
- optional: 极短阻塞,网络慢时放弃加载自定义字体
*/
FID(First Input Delay)→ INP(Interaction to Next Paint)
注意:FID 已被 INP 取代(2024 年 3 月),但很多资料仍在讨论 FID。
FID 定义:用户首次与页面交互(点击、输入)到浏览器响应的延迟。
INP 定义:整个页面生命周期中,所有交互延迟的代表值(通常是 98 分位数)。
javascript
// 环境:浏览器
// 场景:监测 INP(简化版)
let interactions = [];
// 监听所有交互事件
['pointerdown', 'click', 'keydown'].forEach(type => {
document.addEventListener(type, (event) => {
const startTime = performance.now();
// 使用 requestAnimationFrame 测量到下一帧的时间
requestAnimationFrame(() => {
const delay = performance.now() - startTime;
interactions.push(delay);
console.log(`${type} delay:`, delay);
});
});
});
// INP 计算(简化版,实际更复杂)
function calculateINP() {
if (interactions.length === 0) return 0;
// 取 98 分位数
const sorted = interactions.sort((a, b) => a - b);
const index = Math.floor(sorted.length * 0.98);
return sorted[index];
}
评分标准:
- 优秀(Good):< 200 毫秒
- 需要改进(Needs Improvement):200 - 500 毫秒
- 差(Poor):> 500 毫秒
常见问题与优化:
javascript
// 问题 1:长任务阻塞主线程
// 主线程被 JavaScript 执行占用,无法响应用户交互
// ❌ 不好的做法:同步处理大量数据
function processLargeData(data) {
const result = [];
for (let i = 0; i < data.length; i++) {
// 复杂计算
result.push(expensiveOperation(data[i]));
}
return result;
}
// ✅ 好的做法 1:分片处理(Time Slicing)
async function processLargeDataInChunks(data, chunkSize = 100) {
const result = [];
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
// 处理一个分片
for (const item of chunk) {
result.push(expensiveOperation(item));
}
// 让出主线程,让浏览器有机会响应用户交互
await new Promise(resolve => setTimeout(resolve, 0));
}
return result;
}
// ✅ 好的做法 2:使用 Web Worker
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ data: largeData });
worker.onmessage = (event) => {
const result = event.data;
console.log('Processing complete:', result);
};
// worker.js
self.onmessage = (event) => {
const { data } = event.data;
const result = data.map(item => expensiveOperation(item));
self.postMessage(result);
};
javascript
// 问题 2:事件处理函数执行时间过长
// ❌ 不好的做法:在 click 事件中执行耗时操作
button.addEventListener('click', () => {
// 耗时操作
const result = processLargeData(data);
updateUI(result);
});
// ✅ 好的做法 1:防抖(Debounce)
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
input.addEventListener('input', debounce((event) => {
search(event.target.value);
}, 300));
// ✅ 好的做法 2:节流(Throttle)
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
window.addEventListener('scroll', throttle(() => {
updateScrollPosition();
}, 100));
CLS(Cumulative Layout Shift):累积布局偏移
定义:页面生命周期内所有意外布局偏移的累积分数。
javascript
// 环境:浏览器
// 场景:监测 CLS
let clsScore = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 只统计非用户交互引起的布局偏移
if (!entry.hadRecentInput) {
clsScore += entry.value;
console.log('Layout shift:', entry.value);
console.log('Cumulative CLS:', clsScore);
}
}
});
observer.observe({ entryTypes: ['layout-shift'] });
// 布局偏移分数计算:
// score = impact fraction × distance fraction
// - impact fraction: 移动元素占视口的比例
// - distance fraction: 移动距离占视口的比例
评分标准:
- 优秀(Good):< 0.1
- 需要改进(Needs Improvement):0.1 - 0.25
- 差(Poor):> 0.25
常见问题与优化:
html
<!-- 问题 1:图片、视频没有设置尺寸 -->
<!-- ❌ 不好的做法:没有指定尺寸 -->
<img src="hero.jpg" alt="Hero">
<!-- 图片加载完成前,浏览器不知道要预留多少空间
图片加载完成后,下方内容会被推下去,造成布局偏移 -->
<!-- ✅ 好的做法 1:显式指定宽高 -->
<img src="hero.jpg" alt="Hero" width="1200" height="800">
<!-- ✅ 好的做法 2:使用 aspect-ratio(推荐) -->
<style>
.hero-image {
width: 100%;
aspect-ratio: 16 / 9; /* 浏览器会自动计算高度 */
}
</style>
<img src="hero.jpg" alt="Hero" class="hero-image">
html
<!-- 问题 2:动态插入内容(广告、提示条) -->
<!-- ❌ 不好的做法:直接插入,推开现有内容 -->
<div id="banner"></div>
<script>
// 1 秒后加载广告
setTimeout(() => {
document.getElementById('banner').innerHTML = '<div class="ad">广告内容</div>';
}, 1000);
// 广告出现时,下方内容被推下去
</script>
<!-- ✅ 好的做法:预留空间 -->
<div id="banner" style="min-height: 100px;">
<!-- 即使内容未加载,也会预留 100px 空间 -->
</div>
<!-- ✅ 更好的做法:使用骨架屏 -->
<div id="banner">
<div class="skeleton" style="height: 100px; background: #f0f0f0;"></div>
</div>
css
/* 问题 3:Web 字体加载导致文本跳动 */
/* ❌ 不好的做法:默认行为 */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
}
body {
font-family: 'CustomFont', sans-serif;
}
/* 字体加载前显示后备字体,加载后切换,可能导致布局偏移 */
/* ✅ 好的做法:使用 font-display: optional */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: optional; /* 如果字体加载慢,就不使用,避免布局偏移 */
}
/* ✅ 更好的做法:调整后备字体以匹配自定义字体 */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap;
size-adjust: 100%; /* 调整字体大小以匹配后备字体 */
}
2. 其他重要指标
TTFB(Time to First Byte):首字节时间
服务器响应第一个字节的时间。
javascript
// 环境:浏览器
// 场景:测量 TTFB
const navigationTiming = performance.getEntriesByType('navigation')[0];
const ttfb = navigationTiming.responseStart - navigationTiming.requestStart;
console.log('TTFB:', ttfb);
// 影响 TTFB 的因素:
// 1. 服务器处理时间
// 2. 网络延迟
// 3. DNS 查询时间
// 4. TCP 连接时间
// 5. TLS 握手时间(HTTPS)
优化方向:
- 使用 CDN(减少网络延迟)
- 服务端缓存(减少处理时间)
- 数据库查询优化
- 使用 HTTP/2 或 HTTP/3
- DNS 预解析
FCP(First Contentful Paint):首次内容绘制
浏览器渲染第一个 DOM 内容的时间(文本、图片、Canvas等)。
javascript
// 环境:浏览器
// 场景:测量 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'] });
// FCP vs LCP:
// FCP:第一个内容(可能是小图标、文字)
// LCP:最大内容(通常是主图、标题)
// FCP < LCP
TTI(Time to Interactive):可交互时间
页面完全可交互的时间(主线程空闲,没有长任务)。
arduino
// TTI 的判定标准:
// 1. FCP 已完成
// 2. 最近 5 秒内没有长任务(执行时间 > 50ms)
// 3. 没有超过 2 个正在进行的网络请求
// 这个指标较难直接测量,通常使用工具(Lighthouse)
实际场景思考
场景 1:使用 Lighthouse 进行性能分析
Lighthouse 是 Chrome 内置的性能分析工具。
步骤:
- 打开 Chrome DevTools(F12)
- 切换到 "Lighthouse" 标签
- 选择 "Performance" 分析类型
- 选择设备(Desktop / Mobile)
- 点击 "Analyze page load"
Lighthouse 会给出:
- Performance 分数(0-100)
- Core Web Vitals 指标
- 优化建议(按影响排序)
- 诊断信息(资源加载瀑布图等)
解读 Lighthouse 报告:
yaml
// 示例报告:
Performance: 65/100 // 需要改进
Metrics:
FCP: 1.8s ✓ 优秀
LCP: 4.2s ✗ 差
TBT: 350ms ⚠ 需要改进
CLS: 0.05 ✓ 优秀
SI: 3.2s ⚠ 需要改进
Opportunities (优化建议):
1. Eliminate render-blocking resources
→ 节省 1.2s
→ 建议:使用 async/defer 加载 JS
2. Properly size images
→ 节省 800ms
→ 建议:压缩图片,使用 WebP
3. Reduce unused JavaScript
→ 节省 600ms
→ 建议:代码分割,移除未使用代码
Diagnostics (诊断信息):
- Minimize main-thread work (主线程工作时间: 3.5s)
- Reduce JavaScript execution time (JS 执行: 2.1s)
场景 2:实现性能监控
在生产环境中,我们需要持续监控真实用户的性能数据(RUM: Real User Monitoring)。
javascript
// 环境:浏览器
// 场景:收集并上报性能数据
class PerformanceMonitor {
constructor() {
this.metrics = {};
}
// 收集 Core Web Vitals
collectWebVitals() {
// LCP
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.lcp = lastEntry.renderTime || lastEntry.loadTime;
}).observe({ entryTypes: ['largest-contentful-paint'] });
// FID (已废弃,这里仅作示例)
new PerformanceObserver((list) => {
const entries = list.getEntries();
this.metrics.fid = entries[0].processingStart - entries[0].startTime;
}).observe({ entryTypes: ['first-input'] });
// CLS
let clsScore = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsScore += entry.value;
}
}
this.metrics.cls = clsScore;
}).observe({ entryTypes: ['layout-shift'] });
}
// 收集导航时间
collectNavigationTiming() {
const timing = performance.getEntriesByType('navigation')[0];
this.metrics.ttfb = timing.responseStart - timing.requestStart;
this.metrics.domContentLoaded = timing.domContentLoadedEventEnd - timing.domContentLoadedEventStart;
this.metrics.loadComplete = timing.loadEventEnd - timing.loadEventStart;
}
// 上报数据
report() {
// 页面即将卸载时上报
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.sendMetrics();
}
});
}
sendMetrics() {
// 使用 sendBeacon 确保数据发送成功(即使页面关闭)
const data = JSON.stringify({
url: location.href,
metrics: this.metrics,
userAgent: navigator.userAgent,
timestamp: Date.now(),
});
navigator.sendBeacon('/api/performance', data);
}
}
// 使用
const monitor = new PerformanceMonitor();
monitor.collectWebVitals();
monitor.collectNavigationTiming();
monitor.report();
场景 3:优化首屏 LCP
一个实际的优化案例:
arduino
// 问题:电商网站的商品详情页 LCP 达到 5.2 秒
// 排查步骤:
// 1. 运行 Lighthouse,发现 LCP 元素是商品主图(1.5MB)
// 2. 检查 Network 面板,发现:
// - 图片从第三方 CDN 加载(500ms TTFB)
// - 图片格式是 JPG,未压缩
// - 图片加载优先级较低
// 优化方案:
// 方案 1:图片优化
// - 压缩:1.5MB → 300KB
// - 格式转换:JPG → WebP
// - 响应式图片:不同设备加载不同尺寸
html
<!-- 优化前 -->
<img src="https://cdn.example.com/product-large.jpg" alt="Product">
<!-- 优化后 -->
<picture>
<source
type="image/webp"
srcset="https://cdn.example.com/product-320.webp 320w,
https://cdn.example.com/product-640.webp 640w,
https://cdn.example.com/product-1280.webp 1280w"
sizes="(max-width: 640px) 100vw, 50vw"
>
<img
src="https://cdn.example.com/product-1280.jpg"
alt="Product"
width="1280"
height="960"
>
</picture>
html
<!-- 方案 2:预加载关键图片 -->
<head>
<link rel="preload" as="image" href="https://cdn.example.com/product-1280.webp" fetchpriority="high">
</head>
javascript
// 方案 3:图片懒加载(非 LCP 图片)
// 只懒加载首屏以外的图片,首屏图片立即加载
// 环境:浏览器
// 场景:原生懒加载
// LCP 图片(首屏主图):不懒加载
<img src="hero.jpg" alt="Hero" loading="eager">
// 其他图片(首屏以下):懒加载
<img src="detail-1.jpg" alt="Detail" loading="lazy">
// 或者使用 Intersection Observer 实现更精细的控制
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
优化结果:
- LCP:5.2s → 2.1s
- 图片大小:1.5MB → 300KB(WebP)
- Performance 分数:45 → 78
场景 4:优化 INP(减少交互延迟)
javascript
// 问题:数据表格的排序功能响应慢
// ❌ 原始代码:同步排序 10000 条数据
function sortTable(data, key) {
const sorted = data.sort((a, b) => a[key] - b[key]);
renderTable(sorted);
}
button.addEventListener('click', () => {
sortTable(largeData, 'price');
});
// 问题:
// - 排序操作阻塞主线程(~500ms)
// - 用户点击按钮后,需要等待 500ms 才看到反馈
// ✅ 优化方案 1:立即反馈 + 异步处理
button.addEventListener('click', async () => {
// 立即显示加载状态
showLoading();
// 让出主线程
await new Promise(resolve => setTimeout(resolve, 0));
// 执行排序
const sorted = largeData.sort((a, b) => a.price - b.price);
// 更新 UI
renderTable(sorted);
hideLoading();
});
// ✅ 优化方案 2:使用 Web Worker
// main.js
button.addEventListener('click', () => {
showLoading();
const worker = new Worker('sort-worker.js');
worker.postMessage({ data: largeData, key: 'price' });
worker.onmessage = (event) => {
renderTable(event.data);
hideLoading();
worker.terminate();
};
});
// sort-worker.js
self.onmessage = (event) => {
const { data, key } = event.data;
const sorted = data.sort((a, b) => a[key] - b[key]);
self.postMessage(sorted);
};
// ✅ 优化方案 3:虚拟滚动(如果是长列表)
// 只渲染可见部分,减少 DOM 操作
场景 5:优化 CLS(减少布局偏移)
html
// 问题:新闻网站的文章页,广告加载导致内容跳动
// ❌ 原始代码
<div class="article">
<h1>文章标题</h1>
<div id="ad-slot"></div> <!-- 广告位 -->
<p>文章内容...</p>
</div>
<script>
// 2 秒后加载广告
setTimeout(() => {
const ad = document.getElementById('ad-slot');
ad.innerHTML = '<img src="ad.jpg" width="728" height="90">';
}, 2000);
// 问题:广告出现时,文章内容被推下去
</script>
// ✅ 优化方案 1:预留空间
<div class="article">
<h1>文章标题</h1>
<div id="ad-slot" style="min-height: 90px; width: 728px;">
<!-- 预留广告位空间 -->
</div>
<p>文章内容...</p>
</div>
// ✅ 优化方案 2:使用 CSS aspect-ratio
<style>
.ad-container {
width: 100%;
max-width: 728px;
aspect-ratio: 728 / 90; /* 自动计算高度 */
background: #f0f0f0; /* 占位背景色 */
}
</style>
<div class="article">
<h1>文章标题</h1>
<div class="ad-container" id="ad-slot"></div>
<p>文章内容...</p>
</div>
// ✅ 优化方案 3:使用骨架屏
<div class="article">
<h1>文章标题</h1>
<div class="ad-skeleton" id="ad-slot">
<div class="skeleton-box" style="width: 728px; height: 90px;"></div>
</div>
<p>文章内容...</p>
</div>
<style>
.skeleton-box {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
延伸与发散
1. 实验室数据 vs 真实用户数据
arduino
// 实验室数据(Lab Data):
// - 工具:Lighthouse、WebPageTest
// - 环境:固定的设备、网络条件
// - 优点:可重复、可控
// - 缺点:不代表真实用户体验
// 真实用户数据(RUM: Real User Monitoring):
// - 来源:真实用户的浏览器
// - 环境:各种设备、网络条件
// - 优点:反映真实情况
// - 缺点:数据噪音大、难以复现问题
// 最佳实践:两者结合
// 1. 开发阶段:使用 Lighthouse 发现问题
// 2. 上线后:使用 RUM 监控真实性能
// 3. 发现问题:使用 Lighthouse 重现和调试
2. 性能预算(Performance Budget)
javascript
// 性能预算是一种约束机制
// 为项目设定性能目标,超出目标时自动报警或阻止部署
// 示例:package.json 中的性能预算配置
{
"budgets": [
{
"type": "bundle",
"name": "main",
"baseline": "500KB",
"warning": "600KB",
"error": "800KB"
},
{
"type": "initial",
"baseline": "1MB",
"warning": "1.2MB",
"error": "1.5MB"
}
]
}
// Webpack 插件:
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html',
}),
],
performance: {
maxAssetSize: 500000, // 500KB
maxEntrypointSize: 800000, // 800KB
hints: 'error', // 超出预算时报错
},
};
3. 移动端性能优化
arduino
// 移动端的特殊考虑:
// 1. 网络条件差
// - 使用 3G/4G 网络测试
// - 减少资源大小
// - 使用 HTTP/2 多路复用
// 2. CPU 性能弱
// - 减少 JavaScript 执行时间
// - 避免复杂的 CSS 选择器
// - 使用 GPU 加速(transform, opacity)
// 3. 电池续航
// - 减少网络请求
// - 避免频繁的动画
// - 使用 Intersection Observer 代替 scroll 事件
// Chrome DevTools 中模拟移动设备:
// 1. 打开 DevTools → 切换到移动设备模式(Ctrl+Shift+M)
// 2. 选择设备(iPhone, Pixel 等)
// 3. 设置网络条件(Fast 3G, Slow 3G)
// 4. 设置 CPU 限制(4x slowdown)
4. 渐进式 Web 应用(PWA)与性能
javascript
// PWA 的性能优势:
// 1. Service Worker 缓存
// - 离线可用
// - 瞬时加载
// 2. App Shell 架构
// - 缓存应用外壳
// - 动态加载内容
// 示例:缓存应用外壳
// service-worker.js
const CACHE_NAME = 'app-shell-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png',
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
知识点快速回顾
(30 秒版本)
Q: 什么是 Core Web Vitals?包含哪些指标?
A: Core Web Vitals 是 Google 推出的三个核心性能指标:
- LCP(Largest Contentful Paint) :最大内容绘制时间,测量加载性能,目标 < 2.5s
- INP(Interaction to Next Paint) :交互响应时间,测量交互性能,目标 < 200ms(之前是 FID)
- CLS(Cumulative Layout Shift) :累积布局偏移,测量视觉稳定性,目标 < 0.1
Q: 如何测量网站性能?
A:
- 实验室数据:使用 Lighthouse、Chrome DevTools Performance 面板、WebPageTest
- 真实用户数据:使用 Performance API 收集指标,上报到后端分析
- 两者结合:开发阶段用 Lighthouse,上线后监控真实用户数据
(2 分钟版本)
Q: 如何优化 LCP?
A: 常见优化手段:
- 图片优化:压缩、使用 WebP/AVIF、响应式图片、CDN
- 预加载关键资源 :
<link rel="preload">提高优先级 - 减少阻塞资源:CSS 内联关键样式、JS 使用 defer/async
- 服务端优化:减少 TTFB、使用 SSR/SSG
- 字体优化 :
font-display: swap、字体子集化
Q: 什么情况会导致 CLS?如何避免?
A: 常见原因:
- 图片/视频没有设置尺寸
- 动态插入内容(广告、提示条)
- Web 字体加载导致文本重排
优化方案:
- 为图片设置
width和height属性 - 使用
aspect-ratioCSS 属性 - 为动态内容预留空间(骨架屏)
- 使用
font-display: optional避免字体切换
Q: 如何优化 INP(减少交互延迟)?
A:
- 减少主线程阻塞:分片处理大任务、使用 Web Worker
- 优化事件处理:防抖(debounce)、节流(throttle)
- 代码分割:按需加载,减少初始 JavaScript 大小
- 避免长任务 :单个任务不超过 50ms,使用
requestIdleCallback
Q: Lighthouse 分数如何计算?各指标权重是多少?
A: Lighthouse Performance 分数(2024 年权重):
- LCP: 25%
- TBT (Total Blocking Time): 30%
- CLS: 25%
- FCP: 10%
- Speed Index: 10%
分数计算:每个指标有各自的评分曲线,加权平均后得到总分(0-100)。
有关性能指标与优化的高频关键概念
面试时,回答中尽量涵盖这些关键词:
- Core Web Vitals
- LCP / FCP / TTFB
- INP / FID
- CLS
- Lighthouse / Performance API
- 长任务(Long Task)
- 主线程阻塞(Main Thread Blocking)
- 预加载(Preload)
- 懒加载(Lazy Loading)
- 骨架屏(Skeleton Screen)
- 性能预算(Performance Budget)
- RUM(Real User Monitoring)
容易踩的坑
- 过度优化单一指标:盯着 LCP 优化,忽略了 CLS,导致用户体验变差
- 只关注实验室数据:Lighthouse 分数很高,但真实用户反馈慢(网络环境差异)
- 滥用预加载 :给太多资源添加
preload,反而延迟了其他关键资源 - 忽略移动端:桌面端性能好,移动端很差(CPU、网络限制)
- 懒加载首屏图片:LCP 元素不应该懒加载,否则加载更慢
性能优化优先级
markdown
1. 影响 Core Web Vitals 的问题(优先级最高)
├─ LCP > 4s → 立即优化
├─ INP > 500ms → 立即优化
└─ CLS > 0.25 → 立即优化
2. 其他性能问题
├─ TTFB > 1s → 服务端优化
├─ FCP > 3s → 减少阻塞资源
└─ Bundle 过大 → 代码分割
3. 体验优化
├─ 添加加载状态
├─ 骨架屏
└─ 渐进式渲染
小结
性能指标是连接主观体验和客观数据的桥梁。理解 Core Web Vitals(LCP、INP、CLS)的含义、测量方法和优化手段,能帮助我们系统性地提升 Web 应用性能。
这篇文章主要探讨了:
- Core Web Vitals 三大核心指标
- 其他重要性能指标(TTFB、FCP、TTI)
- 使用 Lighthouse 进行性能分析
- 实际优化案例(LCP、INP、CLS)
- 性能监控的实现
- 性能预算的概念
参考资料
- Web Vitals - Google - Core Web Vitals 官方介绍
- Lighthouse Performance Scoring - Google - Lighthouse 评分机制
- Optimize Largest Contentful Paint - web.dev - LCP 优化指南
- Optimize Interaction to Next Paint - web.dev - INP 优化指南
- Optimize Cumulative Layout Shift - web.dev - CLS 优化指南
- Performance APIs - MDN - Performance API 文档
- User-centric performance metrics - web.dev - 以用户为中心的性能指标