重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
core-web-vitals by tech-leads-club/agent-skills
npx skills add https://github.com/tech-leads-club/agent-skills --skill core-web-vitals针对影响 Google 搜索排名和用户体验的三个核心 Web 指标进行针对性优化。
| 指标 | 衡量内容 | 良好 | 待改进 | 差 |
|---|---|---|---|---|
| LCP | 加载 | ≤ 2.5秒 | 2.5秒 – 4秒 | > 4秒 |
| INP | 交互性 | ≤ 200毫秒 | 200毫秒 – 500毫秒 | > 500毫秒 |
| CLS | 视觉稳定性 | ≤ 0.1 | 0.1 – 0.25 | > 0.25 |
Google 在 第 75 百分位 进行衡量 —— 75% 的页面访问必须达到"良好"阈值。
LCP 衡量最大可见内容元素何时渲染完成。通常这是:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
<svg> 元素1. 服务器响应慢 (TTFB > 800ms)
修复:CDN、缓存、优化后端、边缘渲染
2. 渲染阻塞资源
<!-- ❌ 阻塞渲染 -->
<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. 资源加载时间慢
<!-- ❌ 无提示,发现较晚 -->
<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 之后加载
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 } }
}
- [ ] TTFB < 800ms (使用 CDN、边缘缓存)
- [ ] LCP 图片使用 fetchpriority="high" 预加载
- [ ] LCP 图片已优化 (WebP/AVIF,尺寸正确)
- [ ] 关键 CSS 内联 (< 14KB)
- [ ] `<head>` 中没有渲染阻塞的 JavaScript
- [ ] 字体不阻塞文本渲染 (font-display: swap)
- [ ] LCP 元素在初始 HTML 中 (非 JS 渲染)
// 查找你的 LCP 元素
new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
console.log('LCP 元素:', lastEntry.element)
console.log('LCP 时间:', lastEntry.startTime)
}).observe({ type: 'largest-contentful-paint', buffered: true })
INP 衡量页面访问期间所有交互(点击、轻触、按键)的响应性。它报告最差的交互(对于高流量页面,报告第 98 百分位)。
总 INP = 输入延迟 + 处理时间 + 呈现延迟
| 阶段 | 目标 | 优化 |
|---|---|---|
| 输入延迟 | < 50ms | 减少主线程阻塞 |
| 处理 | < 100ms | 优化事件处理程序 |
| 呈现 | < 50ms | 最小化渲染工作 |
1. 长任务阻塞主线程
// ❌ 长同步任务
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. 繁重的事件处理程序
// ❌ 所有工作都在处理程序中
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. 第三方脚本
// ❌ 急切加载,阻塞交互
;<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)
// ❌ 重新渲染整个树
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>
)
}
- [ ] 主线程上没有任务 > 50ms
- [ ] 事件处理程序快速完成 (< 100ms)
- [ ] 立即提供视觉反馈
- [ ] 繁重工作使用 requestIdleCallback 延迟
- [ ] 第三方脚本不阻塞交互
- [ ] 在适当情况下对输入处理程序进行防抖
- [ ] 对 CPU 密集型操作使用 Web Workers
// 识别慢交互
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 200) {
console.warn('慢交互:', {
type: entry.name,
duration: entry.duration,
processingStart: entry.processingStart,
processingEnd: entry.processingEnd,
target: entry.target,
})
}
}
}).observe({ type: 'event', buffered: true, durationThreshold: 16 })
CLS 衡量意外的布局偏移。当可见元素在没有用户交互的情况下在帧之间改变位置时,就会发生偏移。
CLS 公式: 影响分数 × 距离分数
1. 没有尺寸的图片
<!-- ❌ 加载时导致布局偏移 -->
<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
<!-- ❌ 加载前大小未知 -->
<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. 动态注入的内容
// ❌ 在视口上方插入内容
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. Web 字体导致 FOUT
/* ❌ 字体交换导致文本偏移 */
@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. 触发布局的动画
/* ❌ 动画化布局属性 */
.animate {
transition:
height 0.3s,
width 0.3s;
}
/* ✅ 改用 transform */
.animate {
transition: transform 0.3s;
}
.animate.expanded {
transform: scale(1.2);
}
- [ ] 所有图片都有 width/height 或 aspect-ratio
- [ ] 所有视频/嵌入内容都预留了空间
- [ ] 广告有 min-height 容器
- [ ] 字体使用 font-display: optional 或匹配度量
- [ ] 动态内容插入到视口下方
- [ ] 动画仅使用 transform/opacity
- [ ] 没有内容注入到现有内容上方
// 跟踪布局偏移
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
console.log('布局偏移:', entry.value)
entry.sources?.forEach((source) => {
console.log(' 偏移元素:', source.node)
console.log(' 先前矩形:', source.previousRect)
console.log(' 当前矩形:', source.currentRect)
})
}
}
}).observe({ type: 'layout-shift', buffered: true })
npx lighthouse <url>Chrome 用户体验报告 (CrUX) → BigQuery 或 API
Search Console → 核心 Web 指标报告
web-vitals 库 → 发送到你的分析系统
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)
// 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 组件自动处理尺寸
// LCP: 在 head 中预加载
;<link rel="preload" href="/hero.jpg" as="image" fetchpriority="high" />
// INP: 记忆化并使用 useTransition
const [isPending, startTransition] = useTransition()
startTransition(() => setExpensiveState(newValue))
// CLS: 始终在 img 标签中指定尺寸
<!-- LCP: 使用 nuxt/image 并预加载 -->
<NuxtImg src="/hero.jpg" preload loading="eager" />
<!-- INP: 使用异步组件 -->
<component :is="() => import('./Heavy.vue')" />
<!-- CLS: 使用 aspect-ratio CSS -->
<img :style="{ aspectRatio: '16/9' }" />
每周安装数
51
代码仓库
GitHub Stars
1.8K
首次出现
2026年2月5日
安全审计
已安装于
cursor49
opencode49
gemini-cli48
codex48
github-copilot48
amp46
Targeted optimization for the three Core Web Vitals metrics that affect Google Search ranking and user experience.
| Metric | Measures | Good | Needs work | Poor |
|---|---|---|---|---|
| LCP | Loading | ≤ 2.5s | 2.5s – 4s | > 4s |
| INP | Interactivity | ≤ 200ms | 200ms – 500ms | > 500ms |
| CLS | Visual Stability | ≤ 0.1 | 0.1 – 0.25 | > 0.25 |
Google measures at the 75th percentile — 75% of page visits must meet "Good" thresholds.
LCP measures when the largest visible content element renders. Usually this is:
<svg> element1. Slow server response (TTFB > 800ms)
Fix: CDN, caching, optimized backend, edge rendering
2. Render-blocking resources
<!-- ❌ 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
<!-- ❌ 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
// ❌ 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 } }
}
- [ ] 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)
// 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 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).
Total INP = Input Delay + Processing Time + Presentation Delay
| Phase | Target | Optimization |
|---|---|---|
| Input Delay | < 50ms | Reduce main thread blocking |
| Processing | < 100ms | Optimize event handlers |
| Presentation | < 50ms | Minimize rendering work |
1. Long tasks blocking main thread
// ❌ 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
// ❌ 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
// ❌ 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)
// ❌ 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>
)
}
- [ ] 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
// 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 measures unexpected layout shifts. A shift occurs when a visible element changes position between frames without user interaction.
CLS Formula: impact fraction × distance fraction
1. Images without dimensions
<!-- ❌ 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
<!-- ❌ 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
// ❌ 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
/* ❌ 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
/* ❌ 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);
}
- [ ] 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
// 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 })
npx lighthouse <url>Chrome User Experience Report (CrUX) → BigQuery or API
Search Console → Core Web Vitals report
web-vitals library → Send to your analytics
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)
// 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
// 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
<!-- 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' }" />
Weekly Installs
51
Repository
GitHub Stars
1.8K
First Seen
Feb 5, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
cursor49
opencode49
gemini-cli48
codex48
github-copilot48
amp46
Schema标记专家指南:结构化数据实现与SEO优化,提升富媒体搜索结果
34,800 周安装
Neon Postgres 连接配置指南:Prisma、Drizzle ORM 与 PgBouncer 连接池最佳实践
237 周安装
Jotai适配器:连接Jotai状态管理与JSON-Render的桥梁 | 状态管理解决方案
238 周安装
Nansen 聪明钱 Alpha 发现工具 - 追踪智能资金代币积累信号
239 周安装
msgraph技能:本地搜索调用27,700+微软Graph API,无需网络,提升开发效率
238 周安装
NeMo Curator - NVIDIA GPU加速的大语言模型数据整理工具包,16倍去重速度
236 周安装
Claude Code MCP服务器配置指南:扩展AI助手功能(网络搜索/GitHub/文件访问)
244 周安装