@jgamaraalv/ts-dev-kit 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/CHANGELOG.md +52 -0
  4. package/README.md +106 -50
  5. package/package.json +2 -2
  6. package/skills/codebase-adapter/SKILL.md +237 -0
  7. package/skills/codebase-adapter/template.md +25 -0
  8. package/skills/conventional-commits/SKILL.md +12 -0
  9. package/skills/core-web-vitals/SKILL.md +102 -0
  10. package/skills/core-web-vitals/references/cls.md +154 -0
  11. package/skills/core-web-vitals/references/inp.md +140 -0
  12. package/skills/core-web-vitals/references/lcp.md +89 -0
  13. package/skills/core-web-vitals/references/tools.md +112 -0
  14. package/skills/core-web-vitals/scripts/visualize.py +222 -0
  15. package/skills/debug/SKILL.md +10 -27
  16. package/skills/debug/template.md +23 -0
  17. package/skills/{task → execute-task}/SKILL.md +78 -50
  18. package/skills/generate-prd/SKILL.md +56 -0
  19. package/skills/generate-prd/template.md +69 -0
  20. package/skills/generate-task/SKILL.md +136 -0
  21. package/skills/generate-task/template.md +71 -0
  22. package/skills/owasp-security-review/SKILL.md +1 -10
  23. package/skills/owasp-security-review/template.md +6 -0
  24. package/skills/tanstack-query/SKILL.md +348 -0
  25. package/skills/tanstack-query/references/advanced-patterns.md +376 -0
  26. package/skills/tanstack-query/references/api-reference.md +297 -0
  27. package/skills/tanstack-query/references/ssr-nextjs.md +272 -0
  28. package/skills/tanstack-query/references/testing.md +175 -0
  29. package/skills/ui-ux-guidelines/SKILL.md +1 -17
  30. package/skills/ui-ux-guidelines/template.md +15 -0
  31. /package/skills/{task → execute-task}/references/agent-dispatch.md +0 -0
  32. /package/skills/{task → execute-task}/references/verification-protocol.md +0 -0
  33. /package/skills/{task/references/output-templates.md → execute-task/template.md} +0 -0
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: core-web-vitals
3
+ description: "Core Web Vitals reference for measuring, diagnosing, and improving LCP, INP, and CLS. Use when: (1) auditing page performance against Google's thresholds, (2) implementing the web-vitals JS library for field monitoring, (3) diagnosing slow LCP, high INP, or layout shifts, (4) choosing between field and lab measurement tools, (5) optimizing specific metrics with the Chrome team's top recommendations, (6) explaining what each metric measures to non-technical stakeholders, or (7) generating a visual CWV report from metric values or a Lighthouse JSON file."
4
+ allowed-tools: Bash(python3 *)
5
+ ---
6
+
7
+ # Core Web Vitals
8
+
9
+ The three stable Core Web Vitals, each measured at the **75th percentile** of
10
+ real page loads (segmented by mobile and desktop):
11
+
12
+ | Metric | Measures | Good | Needs Improvement | Poor |
13
+ |--------|----------|------|-------------------|------|
14
+ | **LCP** — Largest Contentful Paint | Loading | ≤ 2.5 s | 2.5–4.0 s | > 4.0 s |
15
+ | **INP** — Interaction to Next Paint | Interactivity | ≤ 200 ms | 200–500 ms | > 500 ms |
16
+ | **CLS** — Cumulative Layout Shift | Visual stability | ≤ 0.1 | 0.1–0.25 | > 0.25 |
17
+
18
+ A page **passes** Core Web Vitals only if all three metrics meet "Good" at the 75th percentile.
19
+
20
+ ## Quick setup: measure all three in the field
21
+
22
+ ```bash
23
+ npm install web-vitals
24
+ ```
25
+
26
+ ```js
27
+ import { onCLS, onINP, onLCP } from 'web-vitals';
28
+
29
+ function sendToAnalytics(metric) {
30
+ const body = JSON.stringify(metric);
31
+ (navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
32
+ fetch('/analytics', { body, method: 'POST', keepalive: true });
33
+ }
34
+
35
+ onCLS(sendToAnalytics);
36
+ onINP(sendToAnalytics);
37
+ onLCP(sendToAnalytics);
38
+ ```
39
+
40
+ Each callback receives `{ name, value, rating, delta, id, navigationType }`.
41
+ `rating` is `"good"`, `"needs-improvement"`, or `"poor"`.
42
+
43
+ > The `web-vitals` library handles bfcache restores, prerendered pages, iframe
44
+ > aggregation, and other edge cases that raw PerformanceObserver does not.
45
+
46
+ ## Tools matrix
47
+
48
+ | Tool | Type | LCP | INP | CLS | Notes |
49
+ |------|------|-----|-----|-----|-------|
50
+ | Chrome User Experience Report (CrUX) | Field | ✓ | ✓ | ✓ | 28-day rolling window of real users |
51
+ | PageSpeed Insights | Field + Lab | ✓ | ✓ | ✓ | Field = CrUX data; Lab = Lighthouse |
52
+ | Search Console CWV report | Field | ✓ | ✓ | ✓ | Groups URLs by template |
53
+ | Chrome DevTools Performance panel | Field + Lab | ✓ | ✓ | ✓ | Local profiling, interaction tracing |
54
+ | Lighthouse | Lab | ✓ | TBT* | ✓ | CI integration; INP → use TBT as proxy |
55
+
56
+ *Lighthouse uses **Total Blocking Time (TBT)** as a lab proxy for INP. TBT
57
+ correlates with INP but does not replace field measurement.
58
+
59
+ ## Supporting metrics (non-Core but diagnostic)
60
+
61
+ - **FCP** (First Contentful Paint) — diagnoses render-blocking resources upstream of LCP
62
+ - **TTFB** (Time to First Byte) — server response time; directly affects LCP
63
+ - **TBT** (Total Blocking Time) — lab proxy for INP; identifies long tasks
64
+
65
+ ## When to read reference files
66
+
67
+ | Reference | Read when… |
68
+ |-----------|-----------|
69
+ | [references/lcp.md](references/lcp.md) | LCP > 2.5 s, diagnosing slow image/text load, preload/CDN questions |
70
+ | [references/inp.md](references/inp.md) | INP > 200 ms, slow click/key/tap response, long task investigations |
71
+ | [references/cls.md](references/cls.md) | CLS > 0.1, elements jumping on scroll or load, font/image shift |
72
+ | [references/tools.md](references/tools.md) | Setting up monitoring, using DevTools/Lighthouse/PSI, top-9 optimization checklist |
73
+
74
+ ## Generate a visual report
75
+
76
+ When the user provides metric values or a Lighthouse JSON file, generate an interactive HTML report and open it in the browser:
77
+
78
+ ```bash
79
+ # From manual values
80
+ python3 ~/.claude/skills/core-web-vitals/scripts/visualize.py \
81
+ --lcp 2.1 --inp 180 --cls 0.05 \
82
+ --url https://example.com
83
+
84
+ # From a Lighthouse JSON output
85
+ python3 ~/.claude/skills/core-web-vitals/scripts/visualize.py \
86
+ --lighthouse lighthouse-report.json
87
+
88
+ # Custom output path, no auto-open
89
+ python3 ~/.claude/skills/core-web-vitals/scripts/visualize.py \
90
+ --lcp 3.8 --inp 420 --cls 0.12 \
91
+ --output cwv-report.html --no-open
92
+ ```
93
+
94
+ The script (`scripts/visualize.py`) requires only Python 3 stdlib — no packages to install.
95
+ It outputs a self-contained HTML file with color-coded metric cards, a visual progress bar showing where each value falls on the Good/Needs Improvement/Poor scale, and an overall PASS/FAIL/NEEDS IMPROVEMENT verdict.
96
+
97
+ ## Metric lifecycle
98
+
99
+ Metrics progress through: **Experimental → Pending → Stable**.
100
+ All three current Core Web Vitals (LCP, CLS, INP) are **Stable**.
101
+ INP replaced FID (First Input Delay) in March 2024.
102
+ Changes to stable metrics follow an annual cadence with advance notice.
@@ -0,0 +1,154 @@
1
+ # CLS — Cumulative Layout Shift
2
+
3
+ **Good:** ≤ 0.1 | **Needs improvement:** 0.1–0.25 | **Poor:** > 0.25
4
+ (measured at the 75th percentile of field loads)
5
+
6
+ CLS measures the largest burst of layout shift scores across the entire page
7
+ lifecycle. A layout shift occurs when a visible element changes its start
8
+ position from one rendered frame to the next.
9
+
10
+ ## How the CLS score is calculated
11
+
12
+ ```
13
+ layout shift score = impact fraction × distance fraction
14
+
15
+ impact fraction = combined area of unstable elements (before + after)
16
+ ─────────────────────────────────────────────────
17
+ total viewport area
18
+
19
+ distance fraction = greatest distance any unstable element moved
20
+ ──────────────────────────────────────────────
21
+ largest viewport dimension (width or height)
22
+ ```
23
+
24
+ Shifts are grouped into **session windows** (max 5 s, max 1 s gap between
25
+ shifts). CLS = the session window with the highest cumulative score.
26
+
27
+ **Example:** element moves from top 25% to top 50% of viewport →
28
+ - impact fraction ≈ 0.75 (covers 75% of viewport across both positions)
29
+ - distance fraction = 0.25 (moved 25% of viewport height)
30
+ - layout shift score = 0.75 × 0.25 = **0.1875**
31
+
32
+ ## Expected vs. unexpected layout shifts
33
+
34
+ Only **unexpected** shifts count toward CLS. User-initiated shifts are excluded:
35
+ - Clicking a button that expands content ✓ (expected)
36
+ - Content jumping after an ad loads without reserved space ✗ (unexpected)
37
+ - A banner appearing after page load without reserved space ✗ (unexpected)
38
+
39
+ Shifts within 500 ms of a user interaction do not count.
40
+ Animations and transitions using CSS `transform` do not cause layout shifts —
41
+ they run off the main thread and do not trigger layout recalculation.
42
+
43
+ ## Common causes and fixes
44
+
45
+ ### Images and videos without dimensions
46
+ Images load asynchronously; without explicit dimensions, the browser doesn't
47
+ know how much space to reserve.
48
+
49
+ ```html
50
+ <!-- Bad: no dimensions, causes layout shift when image loads -->
51
+ <img src="/hero.jpg" alt="hero">
52
+
53
+ <!-- Good: explicit width/height let browser reserve space -->
54
+ <img src="/hero.jpg" alt="hero" width="1200" height="600">
55
+
56
+ <!-- Also good for responsive images -->
57
+ <img src="/hero.jpg" alt="hero" width="1200" height="600"
58
+ style="width: 100%; height: auto;">
59
+ ```
60
+
61
+ The `aspect-ratio` CSS property is a modern alternative:
62
+ ```css
63
+ img { aspect-ratio: 16 / 9; width: 100%; }
64
+ ```
65
+
66
+ ### Ads, embeds, and iframes without reserved space
67
+ Always define a minimum height for ad slots and dynamic embed containers:
68
+ ```css
69
+ .ad-slot { min-height: 250px; }
70
+ .video-embed { aspect-ratio: 16 / 9; }
71
+ ```
72
+
73
+ ### Dynamically injected content above existing content
74
+ Avoid inserting banners, cookie notices, or promotional elements above existing
75
+ page content after load. If necessary, reserve the space in the layout before
76
+ content loads, or insert below the fold.
77
+
78
+ ### Web fonts causing FOUT/FOIT shifts
79
+ Flash of Unstyled Text (FOUT) can cause layout shifts if the fallback font
80
+ has different metrics than the loaded font.
81
+
82
+ ```css
83
+ /* Preferred: font-display: optional never shows fallback, eliminates shift */
84
+ @font-face {
85
+ font-family: 'MyFont';
86
+ src: url('/fonts/myfont.woff2') format('woff2');
87
+ font-display: optional;
88
+ }
89
+ ```
90
+
91
+ Use the `size-adjust`, `ascent-override`, `descent-override`, and
92
+ `line-gap-override` descriptors to match fallback font metrics:
93
+ ```css
94
+ @font-face {
95
+ font-family: 'MyFont-fallback';
96
+ src: local('Arial');
97
+ size-adjust: 104%;
98
+ ascent-override: 90%;
99
+ }
100
+ ```
101
+
102
+ ### Animations that trigger layout (not using transform)
103
+ CSS properties that trigger layout recalculation cause shifts:
104
+ - **Avoid:** `top`, `left`, `right`, `bottom`, `margin`, `padding`, `width`, `height`
105
+ - **Use instead:** `transform: translate()`, `transform: scale()`, `opacity`
106
+
107
+ ```css
108
+ /* Bad: triggers layout */
109
+ .slide-in { transition: margin-left 0.3s ease; }
110
+
111
+ /* Good: compositor-only, no layout shift */
112
+ .slide-in { transition: transform 0.3s ease; transform: translateX(0); }
113
+ .slide-in.hidden { transform: translateX(-100%); }
114
+ ```
115
+
116
+ ## bfcache eligibility improves CLS
117
+
118
+ Pages restored from the back/forward cache (bfcache) don't reload resources,
119
+ which eliminates many common layout shifts. Ensure your pages are bfcache
120
+ eligible:
121
+ - Avoid `unload` event listeners
122
+ - Don't use `Cache-Control: no-store` unnecessarily
123
+ - Close open `IndexedDB` transactions before navigating away
124
+ - Avoid `window.opener` references
125
+
126
+ Check bfcache eligibility in Chrome DevTools: Application → Back/forward cache.
127
+
128
+ ## Measure CLS in JavaScript
129
+
130
+ ```js
131
+ let clsValue = 0;
132
+ let clsEntries = [];
133
+
134
+ new PerformanceObserver((list) => {
135
+ for (const entry of list.getEntries()) {
136
+ if (!entry.hadRecentInput) {
137
+ clsValue += entry.value;
138
+ clsEntries.push(entry);
139
+ }
140
+ }
141
+ }).observe({ type: 'layout-shift', buffered: true });
142
+ ```
143
+
144
+ Prefer `onCLS()` from the `web-vitals` library — it correctly implements
145
+ session windowing and excludes user-initiated shifts.
146
+
147
+ ## Key nuances
148
+
149
+ - CLS is measured for the entire page lifecycle, including after user interaction
150
+ - `layout-shift` entries from iframes are not visible in the parent frame's
151
+ raw API — the web-vitals library does not cover this gap either
152
+ - CLS is 0 for pages with no layout shifts (even with heavy JS)
153
+ - Adding new DOM elements that push existing elements down = layout shift
154
+ only if it changes the **start position** of existing visible elements
@@ -0,0 +1,140 @@
1
+ # INP — Interaction to Next Paint
2
+
3
+ **Good:** ≤ 200 ms | **Needs improvement:** 200–500 ms | **Poor:** > 500 ms
4
+ (measured at the 75th percentile of field loads)
5
+
6
+ INP replaced FID (First Input Delay) as a Core Web Vital in March 2024.
7
+ Unlike FID (only measured the first interaction's input delay), INP measures
8
+ **all interactions** on the page — clicks, taps, and key presses — and reports
9
+ the worst one observed.
10
+
11
+ ## What makes up an interaction?
12
+
13
+ Each interaction has three phases:
14
+
15
+ ```
16
+ [user gesture]
17
+
18
+
19
+ ┌─────────────────┐
20
+ │ Input delay │ Time before event handlers start (blocked by other tasks)
21
+ └────────┬────────┘
22
+
23
+
24
+ ┌─────────────────┐
25
+ │ Processing time │ Time to run event handlers (JS execution)
26
+ └────────┬────────┘
27
+
28
+
29
+ ┌─────────────────┐
30
+ │Presentation │ Time to render the next frame (layout, paint, composite)
31
+ │delay │
32
+ └─────────────────┘
33
+
34
+
35
+ [visual feedback]
36
+ ```
37
+
38
+ INP = input delay + processing time + presentation delay
39
+
40
+ Only clicks, taps, and key presses count. Hover and scroll do not.
41
+ No INP value is reported if the user never interacts with the page.
42
+
43
+ ## Common causes of high INP
44
+
45
+ ### Long input delay (main thread is busy)
46
+ - Long tasks (> 50 ms) scheduled during or just before the interaction
47
+ - Timer callbacks (`setInterval`, `setTimeout`) running too frequently
48
+ - Third-party scripts blocking the main thread
49
+
50
+ **Fix: yield to the main thread between tasks**
51
+
52
+ ```js
53
+ // Instead of doing everything synchronously:
54
+ function handleClick() {
55
+ doHeavyWork(); // blocks interaction handling
56
+ updateUI();
57
+ }
58
+
59
+ // Yield after each chunk using scheduler.yield() (Chrome 129+)
60
+ async function handleClick() {
61
+ doFirstChunk();
62
+ await scheduler.yield(); // gives browser a chance to handle interactions
63
+ doSecondChunk();
64
+ await scheduler.yield();
65
+ updateUI();
66
+ }
67
+
68
+ // Fallback for older browsers
69
+ function yieldToMain() {
70
+ return new Promise(resolve => setTimeout(resolve, 0));
71
+ }
72
+ ```
73
+
74
+ ### Heavy event handler processing time
75
+ - Running too much synchronous JS inside `onclick`/`onkeydown` handlers
76
+ - Triggering expensive recalculations (style, layout) inside handlers
77
+
78
+ **Fix: defer non-essential work**
79
+
80
+ ```js
81
+ element.addEventListener('click', async (event) => {
82
+ // Do only what's needed for the immediate visual update
83
+ updateButtonState(event.target);
84
+
85
+ // Defer expensive analytics/processing until after the frame paints
86
+ await scheduler.yield();
87
+ sendAnalytics(event);
88
+ prefetchRelatedContent();
89
+ });
90
+ ```
91
+
92
+ ### Large rendering updates (presentation delay)
93
+ - Re-rendering a large DOM subtree unnecessarily
94
+ - Causing style recalculation on many elements
95
+ - JavaScript animations that force layout (reading `offsetWidth`, `getBoundingClientRect`)
96
+ then writing styles in a loop
97
+
98
+ **Fix: minimize DOM size and avoid layout thrashing**
99
+
100
+ ```js
101
+ // Layout thrashing (bad) — forces synchronous layout each iteration
102
+ elements.forEach(el => {
103
+ const height = el.offsetHeight; // forces layout
104
+ el.style.height = height * 2 + 'px'; // invalidates layout
105
+ });
106
+
107
+ // Batch reads then writes (good)
108
+ const heights = elements.map(el => el.offsetHeight); // one layout
109
+ elements.forEach((el, i) => { el.style.height = heights[i] * 2 + 'px'; });
110
+ ```
111
+
112
+ ## Avoid unnecessary JavaScript
113
+
114
+ - Code-split aggressively — defer JS not needed for initial interaction
115
+ - Remove unused polyfills and libraries
116
+ - Avoid loading third-party scripts that add long tasks during user sessions
117
+
118
+ ## Measure INP in JavaScript (raw API)
119
+
120
+ ```js
121
+ new PerformanceObserver((list) => {
122
+ for (const entry of list.getEntries()) {
123
+ if (entry.interactionId) {
124
+ console.log('Interaction:', entry.name, entry.duration, 'ms');
125
+ }
126
+ }
127
+ }).observe({ type: 'event', buffered: true, durationThreshold: 16 });
128
+ ```
129
+
130
+ Prefer `onINP()` from the `web-vitals` library — it correctly aggregates all
131
+ interactions, selects the worst, and handles attribution.
132
+
133
+ ## Key nuances
134
+
135
+ - INP is the **worst interaction** observed (excluding statistical outliers at
136
+ very high page load counts to reduce noise from accidental interactions)
137
+ - If a page has ≤ 50 interactions, INP = worst interaction
138
+ - If a page has > 50 interactions, the 98th percentile interaction is used
139
+ - Animations triggered by JS do not count as interactions
140
+ - INP is not measurable in lab tools (Lighthouse uses TBT as a proxy)
@@ -0,0 +1,89 @@
1
+ # LCP — Largest Contentful Paint
2
+
3
+ **Good:** ≤ 2.5 s | **Needs improvement:** 2.5–4.0 s | **Poor:** > 4.0 s
4
+ (measured at the 75th percentile of field loads)
5
+
6
+ ## What qualifies as the LCP element?
7
+
8
+ Only these element types are considered:
9
+ - `<img>` elements (including `<image>` inside SVG)
10
+ - `<video>` elements (poster image only)
11
+ - Block-level elements with a CSS `background-image` loaded via `url()`
12
+ - Block-level text elements (`<p>`, `<h1>`, etc.)
13
+
14
+ Elements excluded from LCP consideration: full-viewport background images,
15
+ `opacity: 0` elements, placeholder images, elements the browser heuristically
16
+ considers non-contentful.
17
+
18
+ LCP is the *last* candidate emitted before user interaction or page hide —
19
+ the browser continuously updates the candidate as larger elements paint.
20
+
21
+ ## LCP timing breakdown
22
+
23
+ LCP time = TTFB + resource load delay + resource load time + element render time
24
+
25
+ 1. **TTFB** — time until first byte of HTML arrives
26
+ 2. **Resource load delay** — time from TTFB until the LCP resource starts downloading
27
+ 3. **Resource load time** — how long the resource takes to download
28
+ 4. **Element render time** — time from download complete to paint on screen
29
+
30
+ Diagnosing which phase dominates tells you what to fix.
31
+
32
+ ## Common causes and fixes
33
+
34
+ ### Slow TTFB (phase 1)
35
+ - Use a CDN geographically close to users
36
+ - Enable server-side caching, edge caching, or stale-while-revalidate
37
+ - Reduce database query time / server processing time
38
+ - Target: TTFB ≤ 800 ms
39
+
40
+ ### LCP image not discoverable (phase 2)
41
+ - **Don't** hide the LCP image behind `data-src`, JS lazy-loading, or CSS
42
+ `background-image` in a stylesheet — the preload scanner can't find it
43
+ - **Do** use `<img src="...">` or `<img srcset="...">` in the HTML source
44
+ - **Do** use `<link rel="preload" as="image" href="...">` if the image must
45
+ come from CSS/JS
46
+ - **Do** prefer SSR/SSG over CSR — CSR blocks image discovery behind JS execution
47
+ - Remove `loading="lazy"` from the LCP image; lazy-loading delays it
48
+
49
+ ### LCP image deprioritized (phase 2)
50
+ ```html
51
+ <!-- Add fetchpriority="high" to your LCP image -->
52
+ <img src="/hero.jpg" fetchpriority="high" alt="..." width="1200" height="600">
53
+
54
+ <!-- Or on the preload link -->
55
+ <link rel="preload" as="image" href="/hero.jpg" fetchpriority="high">
56
+ ```
57
+
58
+ ### Slow resource download (phase 3)
59
+ - Compress images (use WebP or AVIF; target < 200 KB for hero images)
60
+ - Use `srcset` + `sizes` to serve appropriately sized images per viewport
61
+ - Serve from a CDN with HTTP/2 or HTTP/3
62
+ - Defer non-critical resources to reduce bandwidth contention
63
+
64
+ ### Render delay (phase 4)
65
+ - Minimize render-blocking scripts and stylesheets in `<head>`
66
+ - Avoid long tasks on the main thread that block painting
67
+ - If using a web font for LCP text: `font-display: optional` or preload the font
68
+
69
+ ## JavaScript API (raw, without web-vitals library)
70
+
71
+ ```js
72
+ new PerformanceObserver((list) => {
73
+ const entries = list.getEntries();
74
+ const last = entries[entries.length - 1];
75
+ console.log('LCP candidate:', last.startTime, last.element);
76
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
77
+ ```
78
+
79
+ Prefer `onLCP()` from the `web-vitals` library — it handles bfcache restores,
80
+ prerendered page activation, and iframe aggregation automatically.
81
+
82
+ ## Key nuances
83
+
84
+ - LCP from bfcache restores counts as a new page visit — measure it
85
+ - Images inside cross-origin iframes contribute to LCP but are not observable
86
+ from the parent frame via the raw API (web-vitals library handles this)
87
+ - For prerendered pages, measure LCP from `activationStart`, not navigation start
88
+ - The largest element is determined by rendered size in the viewport, not
89
+ intrinsic dimensions
@@ -0,0 +1,112 @@
1
+ # CWV Tools & Top Optimization Checklist
2
+
3
+ ## Field tools (real user data)
4
+
5
+ ### Chrome User Experience Report (CrUX)
6
+ - 28-day rolling window of anonymized Chrome user data
7
+ - Aggregated by origin and URL-pattern (template level)
8
+ - Requires sufficient traffic (threshold: ~100 qualifying visits/28 days)
9
+ - Access via: BigQuery, PageSpeed Insights API, CrUX API, Search Console
10
+
11
+ ### PageSpeed Insights (PSI)
12
+ - URL: https://pagespeed.web.dev/
13
+ - Shows CrUX field data (if available) + Lighthouse lab audit in one view
14
+ - API available for programmatic access:
15
+ ```
16
+ https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=<URL>&strategy=mobile
17
+ ```
18
+
19
+ ### Search Console — Core Web Vitals report
20
+ - Groups pages by URL template (not individual URLs)
21
+ - Shows "Good", "Needs improvement", "Poor" counts with 28-day CrUX data
22
+ - Best for finding which page groups have systematic CWV issues
23
+
24
+ ## Lab tools (simulated/local)
25
+
26
+ ### Lighthouse (Chrome DevTools / CLI / CI)
27
+ ```bash
28
+ npm install -g lighthouse
29
+ lighthouse https://example.com --output html --output-path ./report.html
30
+ ```
31
+ - Use `--throttling-method=simulate` for reproducible scores
32
+ - INP not measured in Lighthouse — use TBT (Total Blocking Time) as proxy
33
+ - Run in incognito to avoid extension interference
34
+
35
+ ### Chrome DevTools Performance panel
36
+ - Record interactions to identify long tasks and INP attribution
37
+ - Performance Insights panel: shows LCP, CLS, INP with timeline markers
38
+ - Rendering panel → "Layout Shift Regions" highlights shifting elements in real time
39
+
40
+ ### web-vitals Chrome extension
41
+ - Badge shows live LCP, INP, CLS values as you browse
42
+ - Console logs detailed attribution data
43
+ - Useful for quick spot-checks without instrumenting analytics
44
+
45
+ ## Top 9 optimizations (Chrome team recommendations)
46
+
47
+ ### INP
48
+ 1. **Yield often to break up long tasks** — Use `scheduler.yield()` or
49
+ `setTimeout(0)` to give the browser opportunities between work chunks.
50
+ Any JS task > 50 ms becomes a "long task" that blocks interaction handling.
51
+
52
+ 2. **Avoid unnecessary JavaScript** — Code-split, defer non-critical JS, remove
53
+ unused polyfills. Less JS = fewer long tasks = better INP.
54
+
55
+ 3. **Avoid large rendering updates** — Minimize DOM size, batch DOM writes,
56
+ use `requestAnimationFrame` for visual updates, avoid layout thrashing
57
+ (interleaved reads/writes of layout properties).
58
+
59
+ ### LCP
60
+ 4. **Make the LCP resource discoverable from HTML source and prioritized** —
61
+ Use `<img src>` or `<img srcset>` (not `data-src`). Add `fetchpriority="high"`
62
+ to the LCP image or its `<link rel="preload">`. Remove `loading="lazy"` from LCP.
63
+
64
+ 5. **Aim for instant navigations** — Implement the View Transitions API for
65
+ client-side navigations. Use speculation rules for prerendering next pages:
66
+ ```html
67
+ <script type="speculationrules">
68
+ { "prerender": [{ "where": { "href_matches": "/products/*" } }] }
69
+ </script>
70
+ ```
71
+ Use `rel="prefetch"` for likely next navigations.
72
+
73
+ 6. **Use a CDN to optimize TTFB** — Serve HTML from an edge CDN geographically
74
+ close to users. Edge compute (e.g., Cloudflare Workers, Vercel Edge) can
75
+ stream HTML early while fetching dynamic data in parallel.
76
+ Target TTFB ≤ 800 ms.
77
+
78
+ ### CLS
79
+ 7. **Set explicit sizes on content loaded from the page** — Add `width` and
80
+ `height` attributes to all `<img>` and `<video>` elements. Use CSS
81
+ `aspect-ratio` for responsive containers. Reserve space for ads and embeds
82
+ with `min-height`.
83
+
84
+ 8. **Ensure pages are eligible for bfcache** — Avoid `unload` listeners,
85
+ `Cache-Control: no-store`, and unclosed `IndexedDB` transactions.
86
+ bfcache restores eliminate load-time layout shifts entirely.
87
+
88
+ 9. **Avoid layout-inducing CSS animations/transitions** — Animate only
89
+ `transform` and `opacity`. These run on the compositor thread and do not
90
+ cause layout shifts. Never animate `top`, `left`, `margin`, `width`, etc.
91
+
92
+ ## Measuring in CI/CD
93
+
94
+ ```js
95
+ // Example: assert LCP in Playwright test
96
+ import { onLCP } from 'web-vitals';
97
+
98
+ test('LCP is under 2.5s', async ({ page }) => {
99
+ await page.goto('/');
100
+ const lcp = await page.evaluate(() => new Promise(resolve => {
101
+ onLCP(metric => resolve(metric.value), { reportAllChanges: false });
102
+ }));
103
+ expect(lcp).toBeLessThan(2500);
104
+ });
105
+ ```
106
+
107
+ For automated CWV regression testing use Lighthouse CI:
108
+ ```bash
109
+ npm install -g @lhci/cli
110
+ lhci autorun --collect.url=https://staging.example.com \
111
+ --assert.assertions.largest-contentful-paint=["warn", {"maxNumericValue": 2500}]
112
+ ```