关于 performance
面向开发者的性能优化技能,使用真实用户数据确定优先级,并通过浏览器跟踪诊断原因。它强调修改前后测量,并将静态检查结果标为假设。
主要功能
- 01现场与实验室性能基线
- 02Core Web Vitals 优先级判断
- 03基于跟踪的瓶颈诊断
- 04加载与运行时优化指导
- 05修改前后测量流程
适用场景
- 01查找 Core Web Vitals 不佳的原因
- 02对照基线验证性能改动
- 03在没有可运行页面时审查项目
MCP 相关
在可用 Chrome DevTools MCP 时,流程使用 `performance_start_trace` 和 `performance_analyze_insight` 记录并分析性能跟踪。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ addyosmani/ web-quality-skills --skill performance安装后在 Claude Code 中输入/performance确认已加载。
附带文件references/MEASUREMENT.mdreferences/RUM.md
详细介绍
性能优化
基于真实用户信号确定优化优先级,并使用浏览器跟踪记录进行诊断,以证据为依据优化性能。重点关注加载速度、运行时响应能力和资源交付。
工作原理
- 如果页面可以运行,请先阅读测量工作流,在修改前建立线上与实验室测量基线。
- 优先处理真实用户核心网页指标表现不佳的问题。使用 DevTools 性能跟踪记录及其针对性洞察查找原因。
- 仅检查和修改与已测量瓶颈相关的代码或资源。
- 重新运行条件等效的实验室测量,并报告优化前后的数值、测量条件和不确定性。线上验证仍需等待,直至收集到足够的新用户数据。
如果页面无法运行,请进行静态检查,但将发现称为假设,而非已测得的回归。为每个高影响假设提供可用于验证的命令或浏览器工作流。
优先使用能够记录性能跟踪记录并提供针对性洞察的浏览器工具。使用 Chrome DevTools MCP 时,请调用 performance_start_trace 和 performance_analyze_insight;不要通过 lighthouse_audit 进行性能分析,因为它涵盖的是 Lighthouse 中非性能类的类别。
初始性能预算
预算应反映产品的目标设备、网络、页面类型和用户旅程。以下数值是适用于典型内容页或电商页面的初始指导值,并非通用的通过/不通过标准。如果项目已有预算,请予以保留。
| 资源 | 预算 | 理由 |
|---|---|---|
| 页面总大小 | < 1.5 MB | 限制受限目标网络上的传输时间和数据成本;应使用有代表性的页面进行校准 |
| JavaScript(压缩后) | < 300 KB | 控制解析和执行成本 |
| CSS(压缩后) | < 100 KB | 限制阻塞渲染的工作 |
| 图片(首屏区域) | < 500 KB | 保障可能成为 LCP 资源的图片 |
| 字体 | < 100 KB | 限制关键字体传输量 |
| 第三方资源 | < 200 KB | 限制产品控制范围之外的代码 |
关键渲染路径
服务器响应
- TTFB < 800ms。 首字节时间应尽可能短。使用 CDN、缓存和高效的后端。
- 启用压缩。 对文本资源使用 Gzip 或 Brotli。优先使用 Brotli(体积小 15–20%)。
- 使用 HTTP/2 或 HTTP/3。 多路复用可减少连接开销。
- 边缘缓存。 尽可能在 CDN 边缘缓存 HTML。
- 针对已测量的文档延迟,考虑使用 Early Hints(HTTP 103)。 如果跟踪记录显示 HTML 生成缓慢,而关键子资源稳定,则在同一请求的正常最终响应之前,发送包含
Link标头的临时103响应。请使用 HTTP/2 或更高版本。CDN 可以根据较早的200响应中的Link标头合成103响应;来源站点或边缘处理程序也可以直接发送。对于不支持的客户端,服务器会继续发送最终响应,但请确认当前浏览器和基础设施的支持情况。提示仅限于已确认关键的预加载或预连接:不准确的提示会浪费带宽。Cloudflare 报告称,在一项人工构建的图片密集型测试中,LCP 提升了 20–30%;应将其视为供应商案例研究,而非预期收益,并测量实际结果。请参阅 MDN 的 103 实现示例和 Cloudflare 的研究。
资源加载
预连接到所需来源:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
预加载关键资源:
仅预加载那些延迟发现会在跟踪记录中显现出来的资源。每次预加载都会占用带宽,不必要的高优先级请求可能会延迟 LCP。
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">
<link rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin>
使用 Speculation Rules API 预渲染可能的后续导航:
<script type="speculationrules">
{
"prerender": [{
"where": { "href_matches": "/*" },
"eagerness": "moderate"
}]
}
</script>
moderate 等待比 eager 模式更强的意图信号。衡量预测命中率、传输字节数和服务器成本;错误的预渲染大致相当于一次未使用的导航。有关权衡以及分析所需的 prerenderingchange 门控,请参阅 core-web-vitals → LCP。
延迟加载非关键 CSS:
<style>/* Above-fold styles */</style>
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles.css"></noscript>
JavaScript 优化
延迟加载非必需脚本:
<script src="/critical.js"></script>
<script defer src="/app.js"></script>
<script async src="/analytics.js"></script>
<script type="module" src="/app.mjs"></script>
代码拆分模式:
// Route-based splitting
const Dashboard = lazy(() => import('./Dashboard'));
// Component-based splitting
const HeavyChart = lazy(() => import('./HeavyChart'));
// Feature-based splitting
if (user.isPremium) {
const PremiumFeatures = await import('./PremiumFeatures');
}
Tree shaking 最佳实践:
// ❌ Imports entire library
import _ from 'lodash';
_.debounce(fn, 300);
// ✅ Imports only what's needed
import debounce from 'lodash/debounce';
debounce(fn, 300);
图片优化
格式选择
| 格式 | 用例 | 浏览器支持 |
|---|---|---|
| AVIF | 照片,压缩效果最佳 | 92%+ |
| WebP | 照片,良好的后备格式 | 97%+ |
| PNG | 带透明效果的图形 | 通用 |
| SVG | 图标、徽标、插图 | 通用 |
响应式图片
<picture>
<source
type="image/avif"
srcset="hero-400.avif 400w,
hero-800.avif 800w,
hero-1200.avif 1200w"
sizes="(max-width: 600px) 100vw, 50vw">
<source
type="image/webp"
srcset="hero-400.webp 400w,
hero-800.webp 800w,
hero-1200.webp 1200w"
sizes="(max-width: 600px) 100vw, 50vw">
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw, 50vw"
width="1200"
height="600"
alt="Hero image"
loading="lazy"
decoding="async">
</picture>
LCP 图片优先级
<img
src="hero.webp"
fetchpriority="high"
loading="eager"
decoding="sync"
alt="Hero">
<img
src="product.webp"
loading="lazy"
decoding="async"
alt="Product">
字体优化
加载策略
/* System font stack as fallback */
body {
font-family: 'Custom Font', -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, sans-serif;
}
/* Prevent invisible text */
@font-face {
font-family: 'Custom Font';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* or optional for non-critical */
font-weight: 400;
font-style: normal;
unicode-range: U+0000-00FF; /* Subset to Latin */
}
预加载关键字体
<link rel="preload" href="/fonts/heading.woff2" as="font" type="font/woff2" crossorigin>
可变字体
/* One file instead of multiple weights */
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: swap;
}
缓存策略
Cache-Control 标头
# HTML (short or no cache)
Cache-Control: no-cache, must-revalidate
# Static assets with hash (immutable)
Cache-Control: public, max-age=31536000, immutable
# Static assets without hash
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
# API responses
Cache-Control: private, max-age=0, must-revalidate
Service Worker 缓存
// Cache-first for static assets
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image' ||
event.request.destination === 'style' ||
event.request.destination === 'script') {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request).then((response) => {
const clone = response.clone();
caches.open('static-v1').then((cache) => cache.put(event.request, clone));
return response;
});
})
);
}
});
运行时性能
避免布局抖动
// ❌ Forces multiple reflows
elements.forEach(el => {
const height = el.offsetHeight; // Read
el.style.height = height + 10 + 'px'; // Write
});
// ✅ Batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // All reads
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // All writes
});
对开销大的操作进行防抖
function debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
// Debounce scroll/resize handlers
window.addEventListener('scroll', debounce(handleScroll, 100));
使用 requestAnimationFrame
// ❌ May cause jank
setInterval(animate, 16);
// ✅ Synced with display refresh
function animate() {
// Animation logic
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
虚拟化长列表
// For lists > 100 items, render only visible items
// Use libraries like react-window, vue-virtual-scroller, or native CSS:
.virtual-list {
content-visibility: auto;
contain-intrinsic-size: 0 50px; /* Estimated item height */
}
使用 View Transitions 实现流畅导航
View Transitions API 让浏览器使用单个 GPU 合成快照,在两个 DOM 状态之间交叉淡入淡出(或自定义动画)——不会重复渲染,不会造成布局抖动,并且快照不会计入 CLS。
同文档(SPA 风格)——2026 年基线:
// Wrap the DOM mutation that swaps the view
function navigate(newView) {
if (!document.startViewTransition) return swapDOM(newView);
document.startViewTransition(() => swapDOM(newView));
}
跨文档(MPA 风格)——Chromium 稳定版支持,其他浏览器渐进增强:
/* On both source and destination pages */
@view-transition { navigation: auto; }
这就是完整的集成——现在同源导航会自动淡入淡出。若要让特定元素参与共享元素过渡(例如缩略图展开为主图),请为它们设置相同的 view-transition-name:
.product-thumb[data-id="42"], .product-hero { view-transition-name: product-42; }
将其与上文的 Speculation Rules 配合使用,即可实现即时且带动画的导航。
第三方脚本
加载策略
// ❌ Blocks main thread
<script src="https://analytics.example.com/script.js"></script>
// ✅ Async loading
<script async src="https://analytics.example.com/script.js"></script>
// ✅ Delay until interaction
<script>
document.addEventListener('DOMContentLoaded', () => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
const script = document.createElement('script');
script.src = 'https://widget.example.com/embed.js';
document.body.appendChild(script);
observer.disconnect();
}
});
observer.observe(document.querySelector('#widget-container'));
});
</script>
外观代理模式
<div class="youtube-facade"
data-video-id="abc123"
onclick="loadYouTube(this)">
<img src="/thumbnails/abc123.jpg" alt="Video title">
<button aria-label="Play video">▶</button>
</div>
测量
只要 URL 可运行,就使用测量工作流。该工作流定义了 Chrome DevTools MCP 路由、CrUX 和备用数据源、可重复的实验室条件,以及精简的证据格式。
| 指标 | 类型 | 说明 |
|---|---|---|
| p75 的 LCP、INP、CLS | 实际用户数据 | 反映用户体验结果的 Core Web Vitals;用于通过/失败优先级排序 |
| 跟踪记录中的 LCP、CLS | 实验室 | 针对单次导航的可重复诊断值 |
| TBT | 实验室 | 主线程阻塞诊断指标,也是 INP 的粗略代理指标,并非实际用户 INP |
| FCP、Speed Index | 实验室 | 加载诊断指标,并非 Core Web Vitals |
原始 PerformanceObserver 代码片段可用于当前浏览器会话,但本身并非真实用户数据。当用户需要生产环境遥测时,请阅读第一方 RUM 参考文档,并优先使用 web-vitals,而不是手工编写指标实现。
参考资料
有关 Core Web Vitals 专项优化,请参阅 Core Web Vitals。