core-web-vitals

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Core Web Vitals optimization

核心网页指标优化

Targeted optimization for the three Core Web Vitals metrics that affect Google Search ranking and user experience.
针对影响谷歌搜索排名和用户体验的三项核心网页指标进行针对性优化。

The three metrics

三项指标

MetricMeasuresGoodNeeds workPoor
LCPLoading≤ 2.5s2.5s – 4s> 4s
INPInteractivity≤ 200ms200ms – 500ms> 500ms
CLSVisual Stability≤ 0.10.1 – 0.25> 0.25
Google measures at the 75th percentile — 75% of page visits must meet "Good" thresholds.

指标衡量维度良好待改进较差
LCP加载速度≤ 2.5秒2.5秒 – 4秒> 4秒
INP交互响应性≤ 200毫秒200毫秒 – 500毫秒> 500毫秒
CLS视觉稳定性≤ 0.10.1 – 0.25> 0.25
谷歌以75百分位数为衡量标准——75%的页面访问必须达到「良好」阈值。

LCP: Largest Contentful Paint

LCP: Largest Contentful Paint(最大内容绘制)

LCP measures when the largest visible content element renders. Usually this is:
  • Hero image or video
  • Large text block
  • Background image
  • <svg>
    element
LCP衡量最大可见内容元素的渲染时间。通常包括:
  • 首屏图片或视频
  • 大文本块
  • 背景图片
  • <svg>
    元素

Common LCP issues

常见LCP问题

1. Slow server response (TTFB > 800ms)
Fix: CDN, caching, optimized backend, edge rendering
2. Render-blocking resources
html
<!-- ❌ Blocks rendering -->
<link rel="stylesheet" href="/all-styles.css">

<!-- ✅ Critical CSS inlined, rest deferred -->
<style>/* Critical above-fold CSS */</style>
<link rel="preload" href="/styles.css" as="style" 
      onload="this.onload=null;this.rel='stylesheet'">
3. Slow resource load times
html
<!-- ❌ No hints, discovered late -->
<img src="/hero.jpg" alt="Hero">

<!-- ✅ Preloaded with high priority -->
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">
<img src="/hero.webp" alt="Hero" fetchpriority="high">
4. Client-side rendering delays
javascript
// ❌ Content loads after JavaScript
useEffect(() => {
  fetch('/api/hero-text').then(r => r.json()).then(setHeroText);
}, []);

// ✅ Server-side or static rendering
// Use SSR, SSG, or streaming to send HTML with content
export async function getServerSideProps() {
  const heroText = await fetchHeroText();
  return { props: { heroText } };
}
1. 服务器响应缓慢(TTFB > 800毫秒)
修复方案:CDN、缓存、优化后端、边缘渲染
2. 阻塞渲染的资源
html
<!-- ❌ 阻塞渲染 -->
<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. 资源加载速度慢
html
<!-- ❌ 无预加载提示,发现较晚 -->
<img src="/hero.jpg" alt="Hero">

<!-- ✅ 高优先级预加载 -->
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">
<img src="/hero.webp" alt="Hero" fetchpriority="high">
4. 客户端渲染延迟
javascript
// ❌ 内容在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 optimization checklist

LCP优化检查清单

markdown
- [ ] TTFB < 800ms (use CDN, edge caching)
- [ ] LCP image preloaded with fetchpriority="high"
- [ ] LCP image optimized (WebP/AVIF, correct size)
- [ ] Critical CSS inlined (< 14KB)
- [ ] No render-blocking JavaScript in <head>
- [ ] Fonts don't block text rendering (font-display: swap)
- [ ] LCP element in initial HTML (not JS-rendered)
markdown
- [ ] TTFB < 800毫秒(使用CDN、边缘缓存)
- [ ] LCP图片通过fetchpriority="high"预加载
- [ ] LCP图片已优化(WebP/AVIF格式,尺寸合适)
- [ ] 内联关键CSS(小于14KB)
- [ ] `<head>`中无阻塞渲染的JavaScript
- [ ] 字体不阻塞文本渲染(font-display: swap)
- [ ] LCP元素包含在初始HTML中(非JS渲染)

LCP element identification

LCP元素识别

javascript
// Find your LCP element
new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];
  console.log('LCP element:', lastEntry.element);
  console.log('LCP time:', lastEntry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });

javascript
// 查找你的LCP元素
new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];
  console.log('LCP element:', lastEntry.element);
  console.log('LCP time:', lastEntry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });

