Core Web Vitals 优化与调优 Checklist
Core Web Vitals 是 Google 用于衡量用户体验的三个核心指标——LCP(加载)、CLS(视觉稳定性)、INP(交互响应)。它们直接影响 SEO 排名和用户留存。本章逐一拆解每个指标的原理和优化手段,并提供一份可直接执行的 Nuxt4 性能调优 Checklist。
1. 三大核心指标详解
1.1 LCP(Largest Contentful Paint)
定义:页面最大内容元素渲染完成的时间。通常是首屏的 Hero 图片、视频封面、或大段文字。
| 评级 | 时间 |
|---|---|
| 🟢 Good | ≤ 2.5s |
| 🟡 Needs Improvement | 2.5s - 4.0s |
| 🔴 Poor | > 4.0s |
LCP 元素类型(按优先级):
<img>图片<video>的 poster 图- 通过
background-image加载的图片 - 包含文本的块级元素
影响 LCP 的四个阶段:
TTFB(服务端响应)→ 资源发现 → 资源下载 → 元素渲染
每个阶段都可能是瓶颈——TTFB 慢说明 SSR 或数据库查询慢,资源下载慢说明图片未优化,元素渲染慢说明 CSS/JS 阻塞了关键渲染路径。
1.2 CLS(Cumulative Layout Shift)
定义:页面加载过程中,元素意外移动的累积量。每次布局偏移的影响 = 偏移面积 × 偏移距离。
| 评级 | 分值 |
|---|---|
| 🟢 Good | ≤ 0.1 |
| 🟡 Needs Improvement | 0.1 - 0.25 |
| 🔴 Poor | > 0.25 |
常见触发 CLS 的场景:
- 图片/视频没有预设 width/height,加载后撑开布局
- Web 字体加载导致文本大小变化(FOIT/FOUT)
- 动态注入的广告、横幅
- 异步加载的组件没有骨架屏占位
1.3 INP(Interaction to Next Paint)
定义:用户交互(点击/按键/触摸)到下一帧渲染的延迟。取整个页面生命周期中最差的那次交互。
| 评级 | 时间 |
|---|---|
| 🟢 Good | ≤ 200ms |
| 🟡 Needs Improvement | 200ms - 500ms |
| 🔴 Poor | > 500ms |
INP 的三个阶段:
输入延迟(主线程忙碌)→ 事件处理(回调执行)→ 渲染延迟(DOM 更新 + 绘制)
INP 差通常是因为主线程被长任务(Long Task)阻塞——大量 JS 执行、同步 DOM 操作、复杂计算。
2. LCP 专项优化
2.1 优化 TTFB
| 手段 | 预期收益 | Nuxt 实现 |
|---|---|---|
| SSR 缓存(ISR) | TTFB 从 200ms → 10ms | routeRules: { '/': { isr: 60 } } |
| CDN 边缘缓存 | TTFB -100ms+ | s-maxage + CDN |
| 数据库查询优化 | SSR 渲染 -50ms+ | 索引优化、缓存热点查询 |
| 流式 SSR | TTFB -50% | 下一章详述 |
2.2 优化资源发现
让浏览器尽早发现 LCP 资源:
// 在 useHead 中预加载 LCP 图片
useHead({
link: [{
rel: 'preload',
as: 'image',
href: video.value.thumbnail,
type: 'image/avif',
fetchpriority: 'high',
}],
})或使用 <NuxtImg> 的 preload 属性(自动生成 <link rel="preload">)。
2.3 优化资源下载
- 图片格式转换(AVIF/WebP),见第 36 章
- 响应式图片(移动端不下载 4K 图)
- HTTP/2 多路复用
- CDN 就近分发
2.4 优化渲染
- 避免 CSS
@import(阻塞渲染) - 关键 CSS 内联(见下文)
- 减少渲染阻塞的 JS(
defer/async)
3. CLS 专项优化
3.1 图片尺寸预占位
最有效的 CLS 修复——始终为图片设置 width 和 height:
<!-- ✅ 浏览器根据 width/height 计算宽高比,预留空间 -->
<NuxtImg src="/cover.jpg" width="1280" height="720" class="w-full h-auto" />
<!-- ❌ 没有尺寸,加载前高度为 0,加载后撑开布局 -->
<img src="/cover.jpg" class="w-full" />CSS 的 aspect-ratio 也可以预占空间:
.video-thumbnail {
width: 100%;
aspect-ratio: 16 / 9; /* 固定宽高比 */
object-fit: cover;
}3.2 骨架屏占位
异步加载的内容应使用骨架屏避免 CLS:
<template>
<div v-if="pending" class="space-y-4">
<!-- 骨架屏:和实际内容同尺寸 -->
<div class="h-8 w-48 bg-gray-200 animate-pulse rounded" />
<div class="aspect-video bg-gray-200 animate-pulse rounded" />
<div class="h-4 w-full bg-gray-200 animate-pulse rounded" />
</div>
<VideoDetail v-else :video="data" />
</template>3.3 动态内容稳定策略
| 场景 | CLS 风险 | 解决方案 |
|---|---|---|
| 广告位 | 高 | CSS 预留固定高度 min-height: 250px |
| 懒加载图片 | 中 | width + height 或 aspect-ratio |
| Web 字体 | 中 | font-display: swap + size-adjust |
| Toast 通知 | 低 | 用 position: fixed 脱离文档流 |
| 异步组件 | 中 | <Suspense> + fallback slot |
4. INP 专项优化
4.1 减少主线程阻塞
识别长任务:在 Chrome DevTools 的 Performance 面板中,超过 50ms 的任务会标记为红色。
拆分长任务:用 requestAnimationFrame 或 scheduler.yield() 让出主线程:
// 大量 DOM 操作时,分批执行
async function renderVideoList(videos: Video[]) {
for (let i = 0; i < videos.length; i += 10) {
const batch = videos.slice(i, i + 10)
batch.forEach(renderVideoCard)
// 每渲染 10 个,让出主线程
await new Promise(resolve => requestAnimationFrame(resolve))
}
}4.2 事件处理优化
// ❌ 点击后同步执行复杂计算
function onSearch() {
const results = heavySearch(query.value) // 阻塞 100ms+
searchResults.value = results
}
// ✅ 先响应 UI,再异步计算
async function onSearch() {
isSearching.value = true // 立即显示加载状态
await nextTick() // 让 Vue 更新 DOM
const results = await runInWorker(() => heavySearch(query.value))
searchResults.value = results
isSearching.value = false
}4.3 虚拟列表
大量列表渲染是 INP 的头号杀手。使用虚拟列表只渲染可见区域:
<template>
<!-- 1000 个视频,只渲染可见的 20 个 -->
<VirtualList :items="videos" :item-height="200" :buffer="5">
<template #default="{ item }">
<VideoCard :video="item" />
</template>
</VirtualList>
</template>推荐库:@tanstack/vue-virtual、vue-virtual-scroller。
5. 字体加载优化
5.1 字体对 CLS 和 LCP 的影响
Web 字体加载时,浏览器有两种行为:
- FOIT(Flash of Invisible Text):字体加载前文本不可见(默认行为)→ 影响 LCP
- FOUT(Flash of Unstyled Text):先用系统字体,加载后替换 → 影响 CLS
5.2 最佳实践
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* 先用系统字体,加载后替换 */
unicode-range: U+0000-00FF; /* 只加载需要的字符集 */
}// nuxt.config.ts — 预加载关键字体
export default defineNuxtConfig({
app: {
head: {
link: [{
rel: 'preload',
as: 'font',
type: 'font/woff2',
href: '/fonts/custom.woff2',
crossorigin: 'anonymous',
}],
},
},
})5.3 @nuxtjs/google-fonts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/google-fonts'],
googleFonts: {
families: { 'Noto Sans SC': [400, 700] },
display: 'swap',
download: true, // 下载到本地,避免外部请求
preload: true,
},
})6. 关键 CSS 内联
6.1 为什么需要内联
外部 CSS 文件是渲染阻塞资源——浏览器必须下载完 CSS 才能渲染页面。如果把首屏需要的 CSS 内联到 <style> 标签中,可以消除这个阻塞。
6.2 Nuxt 的内联策略
Nuxt4 在 SSR 模式下,会自动将首屏组件的 <style> 内联到 HTML 中。你可以通过配置进一步优化:
// nuxt.config.ts
export default defineNuxtConfig({
experimental: {
inlineSSRStyles: true, // 内联 SSR 样式(默认已开启)
},
})对于非首屏的 CSS,使用 <link rel="preload"> 异步加载:
<link rel="preload" href="/styles/below-fold.css" as="style" onload="this.rel='stylesheet'">7. AI 视频首屏优化复盘
以 AI 视频详情页为例,串联所有优化手段:
| 优化项 | 实施前 | 实施后 | 指标影响 |
|---|---|---|---|
| ISR 缓存详情页 | TTFB 350ms | TTFB 15ms | LCP -300ms |
| 封面图 AVIF + preload | 下载 2.1s | 下载 0.3s | LCP -1.8s |
| 封面图 width/height | CLS 0.18 | CLS 0.01 | CLS -94% |
| 字体 swap + preload | FOIT 400ms | FOUT 50ms | LCP -350ms |
| 播放器 SDK 动态导入 | 首屏 JS 380KB | 首屏 JS 180KB | TTI -500ms |
| 评论虚拟列表 | INP 450ms | INP 120ms | INP -73% |
综合效果:
| 指标 | 优化前 | 优化后 | 评级变化 |
|---|---|---|---|
| LCP | 4.8s | 1.6s | 🔴 → 🟢 |
| CLS | 0.22 | 0.02 | 🟡 → 🟢 |
| INP | 450ms | 120ms | 🟡 → 🟢 |
8. Nuxt4 性能调优 Checklist
8.1 构建与部署
- 运行
nuxt analyze,确认首屏 JS < 200KB gzip - 配置
manualChunks按更新频率分包 - 开启 Brotli 压缩(CDN 或 Nginx)
- 静态资源设置
Cache-Control: immutable - 配置 CDN 边缘缓存
8.2 SSR 与缓存
- 内容页面配置 ISR(
routeRules.isr) - 热点 API 使用
defineCachedEventHandler - Redis 缓存生产环境热点查询
- 用户专属数据设置
Cache-Control: no-store
8.3 图片
- 使用
@nuxt/image,配置 AVIF + WebP - 所有图片设置 width/height(防 CLS)
- LCP 图片设置
preload+fetchpriority="high" - 非首屏图片
loading="lazy"+placeholder
8.4 字体
-
font-display: swap - 关键字体
<link rel="preload"> - 使用
@nuxtjs/google-fonts下载到本地
8.5 JavaScript
- 大型库动态导入(播放器、编辑器、图表)
- 非首屏组件使用
Lazy前缀 - 使用 ESM 格式依赖(Tree-shakable)
- 复杂计算移到 Web Worker
8.6 CSS
- SSR 内联关键 CSS(Nuxt 默认开启)
- 避免
@import(使用<link>) - 移除未使用的 CSS(PurgeCSS / Tailwind JIT)
8.7 交互
- 长列表使用虚拟滚动
- 长任务拆分(
requestAnimationFrame) - 交互反馈先更新 UI,再异步处理
本章小结
- LCP:优化 TTFB(ISR 缓存)+ 资源发现(preload)+ 资源下载(AVIF)+ 渲染(关键 CSS 内联)
- CLS:图片 width/height + 骨架屏 +
font-display: swap+ 固定高度广告位 - INP:拆分长任务 + 虚拟列表 + 先响应 UI 再异步计算
- 字体:
swap+preload+ 本地化,避免 FOIT/FOUT - Checklist:构建/SSR/图片/字体/JS/CSS/交互 七个维度,逐项检查