name: 核心网页指标优化 description: 优化核心网页指标(LCP、INP、CLS)以提升页面体验和搜索排名。当被问及“改善核心网页指标”、“修复LCP”、“减少CLS”、“优化INP”、“页面体验优化”或“修复布局偏移”时使用。 license: MIT metadata: author: web-quality-skills version: ‘1.0’
核心网页指标优化
针对影响谷歌搜索排名和用户体验的三个核心网页指标进行定向优化。
三个指标
| 指标 | 衡量内容 | 良好 | 需要改进 | 差 |
|---|---|---|---|---|
| LCP | 加载 | ≤ 2.5s | 2.5s – 4s | > 4s |
| INP | 交互性 | ≤ 200ms | 200ms – 500ms | > 500ms |
| CLS | 视觉稳定性 | ≤ 0.1 | 0.1 – 0.25 | > 0.25 |
谷歌按第75百分位测量——75%的页面访问必须满足“良好”阈值。
LCP:最大内容绘制
LCP衡量最大可见内容元素渲染的时间。通常包括:
- 英雄图像或视频
- 大文本块
- 背景图像
<svg>元素
常见LCP问题
1. 服务器响应慢(TTFB > 800ms)
修复方法:CDN、缓存、优化后端、边缘渲染
2. 渲染阻塞资源
<!-- ❌ 阻塞渲染 -->
<link rel="stylesheet" href="/all-styles.css" />
<!-- ✅ 内联关键CSS,其余延迟加载 -->
<style>
/* 关键首屏CSS */
</style>
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
3. 资源加载时间慢
<!-- ❌ 无提示,发现晚 -->
<img src="/hero.jpg" alt="英雄" />
<!-- ✅ 高优先级预加载 -->
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high" />
<img src="/hero.webp" alt="英雄" fetchpriority="high" />
4. 客户端渲染延迟
// ❌ 内容在JavaScript后加载
useEffect(() => {
fetch('/api/hero-text')
.then((r) => r.json())
.then(setHeroText)
}, [])
// ✅ 服务器端或静态渲染
// 使用SSR、SSG或流式传输以发送带内容的HTML
export async function getServerSideProps() {
const heroText = await fetchHeroText()
return { props: { heroText } }
}
LCP优化清单
- [ ] TTFB < 800ms(使用CDN、边缘缓存)
- [ ] LCP图像用fetchpriority="high"预加载
- [ ] LCP图像优化(WebP/AVIF、正确尺寸)
- [ ] 关键CSS内联(< 14KB)
- [ ] <head>中无渲染阻塞JavaScript
- [ ] 字体不阻塞文本渲染(font-display: swap)
- [ ] LCP元素在初始HTML中(非JS渲染)
LCP元素识别
// 找到你的LCP元素
new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
console.log('LCP元素:', lastEntry.element)
console.log('LCP时间:', lastEntry.startTime)
}).observe({ type: 'largest-contentful-paint', buffered: true })
INP:交互到下一次绘制
INP衡量页面访问期间所有交互(点击、点击、按键)的响应性。它报告最差交互(高流量页面按第98百分位)。
INP分解
总INP = 输入延迟 + 处理时间 + 呈现延迟
| 阶段 | 目标 | 优化方法 |
|---|---|---|
| 输入延迟 | < 50ms | 减少主线程阻塞 |
| 处理 | < 100ms | 优化事件处理程序 |
| 呈现 | < 50ms | 最小化渲染工作 |
常见INP问题
1. 长任务阻塞主线程
// ❌ 长同步任务
function processLargeArray(items) {
items.forEach((item) => expensiveOperation(item))
}
// ✅ 分块并让步
async function processLargeArray(items) {
const CHUNK_SIZE = 100
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE)
chunk.forEach((item) => expensiveOperation(item))
// 让步给主线程
await new Promise((r) => setTimeout(r, 0))
// 或使用scheduler.yield()(可用时)
}
}
2. 重事件处理程序
// ❌ 所有工作在处理程序中
button.addEventListener('click', () => {
// 重计算
const result = calculateComplexThing()
// DOM更新
updateUI(result)
// 分析
trackEvent('click')
})
// ✅ 优先提供视觉反馈
button.addEventListener('click', () => {
// 立即视觉反馈
button.classList.add('loading')
// 延迟非关键工作
requestAnimationFrame(() => {
const result = calculateComplexThing()
updateUI(result)
})
// 使用requestIdleCallback进行分析
requestIdleCallback(() => trackEvent('click'))
})
3. 第三方脚本
// ❌ 急切加载,阻塞交互
;<script src="https://heavy-widget.com/widget.js"></script>
// ✅ 按交互或可见性延迟加载
const loadWidget = () => {
import('https://heavy-widget.com/widget.js').then((widget) => widget.init())
}
button.addEventListener('click', loadWidget, { once: true })
4. 过度重新渲染(React/Vue)
// ❌ 重新渲染整个树
function App() {
const [count, setCount] = useState(0)
return (
<div>
<Counter count={count} />
<ExpensiveComponent /> {/* 每次计数变化都重新渲染 */}
</div>
)
}
// ✅ 记忆化昂贵组件
const MemoizedExpensive = React.memo(ExpensiveComponent)
function App() {
const [count, setCount] = useState(0)
return (
<div>
<Counter count={count} />
<MemoizedExpensive />
</div>
)
}
INP优化清单
- [ ] 主线程上无任务 > 50ms
- [ ] 事件处理程序快速完成(< 100ms)
- [ ] 立即提供视觉反馈
- [ ] 重工作延迟使用requestIdleCallback
- [ ] 第三方脚本不阻塞交互
- [ ] 适当使用防抖输入处理程序
- [ ] CPU密集型操作使用Web Workers
INP调试
// 识别慢交互
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 200) {
console.warn('慢交互:', {
type: entry.name,
duration: entry.duration,
processingStart: entry.processingStart,
processingEnd: entry.processingEnd,
target: entry.target,
})
}
}
}).observe({ type: 'event', buffered: true, durationThreshold: 16 })
CLS:累积布局偏移
CLS衡量意外布局偏移。当可见元素在帧间无用户交互时改变位置发生偏移。
CLS公式: 影响分数 × 距离分数
常见CLS原因
1. 无尺寸图像
<!-- ❌ 加载时导致布局偏移 -->
<img src="photo.jpg" alt="照片" />
<!-- ✅ 预留空间 -->
<img src="photo.jpg" alt="照片" width="800" height="600" />
<!-- ✅ 或使用宽高比 -->
<img src="photo.jpg" alt="照片" style="aspect-ratio: 4/3; width: 100%;" />
2. 广告、嵌入和iframe
<!-- ❌ 加载前未知尺寸 -->
<iframe src="https://ad-network.com/ad"></iframe>
<!-- ✅ 用min-height预留空间 -->
<div style="min-height: 250px;">
<iframe src="https://ad-network.com/ad" height="250"></iframe>
</div>
<!-- ✅ 或使用宽高比容器 -->
<div style="aspect-ratio: 16/9;">
<iframe src="https://youtube.com/embed/..." style="width: 100%; height: 100%;"></iframe>
</div>
3. 动态注入内容
// ❌ 在视口上方插入内容
notifications.prepend(newNotification)
// ✅ 在视口下方插入或使用变换
const insertBelow = viewport.bottom < newNotification.top
if (insertBelow) {
notifications.prepend(newNotification)
} else {
// 无偏移动画进入
newNotification.style.transform = 'translateY(-100%)'
notifications.prepend(newNotification)
requestAnimationFrame(() => {
newNotification.style.transform = ''
})
}
4. 网页字体导致FOUT
/* ❌ 字体交换导致文本偏移 */
@font-face {
font-family: 'Custom';
src: url('custom.woff2') format('woff2');
}
/* ✅ 可选字体(慢时不偏移) */
@font-face {
font-family: 'Custom';
src: url('custom.woff2') format('woff2');
font-display: optional;
}
/* ✅ 或匹配后备字体指标 */
@font-face {
font-family: 'Custom';
src: url('custom.woff2') format('woff2');
font-display: swap;
size-adjust: 105%; /* 匹配后备字体尺寸 */
ascent-override: 95%;
descent-override: 20%;
}
5. 动画触发布局
/* ❌ 动画布局属性 */
.animate {
transition:
height 0.3s,
width 0.3s;
}
/* ✅ 使用变换代替 */
.animate {
transition: transform 0.3s;
}
.animate.expanded {
transform: scale(1.2);
}
CLS优化清单
- [ ] 所有图像有宽度/高度或宽高比
- [ ] 所有视频/嵌入有预留空间
- [ ] 广告有min-height容器
- [ ] 字体使用font-display: optional或匹配指标
- [ ] 动态内容在视口下方插入
- [ ] 动画仅使用变换/不透明度
- [ ] 无内容注入到现有内容上方
CLS调试
// 跟踪布局偏移
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
console.log('布局偏移:', entry.value)
entry.sources?.forEach((source) => {
console.log(' 偏移元素:', source.node)
console.log(' 先前矩形:', source.previousRect)
console.log(' 当前矩形:', source.currentRect)
})
}
}
}).observe({ type: 'layout-shift', buffered: true })
测量工具
实验室测试
- Chrome DevTools → 性能面板、Lighthouse
- WebPageTest → 详细瀑布图、胶片条
- Lighthouse CLI →
npx lighthouse <url>
现场数据(真实用户)
- Chrome用户体验报告(CrUX) → BigQuery或API
- Search Console → 核心网页指标报告
- web-vitals库 → 发送到你的分析系统
import { onLCP, onINP, onCLS } from 'web-vitals'
function sendToAnalytics({ name, value, rating }) {
gtag('event', name, {
event_category: 'Web Vitals',
value: Math.round(name === 'CLS' ? value * 1000 : value),
event_label: rating,
})
}
onLCP(sendToAnalytics)
onINP(sendToAnalytics)
onCLS(sendToAnalytics)
框架快速修复
Next.js
// LCP:使用next/image带priority
import Image from 'next/image'
;<Image src="/hero.jpg" priority fill alt="英雄" />
// INP:使用动态导入
const HeavyComponent = dynamic(() => import('./Heavy'), { ssr: false })
// CLS:图像组件自动处理尺寸
React
// LCP:在head中预加载
;<link rel="preload" href="/hero.jpg" as="image" fetchpriority="high" />
// INP:记忆化并使用useTransition
const [isPending, startTransition] = useTransition()
startTransition(() => setExpensiveState(newValue))
// CLS:始终在img标签中指定尺寸
Vue/Nuxt
<!-- LCP:使用nuxt/image带预加载 -->
<NuxtImg src="/hero.jpg" preload loading="eager" />
<!-- INP:使用异步组件 -->
<component :is="() => import('./Heavy.vue')" />
<!-- CLS:使用宽高比CSS -->
<img :style="{ aspectRatio: '16/9' }" />