INP: Interaction to Next Paint

INP: Interaction to Next Paint(交互到下一次绘制)

INP measures responsiveness across ALL interactions (clicks, taps, key presses) during a page visit. It reports the worst interaction (at 98th percentile for high-traffic pages).
INP衡量页面访问期间所有交互(点击、轻触、按键)的响应速度。它报告最差的交互情况(高流量页面取98百分位数)。

INP breakdown

INP分解

Total INP = Input Delay + Processing Time + Presentation Delay
PhaseTargetOptimization
Input Delay< 50msReduce main thread blocking
Processing< 100msOptimize event handlers
Presentation< 50msMinimize rendering work
总INP = 输入延迟 + 处理时间 + 呈现延迟
阶段目标优化方案
输入延迟< 50毫秒减少主线程阻塞
处理时间< 100毫秒优化事件处理程序
呈现延迟< 50毫秒最小化渲染工作量

Common INP issues

常见INP问题

1. Long tasks blocking main thread
javascript
// ❌ Long synchronous task
function processLargeArray(items) {
  items.forEach(item => expensiveOperation(item));
}

// ✅ Break into chunks with yielding
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));
    
    // Yield to main thread
    await new Promise(r => setTimeout(r, 0));
    // Or use scheduler.yield() when available
  }
}
2. Heavy event handlers
javascript
// ❌ All work in handler
button.addEventListener('click', () => {
  // Heavy computation
  const result = calculateComplexThing();
  // DOM updates
  updateUI(result);
  // Analytics
  trackEvent('click');
});

// ✅ Prioritize visual feedback
button.addEventListener('click', () => {
  // Immediate visual feedback
  button.classList.add('loading');
  
  // Defer non-critical work
  requestAnimationFrame(() => {
    const result = calculateComplexThing();
    updateUI(result);
  });
  
  // Use requestIdleCallback for analytics
  requestIdleCallback(() => trackEvent('click'));
});
3. Third-party scripts
javascript
// ❌ Eagerly loaded, blocks interactions
<script src="https://heavy-widget.com/widget.js"></script>

// ✅ Lazy loaded on interaction or visibility
const loadWidget = () => {
  import('https://heavy-widget.com/widget.js')
    .then(widget => widget.init());
};
button.addEventListener('click', loadWidget, { once: true });
4. Excessive re-renders (React/Vue)
javascript
// ❌ Re-renders entire tree
function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <Counter count={count} />
      <ExpensiveComponent /> {/* Re-renders on every count change */}
    </div>
  );
}

// ✅ Memoized expensive components
const MemoizedExpensive = React.memo(ExpensiveComponent);

function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <Counter count={count} />
      <MemoizedExpensive />
    </div>
  );
}
1. 主线程被长任务阻塞
javascript
// ❌ 长同步任务
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. 繁重的事件处理程序
javascript
// ❌ 所有工作都在处理程序中完成
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. 第三方脚本
javascript
// ❌ 提前加载,阻塞交互
<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)
javascript
// ❌ 整棵树重渲染
function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <Counter count={count} />
      <ExpensiveComponent /> {/* 每次count变化时重渲染 */}
    </div>
  );
}

// ✅ 记忆化处理昂贵组件
const MemoizedExpensive = React.memo(ExpensiveComponent);

function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <Counter count={count} />
      <MemoizedExpensive />
    </div>
  );
}

INP optimization checklist

INP优化检查清单

markdown
- [ ] No tasks > 50ms on main thread
- [ ] Event handlers complete quickly (< 100ms)
- [ ] Visual feedback provided immediately
- [ ] Heavy work deferred with requestIdleCallback
- [ ] Third-party scripts don't block interactions
- [ ] Debounced input handlers where appropriate
- [ ] Web Workers for CPU-intensive operations
markdown
- [ ] 主线程无超过50毫秒的任务
- [ ] 事件处理程序快速完成(<100毫秒)
- [ ] 立即提供视觉反馈
- [ ] 使用requestIdleCallback延迟繁重工作
- [ ] 第三方脚本不阻塞交互
- [ ] 适当使用防抖输入处理程序
- [ ] 使用Web Workers处理CPU密集型操作

INP debugging

INP调试

javascript
// Identify slow interactions
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 200) {
      console.warn('Slow interaction:', {
        type: entry.name,
        duration: entry.duration,
        processingStart: entry.processingStart,
        processingEnd: entry.processingEnd,
        target: entry.target
      });
    }
  }
}).observe({ type: 'event', buffered: true, durationThreshold: 16 });

