关于 cwv-audit
cwv-audit 是用于理解和改进 Core Web Vitals 的参考技能,面向开发者和网站负责人,涵盖指标说明、问题排查、测量方法、CrUX 查询及监控配置。
主要功能
- 01LCP、INP 和 CLS 阈值
- 02常见原因与优化指导
- 03真实用户及实验室测量参考
- 04CrUX BigQuery 查询示例
- 05web-vitals 与监控配置指导
适用场景
- 01诊断加载缓慢或页面不稳定
- 02使用 CrUX 数据比较网站指标
- 03规划 Core Web Vitals 监控
- 04集成 web-vitals 测量
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cognyai/ claude-code-marketing-skills --skill cwv-audit安装后在 Claude Code 中输入/cwv-audit确认已加载。
详细介绍
Core Web Vitals 参考
关于 Core Web Vitals(LCP、INP、CLS)的完整技术参考:阈值、常见原因、修复方法、测量工具、CrUX BigQuery 查询、web-vitals 库设置,以及对 SEO 排名的影响。
完整文档:https://cogny.com/docs/core-web-vitals
用法
/cwv-audit # Full CWV overview and audit guidance
/cwv-audit LCP # Deep-dive into LCP causes and fixes
/cwv-audit INP # Deep-dive into INP causes and fixes
/cwv-audit CLS # Deep-dive into CLS causes and fixes
/cwv-audit CrUX competitor # CrUX competitor comparison queries
/cwv-audit monitoring # GA4 + BigQuery monitoring setup
/cwv-audit web-vitals # web-vitals library integration guide
说明
你是一位 Core Web Vitals 性能专家。使用此参考帮助用户了解 CWV 指标、诊断性能问题、编写 CrUX BigQuery 查询、设置监控,并优化网站以改善用户体验和 SEO 排名。
用户提出问题时,找到下方相关部分,并提供准确且可执行的回答。如果有可用的 Search Console 或 GA4 MCP 工具,请使用它们获取用户网站的实际数据。
如果用户提供了具体指标或主题作为参数,请重点关注该领域。否则,请概述全部三项 Web Vitals 指标及其当前阈值。
概览
Core Web Vitals(CWV)是 Google 在搜索中用作排名信号的三项以用户为中心的性能指标。它们衡量加载性能、交互性和视觉稳定性。每个页面都按真实用户现场数据的第 75 百分位数进行评估。
| 指标 | 全称 | 衡量内容 | 良好 | 需要改进 | 差 |
|---|---|---|---|---|---|
| LCP | Largest Contentful Paint | 加载性能 | ≤ 2.5 s | ≤ 4.0 s | > 4.0 s |
| INP | Interaction to Next Paint | 交互性 | ≤ 200 ms | ≤ 500 ms | > 500 ms |
| CLS | Cumulative Layout Shift | 视觉稳定性 | ≤ 0.1 | ≤ 0.25 | > 0.25 |
当某页面在第 75 百分位数下的三项指标都达到“良好”阈值时,该页面即“通过” Core Web Vitals。
LCP — Largest Contentful Paint
衡量内容
LCP 报告视口中最大图像、视频或文本块的渲染时间,相对于页面首次开始加载的时间计算。
阈值
- 良好: ≤ 2.5 秒
- 需要改进: > 2.5 s 且 ≤ 4.0 s
- 差: > 4.0 秒
LCP 候选元素
<img>元素(包括<picture>内的元素)<svg>内的<image>元素<video>元素(海报图像或首次显示的帧)- 通过
url()加载了background-image的元素(不包括 CSS 渐变) - 包含文本节点或行内级文本子元素的块级元素
页面加载时,最大的元素可能会发生变化。用户交互前的最后一条记录即为 LCP 值。
常见原因和修复方法
1. 服务器响应时间(TTFB)慢
- 使用 CDN 从边缘节点提供内容
- 在边缘缓存 HTML(stale-while-revalidate)
- 预连接到所需来源:
<link rel="preconnect" href="https://cdn.example.com"> - 使用 103 Early Hints
2. 阻塞渲染的资源
- 内联关键 CSS,延迟加载非关键样式表
- 延迟或异步加载非关键 JavaScript:
<script defer src="app.js"> - 尽量减小 CSS 体积,避免在 CSS 中使用
@import
3. 资源加载时间长
- 预加载 LCP 图像:
<link rel="preload" as="image" href="hero.webp"> - 使用现代格式(WebP、AVIF),并通过
<picture>提供备用格式 - 为 LCP
<img>设置fetchpriority="high" - 使用带有
srcset和sizes的响应式图像
4. 客户端渲染延迟
- 对 LCP 内容使用 SSR 或 SSG
- 预渲染关键路由
- 减小 JavaScript 包体积
5. Web 字体阻塞 LCP 文本
- 使用
font-display: swap或font-display: optional - 预加载主要字体文件
- 对字体进行子集化,仅包含所需字符
INP — Interaction to Next Paint
衡量内容
INP 衡量整个页面生命周期中所有点击、轻触和键盘交互的延迟,并报告代表最差交互情况的单一值。它于 2024 年 3 月取代 FID,成为 Core Web Vital 指标。
阈值
- 良好: ≤ 200 毫秒
- 需要改进: > 200 ms 且 ≤ 500 ms
- 差: > 500 毫秒
与 FID 的区别
| FID | INP | |
|---|---|---|
| 范围 | 仅首次交互 | 所有交互 |
| 衡量内容 | 仅输入延迟 | 完整延迟:输入延迟 + 处理时间 + 呈现延迟 |
| 状态 | 已弃用(2024 年 3 月) | 官方核心网页指标 |
交互延迟的三个阶段
- 输入延迟 — 从用户操作到事件处理程序开始运行的时间
- 处理时间 — 运行事件处理程序所花费的时间
- 呈现延迟 — 从处理程序执行完毕到下一次绘制的时间
常见原因及解决方案
1. 长任务阻塞主线程
- 使用
scheduler.yield()或setTimeout(0)拆分长任务 - 使用
requestIdleCallback处理非紧急工作 - 将繁重的计算移至 Web Workers
2. JavaScript 执行负担过重
- 使用动态
import()拆分代码 - 在构建时对未使用的导出进行摇树优化
- 延迟加载非关键脚本和第三方标签
3. 代价高昂的事件处理程序
- 对频繁触发的处理程序进行防抖或节流
- 避免在处理程序中强制执行同步布局
- 对屏幕外内容使用
content-visibility: auto
4. DOM 过大且渲染代价高昂
- 对长列表进行虚拟化(react-window、@tanstack/virtual)
- 使用 CSS
contain: layout style paint - 减少 DOM 深度和元素数量
5. SPA 框架中过度重新渲染
- 对组件进行记忆化处理(
React.memo、useMemo) - 对非紧急状态更新使用
startTransition
CLS — 累积布局偏移
衡量内容
CLS 衡量页面生命周期内意外布局偏移分数的总和。当可见元素的位置在没有用户交互触发的情况下发生变化时,就会出现布局偏移。
阈值
- 良好: ≤ 0.1
- 需要改进: > 0.1 且 ≤ 0.25
- 较差: > 0.25
会话窗口计算
- 将偏移分组到会话窗口中(最长 5 秒,间隔 1 秒则结束窗口)
- CLS = 最高会话窗口分数
- 每次偏移分数 = 影响比例 x 距离比例
哪些情况算作布局偏移
- 位置变化不是由用户输入触发的
- CSS
transform动画不计入 - 用户交互后 500ms 内发生的偏移会被排除
常见原因及解决方案
1. 未指定尺寸的图片/视频
<img src="hero.jpg" alt="Hero" width="800" height="400">
<style>.hero-img { aspect-ratio: 16 / 9; width: 100%; }</style>
2. 动态插入的内容
.ad-slot { min-height: 250px; min-width: 300px; contain: layout style paint; }
3. Web 字体导致 FOIT/FOUT
@font-face {
font-family: 'Custom Font';
src: url('custom.woff2') format('woff2');
font-display: optional; /* eliminates layout shift entirely */
}
4. 未预留空间的广告/嵌入内容
.embed-container {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
height: 0; overflow: hidden;
}
.embed-container iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
5. 延迟加载的横幅/ Cookie 栏
- 使用
position: fixed或sticky,使其覆盖内容而不引发布局偏移
测量工具
实际用户数据
| 工具 | 说明 |
|---|---|
| CrUX | 通过 BigQuery、API 和 PageSpeed Insights 提供 28 天滚动数据 |
| Search Console CWV report | 按良好/需要改进/较差对 URL 进行分组 |
| PageSpeed Insights (field section) | 每个 URL/来源的 CrUX 第 75 百分位值 |
| web-vitals library | 在真实浏览器中测量 CWV,并发送到任意端点 |
实验室数据(模拟)
| 工具 | 说明 |
|---|---|
| Lighthouse | 模拟 LCP 和 CLS(不包括 INP) |
| Chrome DevTools Performance panel | 显示所有三个指标的完整火焰图 |
| WebPageTest | 提供带胶片条和瀑布图的多步骤测试 |
重要: 实验室工具无法完整测量 INP。务必使用实际用户数据进行验证。
JavaScript API
// Observe LCP
new PerformanceObserver((list) => {
const entries = list.getEntries();
console.log('LCP:', entries[entries.length - 1].startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });
// Observe CLS
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) console.log('Shift:', entry.value);
}
}).observe({ type: 'layout-shift', buffered: true });
// Observe INP (Event Timing API)
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.interactionId) console.log('Interaction:', entry.duration, 'ms');
}
}).observe({ type: 'event', buffered: true, durationThreshold: 16 });
web-vitals 库
安装
npm install web-vitals
基本用法
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);
发送到 GA4
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToGA4(metric) {
gtag('event', metric.name, {
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
event_category: 'Web Vitals',
event_label: metric.id,
non_interaction: true,
});
}
onLCP(sendToGA4);
onINP(sendToGA4);
onCLS(sendToGA4);
发送到自定义端点
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name, value: metric.value, rating: metric.rating,
delta: metric.delta, id: metric.id, page: window.location.pathname,
});
if (navigator.sendBeacon) navigator.sendBeacon('/api/vitals', body);
else fetch('/api/vitals', { body, method: 'POST', keepalive: true });
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
归因构建
import { onLCP, onINP, onCLS } from 'web-vitals/attribution';
onLCP((metric) => {
console.log('LCP element:', metric.attribution.element);
console.log('TTFB:', metric.attribution.timeToFirstByte);
console.log('Resource load delay:', metric.attribution.resourceLoadDelay);
console.log('Element render delay:', metric.attribution.elementRenderDelay);
});
onINP((metric) => {
console.log('Target:', metric.attribution.interactionTarget);
console.log('Input delay:', metric.attribution.inputDelay);
console.log('Processing:', metric.attribution.processingDuration);
console.log('Presentation delay:', metric.attribution.presentationDelay);
});
onCLS((metric) => {
console.log('Largest shift target:', metric.attribution.largestShiftTarget);
console.log('Largest shift value:', metric.attribution.largestShiftValue);
console.log('Load state:', metric.attribution.loadState);
});
CrUX BigQuery 查询
按来源查看 CWV 分数
SELECT
origin,
form_factor.name AS device,
ROUND(SAFE_DIVIDE(
SUM(IF(lcp.start < 2500, lcp.density, 0)), SUM(lcp.density)
), 4) AS lcp_good_pct,
ROUND(SAFE_DIVIDE(
SUM(IF(inp.start < 200, inp.density, 0)), SUM(inp.density)
), 4) AS inp_good_pct,
ROUND(SAFE_DIVIDE(
SUM(IF(cls.start < 0.1, cls.density, 0)), SUM(cls.density)
), 4) AS cls_good_pct
FROM `chrome-ux-report.all.202501`,
UNNEST(largest_contentful_paint.histogram.bin) AS lcp,
UNNEST(interaction_to_next_paint.histogram.bin) AS inp,
UNNEST(cumulative_layout_shift.histogram.bin) AS cls
WHERE origin = 'https://www.example.com'
GROUP BY origin, device
ORDER BY device
按页面类型查看 CWV(URL 级别)
SELECT
url,
form_factor.name AS device,
ROUND(p75_lcp / 1000, 2) AS p75_lcp_sec,
p75_inp AS p75_inp_ms,
ROUND(p75_cls, 3) AS p75_cls
FROM `chrome-ux-report.materialized.device_summary`
WHERE origin = 'https://www.example.com'
AND url LIKE '%/product/%'
AND date = (SELECT MAX(date) FROM `chrome-ux-report.materialized.device_summary`)
ORDER BY p75_lcp DESC
LIMIT 50
CWV 随时间变化的趋势
SELECT
yyyymm,
ROUND(SAFE_DIVIDE(
SUM(IF(lcp.start < 2500, lcp.density, 0)), SUM(lcp.density)
), 4) AS lcp_good_pct,
ROUND(SAFE_DIVIDE(
SUM(IF(inp.start < 200, inp.density, 0)), SUM(inp.density)
), 4) AS inp_good_pct,
ROUND(SAFE_DIVIDE(
SUM(IF(cls.start < 0.1, cls.density, 0)), SUM(cls.density)
), 4) AS cls_good_pct
FROM `chrome-ux-report.all.*`,
UNNEST(largest_contentful_paint.histogram.bin) AS lcp,
UNNEST(interaction_to_next_paint.histogram.bin) AS inp,
UNNEST(cumulative_layout_shift.histogram.bin) AS cls
WHERE origin = 'https://www.example.com'
AND _TABLE_SUFFIX BETWEEN '202401' AND '202501'
AND form_factor.name = 'phone'
GROUP BY yyyymm
ORDER BY yyyymm
竞争对手比较
WITH origins AS (
SELECT origin FROM UNNEST([
'https://www.yoursite.com',
'https://www.competitor-a.com',
'https://www.competitor-b.com'
]) AS origin
)
SELECT
o.origin, form_factor.name AS device,
ROUND(SAFE_DIVIDE(
SUM(IF(lcp.start < 2500, lcp.density, 0)), SUM(lcp.density)
), 4) AS lcp_good_pct,
ROUND(SAFE_DIVIDE(
SUM(IF(inp.start < 200, inp.density, 0)), SUM(inp.density)
), 4) AS inp_good_pct,
ROUND(SAFE_DIVIDE(
SUM(IF(cls.start < 0.1, cls.density, 0)), SUM(cls.density)
), 4) AS cls_good_pct
FROM origins o
INNER JOIN `chrome-ux-report.all.202501` crux ON crux.origin = o.origin,
UNNEST(largest_contentful_paint.histogram.bin) AS lcp,
UNNEST(interaction_to_next_paint.histogram.bin) AS inp,
UNNEST(cumulative_layout_shift.histogram.bin) AS cls
WHERE form_factor.name = 'phone'
GROUP BY o.origin, device
ORDER BY o.origin, device
SEO 影响
页面体验排名信号
CWV 是 Google 页面体验信号的一部分,与 HTTPS、移动设备友好性和无侵入式插页广告并列。
- CWV 起到决胜作用——当相关性和质量相同时,CWV 较好的页面可能排名更高
- CWV 不是主导信号——它们不会凌驾于优质内容或反向链接之上
- Google 使用 CrUX 的现场数据,以第 75 百分位为准(而非实验室评分)
- 移动设备 CWV 会影响移动端排名;桌面端 CWV 会影响桌面端排名
优化优先级
优先针对移动设备进行优化——由于处理能力较低、网络速度较慢,移动设备通常限制最严格。
优化模式
图片优化
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Hero" width="1200" height="600"
fetchpriority="high" decoding="async">
</picture>
<img src="hero-800.webp" alt="Hero"
srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w"
sizes="(max-width: 600px) 400px, (max-width: 900px) 800px, 1200px"
loading="lazy" decoding="async" width="1200" height="600">
**关键规则:**切勿对 LCP 图片启用延迟加载。应在该图片上设置 fetchpriority="high"。
字体优化
<link rel="preload" as="font" type="font/woff2" href="/fonts/main.woff2" crossorigin>
@font-face {
font-family: 'Main Font';
src: url('/fonts/main.woff2') format('woff2');
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}
JavaScript 优化
<script defer src="/js/app.js"></script>
<script async src="https://analytics.example.com/tag.js"></script>
// Dynamic import for code splitting
const Chart = await import('./components/Chart.js');
// Non-urgent work
requestIdleCallback(() => { initializeAnalytics(); });
CSS 优化
/* Limit recalculation scope */
.card { contain: layout style paint; }
/* Skip rendering off-screen content */
.below-fold { content-visibility: auto; contain-intrinsic-size: 0 500px; }
第三方脚本管理
- 在
window.load之后加载非关键第三方脚本 - 对所需的第三方来源使用
<link rel="preconnect"> - 尽可能使用服务器端标记
- 使用 Chrome DevTools 的 Coverage 标签页审查第三方影响
GA4 + BigQuery 监控设置
将 CWV 发送到 GA4
import { onLCP, onINP, onCLS } from 'web-vitals/attribution';
function sendToGA4(metric) {
gtag('event', metric.name, {
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
event_category: 'Web Vitals',
event_label: metric.id,
metric_rating: metric.rating,
debug_target: metric.attribution?.interactionTarget
|| metric.attribution?.element
|| metric.attribution?.largestShiftTarget
|| '(not set)',
non_interaction: true,
});
}
onLCP(sendToGA4);
onINP(sendToGA4);
onCLS(sendToGA4);
从 GA4 BigQuery 导出数据中查询 CWV
WITH vitals AS (
SELECT
DATE(TIMESTAMP_MICROS(event_timestamp)) AS date,
event_name AS metric_name,
(SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'value') AS metric_value,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'metric_rating') AS rating,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'debug_target') AS debug_target,
device.category AS device_type
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 28 DAY))
AND FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
AND event_name IN ('LCP', 'INP', 'CLS')
)
SELECT
date, metric_name, device_type,
COUNT(*) AS samples,
APPROX_QUANTILES(metric_value, 100)[OFFSET(75)] AS p75,
COUNTIF(rating = 'good') / COUNT(*) AS good_pct,
COUNTIF(rating = 'poor') / COUNT(*) AS poor_pct
FROM vitals
GROUP BY date, metric_name, device_type
ORDER BY date DESC, metric_name, device_type
找出表现最差的页面
WITH page_vitals AS (
SELECT
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location') AS page,
event_name AS metric_name,
(SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'value') AS metric_value,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'metric_rating') AS rating,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'debug_target') AS debug_target
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 28 DAY))
AND FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
AND event_name IN ('LCP', 'INP', 'CLS')
)
SELECT
REGEXP_EXTRACT(page, r'^https?://[^/]+(/.*)') AS path,
metric_name,
COUNT(*) AS samples,
APPROX_QUANTILES(metric_value, 100)[OFFSET(75)] AS p75,
COUNTIF(rating = 'poor') / COUNT(*) AS poor_pct,
APPROX_TOP_COUNT(debug_target, 3) AS top_debug_targets
FROM page_vitals
WHERE page IS NOT NULL
GROUP BY path, metric_name
HAVING samples >= 50
ORDER BY poor_pct DESC
LIMIT 50
资源
- web.dev Core Web Vitals: https://web.dev/articles/vitals
- CrUX 文档: https://developer.chrome.com/docs/crux/
- web-vitals 库: https://github.com/GoogleChrome/web-vitals
- CrUX BigQuery 操作指南: https://developer.chrome.com/docs/crux/bigquery/
- PageSpeed Insights: https://pagespeed.web.dev/
- Cogny 完整文档: https://cogny.com/docs/core-web-vitals