[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M74PRMBSTVH4FNMBXB98TDVF":3,"skill-same:01M74PRMBSTVH4FNMBXB98TDVF":55},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":27,"commitSha":28,"skillDirectory":29,"forksCount":14,"repositoryHtmlUrl":30,"repositoryHomepageUrl":6,"ownerHtmlUrl":31,"ownerBlogUrl":32,"ownerDescription":33,"updatedAt":34,"install":35,"resources":48},"01M74PRMBSTVH4FNMBXB98TDVF","core-web-vitals",null,"Optimize Core Web Vitals (LCP, INP, CLS) for better page experience and search ranking. Use when asked to \"improve Core Web Vitals\", \"fix LCP\", \"reduce CLS\", \"optimize INP\", \"page experience optimization\", or \"fix layout shifts\".","cmetech\u002Fotto-cli","cmetech","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F118027304?v=4","01M74PNNHCP8T9GH9NWAWHK66N","Corey Ellis","01M74PNNJ1JR5BJQG3JK95RVFT",0,"MIT","https:\u002F\u002Fgithub.com\u002Fcmetech\u002Fotto-cli\u002Fblob\u002Fmain\u002Fsrc\u002Fresources\u002Fskills\u002Fcore-web-vitals\u002FSKILL.md",[],[],"2026-11-11 15:11:14",false,"---\nname: core-web-vitals\ndescription: Optimize Core Web Vitals (LCP, INP, CLS) for better page experience and search ranking. Use when asked to \"improve Core Web Vitals\", \"fix LCP\", \"reduce CLS\", \"optimize INP\", \"page experience optimization\", or \"fix layout shifts\".\nlicense: MIT\nmetadata:\n  author: web-quality-skills\n  version: \"1.0\"\n---\n\n# Core Web Vitals optimization\n\nTargeted optimization for the three Core Web Vitals metrics that affect Google Search ranking and user experience.\n\n## The three metrics\n\n| Metric | Measures | Good | Needs work | Poor |\n|--------|----------|------|------------|------|\n| **LCP** | Loading | ≤ 2.5s | 2.5s – 4s | > 4s |\n| **INP** | Interactivity | ≤ 200ms | 200ms – 500ms | > 500ms |\n| **CLS** | Visual Stability | ≤ 0.1 | 0.1 – 0.25 | > 0.25 |\n\nGoogle measures at the **75th percentile** — 75% of page visits must meet \"Good\" thresholds.\n\n---\n\n## LCP: Largest Contentful Paint\n\nLCP measures when the largest visible content element renders. Usually this is:\n- Hero image or video\n- Large text block\n- Background image\n- `\u003Csvg>` element\n\n### Common LCP issues\n\n**1. Slow server response (TTFB > 800ms)**\n```\nFix: CDN, caching, optimized backend, edge rendering\n```\n\n**2. Render-blocking resources**\n```html\n\u003C!-- ❌ Blocks rendering -->\n\u003Clink rel=\"stylesheet\" href=\"\u002Fall-styles.css\">\n\n\u003C!-- ✅ Critical CSS inlined, rest deferred -->\n\u003Cstyle>\u002F* Critical above-fold CSS *\u002F\u003C\u002Fstyle>\n\u003Clink rel=\"preload\" href=\"\u002Fstyles.css\" as=\"style\" \n      onload=\"this.onload=null;this.rel='stylesheet'\">\n```\n\n**3. Slow resource load times**\n```html\n\u003C!-- ❌ No hints, discovered late -->\n\u003Cimg src=\"\u002Fhero.jpg\" alt=\"Hero\">\n\n\u003C!-- ✅ Preloaded with high priority -->\n\u003Clink rel=\"preload\" href=\"\u002Fhero.webp\" as=\"image\" fetchpriority=\"high\">\n\u003Cimg src=\"\u002Fhero.webp\" alt=\"Hero\" fetchpriority=\"high\">\n```\n\n**4. Client-side rendering delays**\n```javascript\n\u002F\u002F ❌ Content loads after JavaScript\nuseEffect(() => {\n  fetch('\u002Fapi\u002Fhero-text').then(r => r.json()).then(setHeroText);\n}, []);\n\n\u002F\u002F ✅ Server-side or static rendering\n\u002F\u002F Use SSR, SSG, or streaming to send HTML with content\nexport async function getServerSideProps() {\n  const heroText = await fetchHeroText();\n  return { props: { heroText } };\n}\n```\n\n### LCP optimization checklist\n\n```markdown\n- [ ] TTFB \u003C 800ms (use CDN, edge caching)\n- [ ] LCP image preloaded with fetchpriority=\"high\"\n- [ ] LCP image optimized (WebP\u002FAVIF, correct size)\n- [ ] Critical CSS inlined (\u003C 14KB)\n- [ ] No render-blocking JavaScript in \u003Chead>\n- [ ] Fonts don't block text rendering (font-display: swap)\n- [ ] LCP element in initial HTML (not JS-rendered)\n```\n\n### LCP element identification\n```javascript\n\u002F\u002F Find your LCP element\nnew PerformanceObserver((list) => {\n  const entries = list.getEntries();\n  const lastEntry = entries[entries.length - 1];\n  console.log('LCP element:', lastEntry.element);\n  console.log('LCP time:', lastEntry.startTime);\n}).observe({ type: 'largest-contentful-paint', buffered: true });\n```\n\n---\n\n## INP: Interaction to Next Paint\n\nINP 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).\n\n### INP breakdown\n\nTotal INP = **Input Delay** + **Processing Time** + **Presentation Delay**\n\n| Phase | Target | Optimization |\n|-------|--------|--------------|\n| Input Delay | \u003C 50ms | Reduce main thread blocking |\n| Processing | \u003C 100ms | Optimize event handlers |\n| Presentation | \u003C 50ms | Minimize rendering work |\n\n### Common INP issues\n\n**1. Long tasks blocking main thread**\n```javascript\n\u002F\u002F ❌ Long synchronous task\nfunction processLargeArray(items) {\n  items.forEach(item => expensiveOperation(item));\n}\n\n\u002F\u002F ✅ Break into chunks with yielding\nasync function processLargeArray(items) {\n  const CHUNK_SIZE = 100;\n  for (let i = 0; i \u003C items.length; i += CHUNK_SIZE) {\n    const chunk = items.slice(i, i + CHUNK_SIZE);\n    chunk.forEach(item => expensiveOperation(item));\n    \n    \u002F\u002F Yield to main thread\n    await new Promise(r => setTimeout(r, 0));\n    \u002F\u002F Or use scheduler.yield() when available\n  }\n}\n```\n\n**2. Heavy event handlers**\n```javascript\n\u002F\u002F ❌ All work in handler\nbutton.addEventListener('click', () => {\n  \u002F\u002F Heavy computation\n  const result = calculateComplexThing();\n  \u002F\u002F DOM updates\n  updateUI(result);\n  \u002F\u002F Analytics\n  trackEvent('click');\n});\n\n\u002F\u002F ✅ Prioritize visual feedback\nbutton.addEventListener('click', () => {\n  \u002F\u002F Immediate visual feedback\n  button.classList.add('loading');\n  \n  \u002F\u002F Defer non-critical work\n  requestAnimationFrame(() => {\n    const result = calculateComplexThing();\n    updateUI(result);\n  });\n  \n  \u002F\u002F Use requestIdleCallback for analytics\n  requestIdleCallback(() => trackEvent('click'));\n});\n```\n\n**3. Third-party scripts**\n```javascript\n\u002F\u002F ❌ Eagerly loaded, blocks interactions\n\u003Cscript src=\"https:\u002F\u002Fheavy-widget.com\u002Fwidget.js\">\u003C\u002Fscript>\n\n\u002F\u002F ✅ Lazy loaded on interaction or visibility\nconst loadWidget = () => {\n  import('https:\u002F\u002Fheavy-widget.com\u002Fwidget.js')\n    .then(widget => widget.init());\n};\nbutton.addEventListener('click', loadWidget, { once: true });\n```\n\n**4. Excessive re-renders (React\u002FVue)**\n```javascript\n\u002F\u002F ❌ Re-renders entire tree\nfunction App() {\n  const [count, setCount] = useState(0);\n  return (\n    \u003Cdiv>\n      \u003CCounter count={count} \u002F>\n      \u003CExpensiveComponent \u002F> {\u002F* Re-renders on every count change *\u002F}\n    \u003C\u002Fdiv>\n  );\n}\n\n\u002F\u002F ✅ Memoized expensive components\nconst MemoizedExpensive = React.memo(ExpensiveComponent);\n\nfunction App() {\n  const [count, setCount] = useState(0);\n  return (\n    \u003Cdiv>\n      \u003CCounter count={count} \u002F>\n      \u003CMemoizedExpensive \u002F>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### INP optimization checklist\n\n```markdown\n- [ ] No tasks > 50ms on main thread\n- [ ] Event handlers complete quickly (\u003C 100ms)\n- [ ] Visual feedback provided immediately\n- [ ] Heavy work deferred with requestIdleCallback\n- [ ] Third-party scripts don't block interactions\n- [ ] Debounced input handlers where appropriate\n- [ ] Web Workers for CPU-intensive operations\n```\n\n### INP debugging\n```javascript\n\u002F\u002F Identify slow interactions\nnew PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    if (entry.duration > 200) {\n      console.warn('Slow interaction:', {\n        type: entry.name,\n        duration: entry.duration,\n        processingStart: entry.processingStart,\n        processingEnd: entry.processingEnd,\n        target: entry.target\n      });\n    }\n  }\n}).observe({ type: 'event', buffered: true, durationThreshold: 16 });\n```\n\n---\n\n## CLS: Cumulative Layout Shift\n\nCLS measures unexpected layout shifts. A shift occurs when a visible element changes position between frames without user interaction.\n\n**CLS Formula:** `impact fraction × distance fraction`\n\n### Common CLS causes\n\n**1. Images without dimensions**\n```html\n\u003C!-- ❌ Causes layout shift when loaded -->\n\u003Cimg src=\"photo.jpg\" alt=\"Photo\">\n\n\u003C!-- ✅ Space reserved -->\n\u003Cimg src=\"photo.jpg\" alt=\"Photo\" width=\"800\" height=\"600\">\n\n\u003C!-- ✅ Or use aspect-ratio -->\n\u003Cimg src=\"photo.jpg\" alt=\"Photo\" style=\"aspect-ratio: 4\u002F3; width: 100%;\">\n```\n\n**2. Ads, embeds, and iframes**\n```html\n\u003C!-- ❌ Unknown size until loaded -->\n\u003Ciframe src=\"https:\u002F\u002Fad-network.com\u002Fad\">\u003C\u002Fiframe>\n\n\u003C!-- ✅ Reserve space with min-height -->\n\u003Cdiv style=\"min-height: 250px;\">\n  \u003Ciframe src=\"https:\u002F\u002Fad-network.com\u002Fad\" height=\"250\">\u003C\u002Fiframe>\n\u003C\u002Fdiv>\n\n\u003C!-- ✅ Or use aspect-ratio container -->\n\u003Cdiv style=\"aspect-ratio: 16\u002F9;\">\n  \u003Ciframe src=\"https:\u002F\u002Fyoutube.com\u002Fembed\u002F...\" \n          style=\"width: 100%; height: 100%;\">\u003C\u002Fiframe>\n\u003C\u002Fdiv>\n```\n\n**3. Dynamically injected content**\n```javascript\n\u002F\u002F ❌ Inserts content above viewport\nnotifications.prepend(newNotification);\n\n\u002F\u002F ✅ Insert below viewport or use transform\nconst insertBelow = viewport.bottom \u003C newNotification.top;\nif (insertBelow) {\n  notifications.prepend(newNotification);\n} else {\n  \u002F\u002F Animate in without shifting\n  newNotification.style.transform = 'translateY(-100%)';\n  notifications.prepend(newNotification);\n  requestAnimationFrame(() => {\n    newNotification.style.transform = '';\n  });\n}\n```\n\n**4. Web fonts causing FOUT**\n```css\n\u002F* ❌ Font swap shifts text *\u002F\n@font-face {\n  font-family: 'Custom';\n  src: url('custom.woff2') format('woff2');\n}\n\n\u002F* ✅ Optional font (no shift if slow) *\u002F\n@font-face {\n  font-family: 'Custom';\n  src: url('custom.woff2') format('woff2');\n  font-display: optional;\n}\n\n\u002F* ✅ Or match fallback metrics *\u002F\n@font-face {\n  font-family: 'Custom';\n  src: url('custom.woff2') format('woff2');\n  font-display: swap;\n  size-adjust: 105%; \u002F* Match fallback size *\u002F\n  ascent-override: 95%;\n  descent-override: 20%;\n}\n```\n\n**5. Animations triggering layout**\n```css\n\u002F* ❌ Animates layout properties *\u002F\n.animate {\n  transition: height 0.3s, width 0.3s;\n}\n\n\u002F* ✅ Use transform instead *\u002F\n.animate {\n  transition: transform 0.3s;\n}\n.animate.expanded {\n  transform: scale(1.2);\n}\n```\n\n### CLS optimization checklist\n\n```markdown\n- [ ] All images have width\u002Fheight or aspect-ratio\n- [ ] All videos\u002Fembeds have reserved space\n- [ ] Ads have min-height containers\n- [ ] Fonts use font-display: optional or matched metrics\n- [ ] Dynamic content inserted below viewport\n- [ ] Animations use transform\u002Fopacity only\n- [ ] No content injected above existing content\n```\n\n### CLS debugging\n```javascript\n\u002F\u002F Track layout shifts\nnew PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    if (!entry.hadRecentInput) {\n      console.log('Layout shift:', entry.value);\n      entry.sources?.forEach(source => {\n        console.log('  Shifted element:', source.node);\n        console.log('  Previous rect:', source.previousRect);\n        console.log('  Current rect:', source.currentRect);\n      });\n    }\n  }\n}).observe({ type: 'layout-shift', buffered: true });\n```\n\n---\n\n## Measurement tools\n\n### Lab testing\n- **Chrome DevTools** → Performance panel, Lighthouse\n- **WebPageTest** → Detailed waterfall, filmstrip\n- **Lighthouse CLI** → `npx lighthouse \u003Curl>`\n\n### Field data (real users)\n- **Chrome User Experience Report (CrUX)** → BigQuery or API\n- **Search Console** → Core Web Vitals report\n- **web-vitals library** → Send to your analytics\n\n```javascript\nimport {onLCP, onINP, onCLS} from 'web-vitals';\n\nfunction sendToAnalytics({name, value, rating}) {\n  gtag('event', name, {\n    event_category: 'Web Vitals',\n    value: Math.round(name === 'CLS' ? value * 1000 : value),\n    event_label: rating\n  });\n}\n\nonLCP(sendToAnalytics);\nonINP(sendToAnalytics);\nonCLS(sendToAnalytics);\n```\n\n---\n\n## Framework quick fixes\n\n### Next.js\n```jsx\n\u002F\u002F LCP: Use next\u002Fimage with priority\nimport Image from 'next\u002Fimage';\n\u003CImage src=\"\u002Fhero.jpg\" priority fill alt=\"Hero\" \u002F>\n\n\u002F\u002F INP: Use dynamic imports\nconst HeavyComponent = dynamic(() => import('.\u002FHeavy'), { ssr: false });\n\n\u002F\u002F CLS: Image component handles dimensions automatically\n```\n\n### React\n```jsx\n\u002F\u002F LCP: Preload in head\n\u003Clink rel=\"preload\" href=\"\u002Fhero.jpg\" as=\"image\" fetchpriority=\"high\" \u002F>\n\n\u002F\u002F INP: Memoize and useTransition\nconst [isPending, startTransition] = useTransition();\nstartTransition(() => setExpensiveState(newValue));\n\n\u002F\u002F CLS: Always specify dimensions in img tags\n```\n\n### Vue\u002FNuxt\n```vue\n\u003C!-- LCP: Use nuxt\u002Fimage with preload -->\n\u003CNuxtImg src=\"\u002Fhero.jpg\" preload loading=\"eager\" \u002F>\n\n\u003C!-- INP: Use async components -->\n\u003Ccomponent :is=\"() => import('.\u002FHeavy.vue')\" \u002F>\n\n\u003C!-- CLS: Use aspect-ratio CSS -->\n\u003Cimg :style=\"{ aspectRatio: '16\u002F9' }\" \u002F>\n```\n\n## References\n\n- [web.dev LCP](https:\u002F\u002Fweb.dev\u002Farticles\u002Flcp)\n- [web.dev INP](https:\u002F\u002Fweb.dev\u002Farticles\u002Finp)\n- [web.dev CLS](https:\u002F\u002Fweb.dev\u002Farticles\u002Fcls)\n- [Code Optimizer skill](..\u002Fcode-optimizer\u002FSKILL.md)\n","# Core Web Vitals optimization\n\nTargeted optimization for the three Core Web Vitals metrics that affect Google Search ranking and user experience.\n\n## The three metrics\n\n| Metric | Measures | Good | Needs work | Poor |\n|--------|----------|------|------------|------|\n| **LCP** | Loading | ≤ 2.5s | 2.5s – 4s | > 4s |\n| **INP** | Interactivity | ≤ 200ms | 200ms – 500ms | > 500ms |\n| **CLS** | Visual Stability | ≤ 0.1 | 0.1 – 0.25 | > 0.25 |\n\nGoogle measures at the **75th percentile** — 75% of page visits must meet \"Good\" thresholds.\n\n---\n\n## LCP: Largest Contentful Paint\n\nLCP measures when the largest visible content element renders. Usually this is:\n- Hero image or video\n- Large text block\n- Background image\n- `\u003Csvg>` element\n\n### Common LCP issues\n\n**1. Slow server response (TTFB > 800ms)**\n```\nFix: CDN, caching, optimized backend, edge rendering\n```\n\n**2. Render-blocking resources**\n```html\n\u003C!-- ❌ Blocks rendering -->\n\u003Clink rel=\"stylesheet\" href=\"\u002Fall-styles.css\">\n\n\u003C!-- ✅ Critical CSS inlined, rest deferred -->\n\u003Cstyle>\u002F* Critical above-fold CSS *\u002F\u003C\u002Fstyle>\n\u003Clink rel=\"preload\" href=\"\u002Fstyles.css\" as=\"style\" \n      onload=\"this.onload=null;this.rel='stylesheet'\">\n```\n\n**3. Slow resource load times**\n```html\n\u003C!-- ❌ No hints, discovered late -->\n\u003Cimg src=\"\u002Fhero.jpg\" alt=\"Hero\">\n\n\u003C!-- ✅ Preloaded with high priority -->\n\u003Clink rel=\"preload\" href=\"\u002Fhero.webp\" as=\"image\" fetchpriority=\"high\">\n\u003Cimg src=\"\u002Fhero.webp\" alt=\"Hero\" fetchpriority=\"high\">\n```\n\n**4. Client-side rendering delays**\n```javascript\n\u002F\u002F ❌ Content loads after JavaScript\nuseEffect(() => {\n  fetch('\u002Fapi\u002Fhero-text').then(r => r.json()).then(setHeroText);\n}, []);\n\n\u002F\u002F ✅ Server-side or static rendering\n\u002F\u002F Use SSR, SSG, or streaming to send HTML with content\nexport async function getServerSideProps() {\n  const heroText = await fetchHeroText();\n  return { props: { heroText } };\n}\n```\n\n### LCP optimization checklist\n\n```markdown\n- [ ] TTFB \u003C 800ms (use CDN, edge caching)\n- [ ] LCP image preloaded with fetchpriority=\"high\"\n- [ ] LCP image optimized (WebP\u002FAVIF, correct size)\n- [ ] Critical CSS inlined (\u003C 14KB)\n- [ ] No render-blocking JavaScript in \u003Chead>\n- [ ] Fonts don't block text rendering (font-display: swap)\n- [ ] LCP element in initial HTML (not JS-rendered)\n```\n\n### LCP element identification\n```javascript\n\u002F\u002F Find your LCP element\nnew PerformanceObserver((list) => {\n  const entries = list.getEntries();\n  const lastEntry = entries[entries.length - 1];\n  console.log('LCP element:', lastEntry.element);\n  console.log('LCP time:', lastEntry.startTime);\n}).observe({ type: 'largest-contentful-paint', buffered: true });\n```\n\n---\n\n## INP: Interaction to Next Paint\n\nINP 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).\n\n### INP breakdown\n\nTotal INP = **Input Delay** + **Processing Time** + **Presentation Delay**\n\n| Phase | Target | Optimization |\n|-------|--------|--------------|\n| Input Delay | \u003C 50ms | Reduce main thread blocking |\n| Processing | \u003C 100ms | Optimize event handlers |\n| Presentation | \u003C 50ms | Minimize rendering work |\n\n### Common INP issues\n\n**1. Long tasks blocking main thread**\n```javascript\n\u002F\u002F ❌ Long synchronous task\nfunction processLargeArray(items) {\n  items.forEach(item => expensiveOperation(item));\n}\n\n\u002F\u002F ✅ Break into chunks with yielding\nasync function processLargeArray(items) {\n  const CHUNK_SIZE = 100;\n  for (let i = 0; i \u003C items.length; i += CHUNK_SIZE) {\n    const chunk = items.slice(i, i + CHUNK_SIZE);\n    chunk.forEach(item => expensiveOperation(item));\n    \n    \u002F\u002F Yield to main thread\n    await new Promise(r => setTimeout(r, 0));\n    \u002F\u002F Or use scheduler.yield() when available\n  }\n}\n```\n\n**2. Heavy event handlers**\n```javascript\n\u002F\u002F ❌ All work in handler\nbutton.addEventListener('click', () => {\n  \u002F\u002F Heavy computation\n  const result = calculateComplexThing();\n  \u002F\u002F DOM updates\n  updateUI(result);\n  \u002F\u002F Analytics\n  trackEvent('click');\n});\n\n\u002F\u002F ✅ Prioritize visual feedback\nbutton.addEventListener('click', () => {\n  \u002F\u002F Immediate visual feedback\n  button.classList.add('loading');\n  \n  \u002F\u002F Defer non-critical work\n  requestAnimationFrame(() => {\n    const result = calculateComplexThing();\n    updateUI(result);\n  });\n  \n  \u002F\u002F Use requestIdleCallback for analytics\n  requestIdleCallback(() => trackEvent('click'));\n});\n```\n\n**3. Third-party scripts**\n```javascript\n\u002F\u002F ❌ Eagerly loaded, blocks interactions\n\u003Cscript src=\"https:\u002F\u002Fheavy-widget.com\u002Fwidget.js\">\u003C\u002Fscript>\n\n\u002F\u002F ✅ Lazy loaded on interaction or visibility\nconst loadWidget = () => {\n  import('https:\u002F\u002Fheavy-widget.com\u002Fwidget.js')\n    .then(widget => widget.init());\n};\nbutton.addEventListener('click', loadWidget, { once: true });\n```\n\n**4. Excessive re-renders (React\u002FVue)**\n```javascript\n\u002F\u002F ❌ Re-renders entire tree\nfunction App() {\n  const [count, setCount] = useState(0);\n  return (\n    \u003Cdiv>\n      \u003CCounter count={count} \u002F>\n      \u003CExpensiveComponent \u002F> {\u002F* Re-renders on every count change *\u002F}\n    \u003C\u002Fdiv>\n  );\n}\n\n\u002F\u002F ✅ Memoized expensive components\nconst MemoizedExpensive = React.memo(ExpensiveComponent);\n\nfunction App() {\n  const [count, setCount] = useState(0);\n  return (\n    \u003Cdiv>\n      \u003CCounter count={count} \u002F>\n      \u003CMemoizedExpensive \u002F>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### INP optimization checklist\n\n```markdown\n- [ ] No tasks > 50ms on main thread\n- [ ] Event handlers complete quickly (\u003C 100ms)\n- [ ] Visual feedback provided immediately\n- [ ] Heavy work deferred with requestIdleCallback\n- [ ] Third-party scripts don't block interactions\n- [ ] Debounced input handlers where appropriate\n- [ ] Web Workers for CPU-intensive operations\n```\n\n### INP debugging\n```javascript\n\u002F\u002F Identify slow interactions\nnew PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    if (entry.duration > 200) {\n      console.warn('Slow interaction:', {\n        type: entry.name,\n        duration: entry.duration,\n        processingStart: entry.processingStart,\n        processingEnd: entry.processingEnd,\n        target: entry.target\n      });\n    }\n  }\n}).observe({ type: 'event', buffered: true, durationThreshold: 16 });\n```\n\n---\n\n## CLS: Cumulative Layout Shift\n\nCLS measures unexpected layout shifts. A shift occurs when a visible element changes position between frames without user interaction.\n\n**CLS Formula:** `impact fraction × distance fraction`\n\n### Common CLS causes\n\n**1. Images without dimensions**\n```html\n\u003C!-- ❌ Causes layout shift when loaded -->\n\u003Cimg src=\"photo.jpg\" alt=\"Photo\">\n\n\u003C!-- ✅ Space reserved -->\n\u003Cimg src=\"photo.jpg\" alt=\"Photo\" width=\"800\" height=\"600\">\n\n\u003C!-- ✅ Or use aspect-ratio -->\n\u003Cimg src=\"photo.jpg\" alt=\"Photo\" style=\"aspect-ratio: 4\u002F3; width: 100%;\">\n```\n\n**2. Ads, embeds, and iframes**\n```html\n\u003C!-- ❌ Unknown size until loaded -->\n\u003Ciframe src=\"https:\u002F\u002Fad-network.com\u002Fad\">\u003C\u002Fiframe>\n\n\u003C!-- ✅ Reserve space with min-height -->\n\u003Cdiv style=\"min-height: 250px;\">\n  \u003Ciframe src=\"https:\u002F\u002Fad-network.com\u002Fad\" height=\"250\">\u003C\u002Fiframe>\n\u003C\u002Fdiv>\n\n\u003C!-- ✅ Or use aspect-ratio container -->\n\u003Cdiv style=\"aspect-ratio: 16\u002F9;\">\n  \u003Ciframe src=\"https:\u002F\u002Fyoutube.com\u002Fembed\u002F...\" \n          style=\"width: 100%; height: 100%;\">\u003C\u002Fiframe>\n\u003C\u002Fdiv>\n```\n\n**3. Dynamically injected content**\n```javascript\n\u002F\u002F ❌ Inserts content above viewport\nnotifications.prepend(newNotification);\n\n\u002F\u002F ✅ Insert below viewport or use transform\nconst insertBelow = viewport.bottom \u003C newNotification.top;\nif (insertBelow) {\n  notifications.prepend(newNotification);\n} else {\n  \u002F\u002F Animate in without shifting\n  newNotification.style.transform = 'translateY(-100%)';\n  notifications.prepend(newNotification);\n  requestAnimationFrame(() => {\n    newNotification.style.transform = '';\n  });\n}\n```\n\n**4. Web fonts causing FOUT**\n```css\n\u002F* ❌ Font swap shifts text *\u002F\n@font-face {\n  font-family: 'Custom';\n  src: url('custom.woff2') format('woff2');\n}\n\n\u002F* ✅ Optional font (no shift if slow) *\u002F\n@font-face {\n  font-family: 'Custom';\n  src: url('custom.woff2') format('woff2');\n  font-display: optional;\n}\n\n\u002F* ✅ Or match fallback metrics *\u002F\n@font-face {\n  font-family: 'Custom';\n  src: url('custom.woff2') format('woff2');\n  font-display: swap;\n  size-adjust: 105%; \u002F* Match fallback size *\u002F\n  ascent-override: 95%;\n  descent-override: 20%;\n}\n```\n\n**5. Animations triggering layout**\n```css\n\u002F* ❌ Animates layout properties *\u002F\n.animate {\n  transition: height 0.3s, width 0.3s;\n}\n\n\u002F* ✅ Use transform instead *\u002F\n.animate {\n  transition: transform 0.3s;\n}\n.animate.expanded {\n  transform: scale(1.2);\n}\n```\n\n### CLS optimization checklist\n\n```markdown\n- [ ] All images have width\u002Fheight or aspect-ratio\n- [ ] All videos\u002Fembeds have reserved space\n- [ ] Ads have min-height containers\n- [ ] Fonts use font-display: optional or matched metrics\n- [ ] Dynamic content inserted below viewport\n- [ ] Animations use transform\u002Fopacity only\n- [ ] No content injected above existing content\n```\n\n### CLS debugging\n```javascript\n\u002F\u002F Track layout shifts\nnew PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    if (!entry.hadRecentInput) {\n      console.log('Layout shift:', entry.value);\n      entry.sources?.forEach(source => {\n        console.log('  Shifted element:', source.node);\n        console.log('  Previous rect:', source.previousRect);\n        console.log('  Current rect:', source.currentRect);\n      });\n    }\n  }\n}).observe({ type: 'layout-shift', buffered: true });\n```\n\n---\n\n## Measurement tools\n\n### Lab testing\n- **Chrome DevTools** → Performance panel, Lighthouse\n- **WebPageTest** → Detailed waterfall, filmstrip\n- **Lighthouse CLI** → `npx lighthouse \u003Curl>`\n\n### Field data (real users)\n- **Chrome User Experience Report (CrUX)** → BigQuery or API\n- **Search Console** → Core Web Vitals report\n- **web-vitals library** → Send to your analytics\n\n```javascript\nimport {onLCP, onINP, onCLS} from 'web-vitals';\n\nfunction sendToAnalytics({name, value, rating}) {\n  gtag('event', name, {\n    event_category: 'Web Vitals',\n    value: Math.round(name === 'CLS' ? value * 1000 : value),\n    event_label: rating\n  });\n}\n\nonLCP(sendToAnalytics);\nonINP(sendToAnalytics);\nonCLS(sendToAnalytics);\n```\n\n---\n\n## Framework quick fixes\n\n### Next.js\n```jsx\n\u002F\u002F LCP: Use next\u002Fimage with priority\nimport Image from 'next\u002Fimage';\n\u003CImage src=\"\u002Fhero.jpg\" priority fill alt=\"Hero\" \u002F>\n\n\u002F\u002F INP: Use dynamic imports\nconst HeavyComponent = dynamic(() => import('.\u002FHeavy'), { ssr: false });\n\n\u002F\u002F CLS: Image component handles dimensions automatically\n```\n\n### React\n```jsx\n\u002F\u002F LCP: Preload in head\n\u003Clink rel=\"preload\" href=\"\u002Fhero.jpg\" as=\"image\" fetchpriority=\"high\" \u002F>\n\n\u002F\u002F INP: Memoize and useTransition\nconst [isPending, startTransition] = useTransition();\nstartTransition(() => setExpensiveState(newValue));\n\n\u002F\u002F CLS: Always specify dimensions in img tags\n```\n\n### Vue\u002FNuxt\n```vue\n\u003C!-- LCP: Use nuxt\u002Fimage with preload -->\n\u003CNuxtImg src=\"\u002Fhero.jpg\" preload loading=\"eager\" \u002F>\n\n\u003C!-- INP: Use async components -->\n\u003Ccomponent :is=\"() => import('.\u002FHeavy.vue')\" \u002F>\n\n\u003C!-- CLS: Use aspect-ratio CSS -->\n\u003Cimg :style=\"{ aspectRatio: '16\u002F9' }\" \u002F>\n```\n\n## References\n\n- [web.dev LCP](https:\u002F\u002Fweb.dev\u002Farticles\u002Flcp)\n- [web.dev INP](https:\u002F\u002Fweb.dev\u002Farticles\u002Finp)\n- [web.dev CLS](https:\u002F\u002Fweb.dev\u002Farticles\u002Fcls)\n- [Code Optimizer skill](..\u002Fcode-optimizer\u002FSKILL.md)\n",{"name":5,"license":15,"metadata":24,"description":7},{"author":25,"version":26},"web-quality-skills","1.0","main","e62d23e713a36a0570f7f5c3ee69868b1131e548","src\u002Fresources\u002Fskills\u002Fcore-web-vitals","https:\u002F\u002Fgithub.com\u002Fcmetech\u002Fotto-cli","https:\u002F\u002Fgithub.com\u002Fcmetech","https:\u002F\u002Fwww.cmetech.io","Corey Ellis is a seasoned data engineer with over 20 years of experience in the field. He is the founder of CME Technologies LLC.","2026-06-17 22:50:56",{"source":8,"skillName":5,"commands":36,"prompt":46,"zipAvailable":47},[37,40,43],{"tool":38,"command":39},"npx","npx skills add cmetech\u002Fotto-cli --skill core-web-vitals",{"tool":41,"command":42},"bunx","bunx skills add cmetech\u002Fotto-cli --skill core-web-vitals",{"tool":44,"command":45},"pnpm","pnpm dlx skills add cmetech\u002Fotto-cli --skill core-web-vitals","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fcmetech\u002Fotto-cli\u002Fblob\u002Fmain\u002Fsrc\u002Fresources\u002Fskills\u002Fcore-web-vitals\u002FSKILL.md",true,[49],{"path":50,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":53,"sourceHtmlUrl":54},"references\u002FLCP.md","REFERENCE","参考文档",4431,"https:\u002F\u002Fgithub.com\u002Fcmetech\u002Fotto-cli\u002Fblob\u002Fmain\u002Fsrc\u002Fresources\u002Fskills\u002Fcore-web-vitals\u002Freferences\u002FLCP.md",[56,63,70,77,84,91],{"skillNo":57,"name":58,"nameZh":6,"description":59,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":60,"categories":61,"keywords":62,"publishedAt":19},"01M74PWE6AVSD8RDS1M3Q6TH95","write-milestone-brief","Synthesize the current conversation into a milestone brief (PRD). Writes to `M###-CONTEXT.md` by default, or files a GitHub issue only with explicit user confirmation. Use when asked to \"turn this into a PRD\", \"draft a milestone brief\", \"capture this context\", \"write it up\", or when enough has been discussed to commit the plan to paper. Does not interview — it synthesizes what is already known.","https:\u002F\u002Fgithub.com\u002Fcmetech\u002Fotto-cli\u002Fblob\u002Fmain\u002Fsrc\u002Fresources\u002Fskills\u002Fwrite-milestone-brief\u002FSKILL.md",[],[],{"skillNo":64,"name":65,"nameZh":6,"description":66,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":67,"categories":68,"keywords":69,"publishedAt":19},"01M74PWB6CEEQPGS9HRQY4QQPZ","write-docs","Collaborative document authoring workflow for proposals, technical specs, decision docs, README sections, ADRs, and long-form prose that must work for fresh readers. Use when asked to \"write the docs\", \"draft a proposal\", \"write a spec\", \"write an RFC\", \"write the README\", or when a document needs to be understandable by someone without this session's context. Three stages: gather context, iterate on structure, reader-test for a stranger.","https:\u002F\u002Fgithub.com\u002Fcmetech\u002Fotto-cli\u002Fblob\u002Fmain\u002Fsrc\u002Fresources\u002Fskills\u002Fwrite-docs\u002FSKILL.md",[],[],{"skillNo":71,"name":72,"nameZh":6,"description":73,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":74,"categories":75,"keywords":76,"publishedAt":19},"01M74PW7SJQ8H516MDBQ7WV1BN","web-quality-audit","Comprehensive web quality audit covering performance, accessibility, SEO, and best practices. Use when asked to \"audit my site\", \"review web quality\", \"run lighthouse audit\", \"check page quality\", or \"optimize my website\".","https:\u002F\u002Fgithub.com\u002Fcmetech\u002Fotto-cli\u002Fblob\u002Fmain\u002Fsrc\u002Fresources\u002Fskills\u002Fweb-quality-audit\u002FSKILL.md",[],[],{"skillNo":78,"name":79,"nameZh":6,"description":80,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":81,"categories":82,"keywords":83,"publishedAt":19},"01M74PW5JAA6D34Z8QBN1HQ014","web-design-guidelines","Review UI code for Web Interface Guidelines compliance. Use when asked to \"review my UI\", \"check accessibility\", \"audit design\", \"review UX\", or \"check my site against best practices\".","https:\u002F\u002Fgithub.com\u002Fcmetech\u002Fotto-cli\u002Fblob\u002Fmain\u002Fsrc\u002Fresources\u002Fskills\u002Fweb-design-guidelines\u002FSKILL.md",[],[],{"skillNo":85,"name":86,"nameZh":6,"description":87,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":88,"categories":89,"keywords":90,"publishedAt":19},"01M74PW1YP2Y03GMDYD443BVHQ","verify-before-complete","Block completion claims until verification evidence has been produced in the current message. Use before marking a task\u002Fslice\u002Fmilestone complete, before creating a commit or PR, before saying \"it works\" or \"tests pass\", and any time you are about to claim work is done. The rule is: evidence before claims, always — running the verification must happen now, not \"earlier in the session\". Fresh output or no claim.","https:\u002F\u002Fgithub.com\u002Fcmetech\u002Fotto-cli\u002Fblob\u002Fmain\u002Fsrc\u002Fresources\u002Fskills\u002Fverify-before-complete\u002FSKILL.md",[],[],{"skillNo":92,"name":93,"nameZh":6,"description":94,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":95,"categories":96,"keywords":97,"publishedAt":19},"01M74PVXR5GE8HE55YWEG7ENBP","userinterface-wiki","UI\u002FUX best practices for web interfaces. Use when reviewing animations, CSS, audio, typography, UX patterns, prefetching, or icon implementations. Covers 11 categories from animation principles to typography. Outputs file:line findings.","https:\u002F\u002Fgithub.com\u002Fcmetech\u002Fotto-cli\u002Fblob\u002Fmain\u002Fsrc\u002Fresources\u002Fskills\u002Fuserinterface-wiki\u002FSKILL.md",[],[]]