javascript
// 识别缓慢交互
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 200) {
      console.warn('Slow interaction:', {
        type: entry.name,
        duration: entry.duration,
        processingStart: entry.processingStart,
        processingEnd: entry.processingEnd,
        target: entry.target
      });
    }
  }
}).observe({ type: 'event', buffered: true, durationThreshold: 16 });

CLS: Cumulative Layout Shift

CLS: Cumulative Layout Shift(累积布局偏移)

CLS measures unexpected layout shifts. A shift occurs when a visible element changes position between frames without user interaction.
CLS Formula:
impact fraction × distance fraction
CLS衡量意外的布局偏移。当可见元素在无用户交互的情况下在帧之间改变位置时,就会发生偏移。
CLS计算公式:
影响占比 × 距离占比

Common CLS causes

常见CLS原因

1. Images without dimensions
html
<!-- ❌ Causes layout shift when loaded -->
<img src="photo.jpg" alt="Photo">

<!-- ✅ Space reserved -->
<img src="photo.jpg" alt="Photo" width="800" height="600">

<!-- ✅ Or use aspect-ratio -->
<img src="photo.jpg" alt="Photo" style="aspect-ratio: 4/3; width: 100%;">
2. Ads, embeds, and iframes
html
<!-- ❌ Unknown size until loaded -->
<iframe src="https://ad-network.com/ad"></iframe>

<!-- ✅ Reserve space with min-height -->
<div style="min-height: 250px;">
  <iframe src="https://ad-network.com/ad" height="250"></iframe>
</div>

<!-- ✅ Or use aspect-ratio container -->
<div style="aspect-ratio: 16/9;">
  <iframe src="https://youtube.com/embed/..." 
          style="width: 100%; height: 100%;"></iframe>
</div>
3. Dynamically injected content
javascript
// ❌ Inserts content above viewport
notifications.prepend(newNotification);

// ✅ Insert below viewport or use transform
const insertBelow = viewport.bottom < newNotification.top;
if (insertBelow) {
  notifications.prepend(newNotification);
} else {
  // Animate in without shifting
  newNotification.style.transform = 'translateY(-100%)';
  notifications.prepend(newNotification);
  requestAnimationFrame(() => {
    newNotification.style.transform = '';
  });
}
4. Web fonts causing FOUT
css
/* ❌ Font swap shifts text */
@font-face {
  font-family: 'Custom';
  src: url('custom.woff2') format('woff2');
}

/* ✅ Optional font (no shift if slow) */
@font-face {
  font-family: 'Custom';
  src: url('custom.woff2') format('woff2');
  font-display: optional;
}

/* ✅ Or match fallback metrics */
@font-face {
  font-family: 'Custom';
  src: url('custom.woff2') format('woff2');
  font-display: swap;
  size-adjust: 105%; /* Match fallback size */
  ascent-override: 95%;
  descent-override: 20%;
}
5. Animations triggering layout
css
/* ❌ Animates layout properties */
.animate {
  transition: height 0.3s, width 0.3s;
}

/* ✅ Use transform instead */
.animate {
  transition: transform 0.3s;
}
.animate.expanded {
  transform: scale(1.2);
}
1. 未指定尺寸的图片
html
<!-- ❌ 加载时导致布局偏移 -->
<img src="photo.jpg" alt="Photo">

<!-- ✅ 预留空间 -->
<img src="photo.jpg" alt="Photo" width="800" height="600">

<!-- ✅ 或使用aspect-ratio -->
<img src="photo.jpg" alt="Photo" style="aspect-ratio: 4/3; width: 100%;">
2. 广告、嵌入内容和iframe
html
<!-- ❌ 加载前尺寸未知 -->
<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>

<!-- ✅ 或使用aspect-ratio容器 -->
<div style="aspect-ratio: 16/9;">
  <iframe src="https://youtube.com/embed/..." 
          style="width: 100%; height: 100%;"></iframe>
</div>
3. 动态注入的内容
javascript
// ❌ 在视口上方插入内容
notifications.prepend(newNotification);

// ✅ 在视口下方插入或使用transform
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(无样式文本闪烁)
css
/* ❌ 字体替换导致文本偏移 */
@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. 触发布局的动画
css
/* ❌ 动画布局属性 */
.animate {
  transition: height 0.3s, width 0.3s;
}

