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 Improvement2.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 Improvement0.1 - 0.25
🔴 Poor> 0.25

常见触发 CLS 的场景

  • 图片/视频没有预设 width/height,加载后撑开布局
  • Web 字体加载导致文本大小变化(FOIT/FOUT)
  • 动态注入的广告、横幅
  • 异步加载的组件没有骨架屏占位

1.3 INP(Interaction to Next Paint)

定义:用户交互(点击/按键/触摸)到下一帧渲染的延迟。取整个页面生命周期中最差的那次交互。

评级时间
🟢 Good≤ 200ms
🟡 Needs Improvement200ms - 500ms
🔴 Poor> 500ms

INP 的三个阶段

输入延迟(主线程忙碌)→ 事件处理(回调执行)→ 渲染延迟(DOM 更新 + 绘制)

INP 差通常是因为主线程被长任务(Long Task)阻塞——大量 JS 执行、同步 DOM 操作、复杂计算。

2. LCP 专项优化

2.1 优化 TTFB

手段预期收益Nuxt 实现
SSR 缓存(ISR)TTFB 从 200ms → 10msrouteRules: { '/': { isr: 60 } }
CDN 边缘缓存TTFB -100ms+s-maxage + CDN
数据库查询优化SSR 渲染 -50ms+索引优化、缓存热点查询
流式 SSRTTFB -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 + heightaspect-ratio
Web 字体font-display: swap + size-adjust
Toast 通知position: fixed 脱离文档流
异步组件&lt;Suspense&gt; + fallback slot

4. INP 专项优化

4.1 减少主线程阻塞

识别长任务:在 Chrome DevTools 的 Performance 面板中,超过 50ms 的任务会标记为红色。

拆分长任务:用 requestAnimationFramescheduler.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-virtualvue-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 内联到 &lt;style&gt; 标签中,可以消除这个阻塞。

6.2 Nuxt 的内联策略

Nuxt4 在 SSR 模式下,会自动将首屏组件的 &lt;style&gt; 内联到 HTML 中。你可以通过配置进一步优化:

// nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    inlineSSRStyles: true,  // 内联 SSR 样式(默认已开启)
  },
})

对于非首屏的 CSS,使用 &lt;link rel="preload"&gt; 异步加载:

<link rel="preload" href="/styles/below-fold.css" as="style" onload="this.rel='stylesheet'">

7. AI 视频首屏优化复盘

以 AI 视频详情页为例,串联所有优化手段:

优化项实施前实施后指标影响
ISR 缓存详情页TTFB 350msTTFB 15msLCP -300ms
封面图 AVIF + preload下载 2.1s下载 0.3sLCP -1.8s
封面图 width/heightCLS 0.18CLS 0.01CLS -94%
字体 swap + preloadFOIT 400msFOUT 50msLCP -350ms
播放器 SDK 动态导入首屏 JS 380KB首屏 JS 180KBTTI -500ms
评论虚拟列表INP 450msINP 120msINP -73%

综合效果

指标优化前优化后评级变化
LCP4.8s1.6s🔴 → 🟢
CLS0.220.02🟡 → 🟢
INP450ms120ms🟡 → 🟢

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
  • 关键字体 &lt;link rel="preload"&gt;
  • 使用 @nuxtjs/google-fonts 下载到本地

8.5 JavaScript

  • 大型库动态导入(播放器、编辑器、图表)
  • 非首屏组件使用 Lazy 前缀
  • 使用 ESM 格式依赖(Tree-shakable)
  • 复杂计算移到 Web Worker

8.6 CSS

  • SSR 内联关键 CSS(Nuxt 默认开启)
  • 避免 @import(使用 &lt;link&gt;
  • 移除未使用的 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/交互 七个维度,逐项检查