mcpField
C
core-web-vitals

core-web-vitals

github.com/tech-leads-club开发7.1k259 次安装2 天前更新
代码仓库

关于 core-web-vitals

面向开发者的 Core Web Vitals 优化技能,涵盖页面加载、交互响应和视觉稳定性,并提供诊断方法、检查清单与优化建议。

主要功能

  1. 01LCP 优化指导
  2. 02INP 响应优化指导
  3. 03CLS 稳定性优化指导
  4. 04使用 PerformanceObserver 调试
  5. 05实验室与真实用户测量参考

适用场景

  1. 01诊断最大内容渲染缓慢
  2. 02减少点击和按键后的延迟
  3. 03避免页面意外布局偏移

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/tech-leads-club/agent-skills --skill core-web-vitals

安装后在 Claude Code 中输入/core-web-vitals确认已加载。

附带文件references/LCP.md

详细介绍

Core Web Vitals 优化

针对影响 Google 搜索排名和用户体验的三项 Core Web Vitals 指标进行优化。

三项指标

指标衡量内容良好有待改进较差
LCP加载≤ 2.5s2.5s – 4s> 4s
INP交互性≤ 200ms200ms – 500ms> 500ms
CLS视觉稳定性≤ 0.10.1 – 0.25> 0.25

Google 按第 75 百分位进行衡量——75% 的页面访问必须达到“良好”阈值。


LCP:最大内容绘制

LCP 衡量最大的可见内容元素完成渲染的时间。通常包括:

  • 主视觉图片或视频
  • 大段文本
  • 背景图片
  • <svg> 元素
常见的 LCP 问题

1. 服务器响应缓慢(TTFB > 800ms)

Fix: CDN, caching, optimized backend, edge rendering

2. 阻塞渲染的资源


<link rel="stylesheet" href="/all-styles.css" />


<style>
  /* Critical above-fold CSS */
</style>
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />

3. 资源加载时间过长


<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. 客户端渲染延迟

// ❌ 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 } }
}
LCP 优化清单
- [ ] 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)
识别 LCP 元素
// 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 })

INP:下次绘制交互

INP 衡量用户访问页面期间所有交互(点击、轻触、按键)的响应速度。它报告最差交互的情况(高流量页面取第 98 百分位)。

INP 分解

INP 总计 = 输入延迟 + 处理时间 + 呈现延迟

阶段目标优化方式
输入延迟< 50ms减少主线程阻塞
处理< 100ms优化事件处理程序
呈现< 50ms尽量减少渲染工作
常见的 INP 问题

1. 长任务阻塞主线程

// ❌ 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. 事件处理程序负载过重

// ❌ 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. 第三方脚本

// ❌ 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. 过度重新渲染(React/Vue)

// ❌ 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>
  )
}
INP 优化清单
- [ ] 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
调试 INP
// 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 })

CLS:累积布局偏移

CLS 衡量意外的布局偏移。当可见元素在没有用户交互的情况下于不同帧之间改变位置时,就会发生偏移。

CLS 公式: 影响比例 × 距离比例

CLS 的常见原因

1. 图片未设置尺寸


<img src="photo.jpg" alt="Photo" />


<img src="photo.jpg" alt="Photo" width="800" height="600" />


<img src="photo.jpg" alt="Photo" style="aspect-ratio: 4/3; width: 100%;" />

2. 广告、嵌入内容和 iframe


<iframe src="https://ad-network.com/ad"></iframe>


<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. 动态注入的内容

// ❌ 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 字体导致 FOUT

/* ❌ 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. 触发布局的动画

/* ❌ 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);
}
CLS 优化清单
- [ ] 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
调试 CLS
// 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 })

测量工具

实验室测试
  • Chrome DevTools → Performance 面板、Lighthouse
  • WebPageTest → 详细的瀑布图、胶片条
  • Lighthouse CLI → npx lighthouse <url>
实测数据(真实用户)
  • Chrome User Experience Report (CrUX) → BigQuery 或 API
  • Search Console → Core Web Vitals 报告
  • web-vitals library → 发送到你的分析工具
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: 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
React
// 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
Vue/Nuxt

<NuxtImg src="/hero.jpg" preload loading="eager" />


<component :is="() => import('./Heavy.vue')" />


<img :style="{ aspectRatio: '16/9' }" />

参考资料

评论

支持 Markdown