/* ✅ 使用transform替代 */
.animate {
  transition: transform 0.3s;
}
.animate.expanded {
  transform: scale(1.2);
}

CLS optimization checklist

CLS优化检查清单

markdown
- [ ] All images have width/height or aspect-ratio
- [ ] All videos/embeds have reserved space
- [ ] Ads have min-height containers
- [ ] Fonts use font-display: optional or matched metrics
- [ ] Dynamic content inserted below viewport
- [ ] Animations use transform/opacity only
- [ ] No content injected above existing content
markdown
- [ ] 所有图片都指定width/height或aspect-ratio
- [ ] 所有视频/嵌入内容都预留空间
- [ ] 广告使用min-height容器
- [ ] 字体使用font-display: optional或匹配指标
- [ ] 动态内容插入到视口下方
- [ ] 动画仅使用transform/opacity
- [ ] 不在现有内容上方注入新内容

CLS debugging

CLS调试

javascript
// Track layout shifts
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      console.log('Layout shift:', entry.value);
      entry.sources?.forEach(source => {
        console.log('  Shifted element:', source.node);
        console.log('  Previous rect:', source.previousRect);
        console.log('  Current rect:', source.currentRect);
      });
    }
  }
}).observe({ type: 'layout-shift', buffered: true });

javascript
// 跟踪布局偏移
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      console.log('Layout shift:', entry.value);
      entry.sources?.forEach(source => {
        console.log('  偏移元素:', source.node);
        console.log('  之前的矩形:', source.previousRect);
        console.log('  当前的矩形:', source.currentRect);
      });
    }
  }
}).observe({ type: 'layout-shift', buffered: true });

Measurement tools

测量工具

Lab testing

实验室测试

  • Chrome DevTools → Performance panel, Lighthouse
  • WebPageTest → Detailed waterfall, filmstrip
  • Lighthouse CLI
    npx lighthouse <url>
  • Chrome DevTools → 性能面板、Lighthouse
  • WebPageTest → 详细瀑布图、胶片视图
  • Lighthouse CLI
    npx lighthouse <url>

Field data (real users)

真实用户数据(字段数据)

  • Chrome User Experience Report (CrUX) → BigQuery or API
  • Search Console → Core Web Vitals report
  • web-vitals library → Send to your analytics
javascript
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);

  • Chrome用户体验报告(CrUX) → BigQuery或API
  • 搜索控制台 → 核心网页指标报告
  • web-vitals库 → 发送到你的分析平台
javascript
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);

Framework quick fixes

框架快速修复

Next.js

Next.js

jsx
// LCP: Use next/image with priority
import Image from 'next/image';
<Image src="/hero.jpg" priority fill alt="Hero" />

// INP: Use dynamic imports
const HeavyComponent = dynamic(() => import('./Heavy'), { ssr: false });

// CLS: Image component handles dimensions automatically
jsx
// LCP:使用next/image并设置priority
import Image from 'next/image';
<Image src="/hero.jpg" priority fill alt="Hero" />

// INP:使用动态导入
const HeavyComponent = dynamic(() => import('./Heavy'), { ssr: false });

// CLS:Image组件自动处理尺寸

React

React

jsx
// LCP: Preload in head
<link rel="preload" href="/hero.jpg" as="image" fetchpriority="high" />

// INP: Memoize and useTransition
const [isPending, startTransition] = useTransition();
startTransition(() => setExpensiveState(newValue));

// CLS: Always specify dimensions in img tags
jsx
// LCP:在head中预加载
<link rel="preload" href="/hero.jpg" as="image" fetchpriority="high" />

// INP:使用memo和useTransition
const [isPending, startTransition] = useTransition();
startTransition(() => setExpensiveState(newValue));

// CLS:始终在img标签中指定尺寸

Vue/Nuxt

Vue/Nuxt

vue
<!-- LCP: Use nuxt/image with preload -->
<NuxtImg src="/hero.jpg" preload loading="eager" />

<!-- INP: Use async components -->
<component :is="() => import('./Heavy.vue')" />

<!-- CLS: Use aspect-ratio CSS -->
<img :style="{ aspectRatio: '16/9' }" />
vue
<!-- LCP:使用nuxt/image并设置preload -->
<NuxtImg src="/hero.jpg" preload loading="eager" />

<!-- INP:使用异步组件 -->
<component :is="() => import('./Heavy.vue')" />

<!-- CLS:使用aspect-ratio CSS -->
<img :style="{ aspectRatio: '16/9' }" />

References

参考资料