@ctxr/skill-frontend-excellence 0.1.1

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.
@@ -0,0 +1,242 @@
1
+ # Lighthouse Mastery
2
+
3
+ A complete playbook for hitting and holding the top of the Lighthouse curve. Framework-agnostic. Every audit listed below is reachable from `lighthouse --output=json` or the Chrome DevTools "Lighthouse" panel.
4
+
5
+ ## Score Targets (recap)
6
+
7
+ | Category | Performance (mobile) | Performance (desktop) | A11y | Best Practices | SEO |
8
+ |----------|---------------------|----------------------|------|----------------|-----|
9
+ | Universal target | >= 95 | >= 99 | 100 | 100 | 100 (or n/a if `noindex`) |
10
+
11
+ A score of 99 hides one or two regressions; only 100 means "no audits failed". For Best Practices, Accessibility, and SEO, demand 100 unless an audit is genuinely inapplicable. For Performance, hold the bar at 95 mobile and 99 desktop. A given surface may consciously relax the Performance bar (e.g., a heavy interactive tool that legitimately ships more JS), but the relaxation should be a recorded, justified exception, not a default.
12
+
13
+ ## How Lighthouse Computes Performance
14
+
15
+ The performance score is a weighted sum of the lab metrics, scaled against the HTTPArchive distribution of the public web. The current weights (Lighthouse 11+):
16
+
17
+ | Metric | Weight | What it measures |
18
+ |--------|-------:|------------------|
19
+ | Largest Contentful Paint (LCP) | 25% | When the largest visible element finished painting |
20
+ | Total Blocking Time (TBT) | 30% | Time the main thread was blocked by long tasks during load |
21
+ | Cumulative Layout Shift (CLS) | 25% | Total visible layout shift during the lifetime of the page view |
22
+ | First Contentful Paint (FCP) | 10% | When the first text or image painted |
23
+ | Speed Index (SI) | 10% | How quickly content visually populated |
24
+
25
+ Implications:
26
+
27
+ - TBT (30%) is the single largest lever. Reducing main-thread time during load yields the largest score gains.
28
+ - LCP and CLS together are 50% of the score. The LCP element and any late-arriving content are the two highest-leverage targets.
29
+ - FCP and SI together are only 20%. Optimizing them without fixing TBT/LCP/CLS will plateau.
30
+ - INP is reported but does not currently affect the lab Performance score. It does affect real-user CrUX data and Search Console.
31
+
32
+ ## Run Lighthouse Properly
33
+
34
+ The most common cause of "Lighthouse keeps failing" is running it wrong.
35
+
36
+ ### Always run against a production build
37
+
38
+ Dev servers ship unminified, unoptimized assets and dev-only React (or equivalent) instrumentation. A dev-mode Lighthouse run is not informative.
39
+
40
+ ```
41
+ # Generic pattern
42
+ <framework-build-command>
43
+ <framework-start-prod-server> --port 3001 &
44
+ npx lighthouse "http://localhost:3001/<path>" \
45
+ --output=json --output=html \
46
+ --output-path=/tmp/lh-mobile \
47
+ --chrome-flags="--headless=new --no-sandbox --disable-gpu" \
48
+ --only-categories=performance,accessibility,best-practices,seo \
49
+ --quiet
50
+ ```
51
+
52
+ ### Always run mobile and desktop independently
53
+
54
+ ```
55
+ # Mobile (default preset: simulated 4G, Moto G Power, 5.6x CPU throttle)
56
+ npx lighthouse "http://localhost:3001/<path>" --output=json --output-path=/tmp/lh-mob.json \
57
+ --chrome-flags="--headless=new --no-sandbox --disable-gpu" --quiet
58
+
59
+ # Desktop (preset: native viewport, 0x throttle for CPU, 40ms RTT)
60
+ npx lighthouse "http://localhost:3001/<path>" --output=json --output-path=/tmp/lh-desk.json \
61
+ --preset=desktop --chrome-flags="--headless=new --no-sandbox --disable-gpu" --quiet
62
+ ```
63
+
64
+ Mobile uses 4x CPU throttle and a slow 4G network profile. Most regressions show up here first. Desktop uses no throttle.
65
+
66
+ ### Pin Chrome for reproducibility
67
+
68
+ Local Chrome updates can shift scores by 2-5 points. For CI, use a pinned Chrome (`@puppeteer/browsers` or Playwright's bundled Chromium). Set `CHROME_PATH` explicitly:
69
+
70
+ ```
71
+ export CHROME_PATH="$(find ~/Library/Caches/ms-playwright -name 'Google Chrome for Testing' -type f 2>/dev/null | head -1)"
72
+ ```
73
+
74
+ ### Run multiple times and take the median
75
+
76
+ Lighthouse has natural variance of 2-4 points per run. For any decision, run at least 3 times and use the median. Lighthouse CI's `numberOfRuns: 3` (or 5) is the standard.
77
+
78
+ ### Check from a real cold cache
79
+
80
+ Pass `--throttling-method=devtools` (or `provided`) only when comparing against real network conditions. The default `simulated` mode is fine for relative regression checks.
81
+
82
+ ## Failing Audit -> Fix Map
83
+
84
+ The following table maps every common failing Lighthouse audit to the underlying cause and the concrete fix. Items are grouped by category and ordered by frequency.
85
+
86
+ ### Performance audits
87
+
88
+ | Audit (id) | Symptom | Root cause | Fix |
89
+ |-----------|---------|------------|-----|
90
+ | `largest-contentful-paint-element` | LCP element is slow | The biggest element in the viewport is text or an image that loads late | Identify it. If image: add `fetchpriority="high"`, declare `width`/`height`, serve responsive `srcset`, prefer AVIF/WebP, never lazy-load it. If text: ensure the font that renders it loads with `font-display: swap` and is preloaded. Pre-render the section server-side. |
91
+ | `render-blocking-resources` | Stylesheet or script blocks first paint | Synchronous `<link rel="stylesheet">` or `<script>` in head | Inline critical CSS (under 14 KB), defer the rest with `<link rel="preload" as="style" onload="this.rel='stylesheet'">` or `media="print" onload="this.media='all'"`. Move scripts to `defer` or `async`. |
92
+ | `unused-css-rules` | > 20 KB unused CSS | Global stylesheet, full Tailwind preflight, or vendor framework CSS | Run a production CSS purge (Tailwind/UnoCSS purge, PurgeCSS, css-modules tree-shake). Split CSS by route. Inline only above-the-fold rules. |
93
+ | `unused-javascript` | > 40 KB unused JS | Imported library where only one function is used; whole-module imports | Use named imports, replace heavy libs with native APIs (date-fns -> Intl.DateTimeFormat, lodash -> ES built-ins). Audit with `source-map-explorer` or `webpack-bundle-analyzer`. |
94
+ | `total-byte-weight` | Page exceeds ~1.6 MB total | Unoptimized images, unused JS, untreeshaken vendor | Compress images (AVIF/WebP), code-split, treeshake, drop polyfills for evergreen browsers (`browserslist: ['>0.5%', 'not dead', 'not op_mini all']`). |
95
+ | `efficient-animated-content` | GIFs detected | A GIF is animated and unoptimized | Replace with `<video autoplay muted loop playsinline preload="metadata">` serving WebM + MP4, or with WebP/AVIF animated. |
96
+ | `modern-image-formats` | JPEGs/PNGs served | Asset pipeline emits legacy formats | Add an AVIF/WebP step. Serve via `<picture>` with `<source type="image/avif">`, `<source type="image/webp">`, then `<img>` fallback. |
97
+ | `uses-optimized-images` | Images larger than necessary | No compression at the build step | Run lossy compression (mozjpeg quality 75-82, oxipng lossy, AVIF quality 50-65). Strip metadata. |
98
+ | `uses-responsive-images` | Image larger than its rendered box | One size for all viewports | Add `srcset` with at least 3 widths (e.g., 480w, 960w, 1440w) and an explicit `sizes` attribute. |
99
+ | `offscreen-images` | Below-the-fold images load eagerly | Missing `loading="lazy"` | Add `loading="lazy"` to all below-the-fold `<img>`. Never lazy-load the LCP element. |
100
+ | `uses-text-compression` | Text assets uncompressed | Server not negotiating compression | Enable Brotli at the edge (CDN), fallback to gzip. Verify with `curl -H "Accept-Encoding: br,gzip" -I`. |
101
+ | `uses-rel-preload` | Critical request discovered late | Late-discovered font, hero image, or critical script | Add `<link rel="preload" as="font" type="font/woff2" crossorigin>` or `as="image"` for the LCP image. Limit preloads to genuinely critical resources. |
102
+ | `font-display` | Invisible text during font load | `font-display` not set, defaulting to `block` | Set `font-display: swap` on body fonts and `font-display: optional` on display fonts when LCP risk exists. |
103
+ | `uses-long-cache-ttl` | Static assets cached < 1 year | CDN/edge cache headers too short | Static immutable assets get `Cache-Control: public, max-age=31536000, immutable`. HTML gets `s-maxage=300, stale-while-revalidate=86400` or similar. |
104
+ | `legacy-javascript` | Polyfills for evergreen browsers | Babel preset shipping ES5 to modern browsers | Set `target: 'es2020'` or higher. Use module/nomodule pattern only if you actively support legacy. |
105
+ | `bootup-time` | JS execution > 2s on mobile | Heavy hydration, large initial bundle, sync work in main bundle | Code-split, defer non-critical hydration (islands architecture, lazy hydration), move heavy work to web workers. |
106
+ | `mainthread-work-breakdown` | Long tasks > 50ms | Single bundle parsing, heavy framework init, sync layout reads | Split bundles, defer hydration, batch reads/writes, move to `requestIdleCallback`. |
107
+ | `dom-size` | DOM > 1500 nodes | Mega-pages, virtualized lists not virtualized, ad/embed bloat | Virtualize lists with 50+ items. Lazy-render off-screen sections. Strip dead containers. |
108
+ | `third-party-summary` | Third parties take > 250ms main thread | Ads, analytics, chat widgets, embeds | Audit and remove. Lazy-load chat (`requestIdleCallback`). Use first-party proxy for analytics where possible. Use `partytown` to move third-party JS to a worker. |
109
+ | `cumulative-layout-shift` | CLS > 0.1 | Images/iframes without dimensions, late-loaded fonts, late-injected content (banners, ads) | Declare width/height on every `<img>`, `<iframe>`, `<video>`. Reserve space with `aspect-ratio`. Use `font-display: optional` + `size-adjust` to prevent FOUT shifts. Never inject above-the-fold content after first paint. |
110
+ | `non-composited-animations` | Animation triggered layout/paint | Animating `width`, `height`, `top`, `left`, `margin` | Refactor to `transform` (`translate3d`, `scale`) and `opacity`. |
111
+ | `uses-passive-event-listeners` | Touch/wheel listeners block scroll | `addEventListener('touchstart', fn)` without `{ passive: true }` | Add `{ passive: true }` to scroll-side listeners. |
112
+ | `no-document-write` | `document.write` detected | Legacy third party | Remove. Replace with async DOM manipulation. |
113
+
114
+ ### Accessibility audits
115
+
116
+ Lighthouse runs the axe-core ruleset. Failing any rule drops the score below 100. The score is binary on most rules.
117
+
118
+ | Audit | Root cause | Fix |
119
+ |-------|-----------|-----|
120
+ | `color-contrast` | Foreground/background ratio below 4.5:1 (or 3:1 for large) | Darken foreground or lighten/darken background. Verify with `getComputedStyle` ratio in DevTools. Test light AND dark modes independently. |
121
+ | `image-alt` | `<img>` without `alt` | Add `alt="..."` describing the image. Decorative images get `alt=""` (empty string, not missing). |
122
+ | `label` | Form control without label | Wrap with `<label>` or use `for`/`id` association. `aria-label` is acceptable when a visible label is genuinely impossible. |
123
+ | `link-name` | Link with no discernible name | Provide visible text or `aria-label`. Icon-only links must have `aria-label`. |
124
+ | `button-name` | Button with no discernible name | Same as link-name. |
125
+ | `aria-required-attr` / `aria-valid-attr-value` | Invalid ARIA usage | Validate against the ARIA 1.2 spec. Prefer native semantics over ARIA. |
126
+ | `heading-order` | Skipped heading level | Use sequential h1 -> h2 -> h3. Never use a heading for styling. |
127
+ | `html-has-lang` | Missing `<html lang="...">` | Set the document language. |
128
+ | `html-lang-valid` | Invalid lang code | Use a valid BCP 47 code (`en`, `en-US`, `de-DE`). |
129
+ | `meta-viewport` | Missing or zoom-disabled viewport | `<meta name="viewport" content="width=device-width, initial-scale=1">`. Never use `user-scalable=no` or `maximum-scale=1`. |
130
+ | `tabindex` | Positive tabindex (`tabindex="3"`) | Use natural document order. `tabindex="0"` and `tabindex="-1"` are fine. Positive values break expected tab flow. |
131
+ | `duplicate-id` | Two elements share the same `id` | IDs must be unique. |
132
+ | `bypass` | No skip link | Add `<a href="#main" class="sr-only-focusable">Skip to content</a>` as the first focusable element. |
133
+ | `aria-hidden-focus` | Focusable element inside `aria-hidden` | Don't focus into hidden subtrees. Move focus or remove `aria-hidden`. |
134
+ | `frame-title` | `<iframe>` without `title` | Provide `title` describing the frame's purpose. |
135
+ | `list` | `<ul>`/`<ol>` containing non-`<li>` children | Restructure markup. |
136
+ | `td-headers-attr`, `th-has-data-cells` | Data table without proper header association | Use `<th scope="col">` and `<th scope="row">`. For complex tables, use `headers="..."`. |
137
+
138
+ ### Best Practices audits
139
+
140
+ | Audit | Cause | Fix |
141
+ |-------|-------|-----|
142
+ | `is-on-https` | Page or any sub-resource over HTTP | Force HTTPS at the edge with HSTS. Audit mixed content (`https://` page loading `http://` sub-resources). |
143
+ | `errors-in-console` | JS errors at load | Fix the errors. Lighthouse fails on any console error. |
144
+ | `image-aspect-ratio` | Image rendered at distorted aspect ratio | Honor the natural aspect ratio or use `object-fit: cover/contain`. |
145
+ | `image-size-responsive` | Image natural size much smaller than rendered | Serve a higher-resolution source. |
146
+ | `notification-on-start` | Notification permission requested without user gesture | Move to a user-initiated trigger. |
147
+ | `geolocation-on-start` | Geolocation requested without user gesture | Same. |
148
+ | `paste-preventing-inputs` | `onpaste="return false"` | Remove. Users must be able to paste. |
149
+ | `inspector-issues` | DevTools-flagged issues | Open Issues panel, fix each. |
150
+ | `csp-xss` | Missing or weak CSP | Add a Content-Security-Policy with `default-src`, `script-src`, `style-src`, `img-src`, `connect-src`. Use nonces for inline. |
151
+ | `valid-source-maps` | Source maps not served or not valid | Serve `.map` files for first-party JS to aid debugging (consider whether this exposes intellectual property). |
152
+ | `no-unload-listeners` | `unload` event listener | Replace with `pagehide` or `visibilitychange`. |
153
+ | `deprecations` | Deprecated API used | Replace per the deprecation message. |
154
+
155
+ ### SEO audits
156
+
157
+ | Audit | Cause | Fix |
158
+ |-------|-------|-----|
159
+ | `document-title` | Missing `<title>` | Add a unique, intent-matching title (50-60 chars). |
160
+ | `meta-description` | Missing `<meta name="description">` | Add a unique 140-160 char description with the primary intent term and a value proposition. |
161
+ | `http-status-code` | Non-200 status | Fix the route. |
162
+ | `link-text` | Generic link text ("click here", "read more") | Replace with descriptive anchor text. |
163
+ | `crawlable-anchors` | Anchors with `javascript:void(0)` or no `href` | Use real `href` values. SPAs should still emit href on link components. |
164
+ | `is-crawlable` | `noindex` or robots block | Remove the block on indexable pages. |
165
+ | `robots-txt` | robots.txt malformed or unreachable | Validate, ensure it returns 200 and references the sitemap. |
166
+ | `image-alt` | Missing alt | See accessibility. |
167
+ | `hreflang` | Invalid hreflang | Use valid codes; ensure mutual hreflang on all locale variants. |
168
+ | `canonical` | Missing or incorrect canonical | Self-referencing canonical on the indexable URL. |
169
+ | `structured-data` | (manual) | Validate via Rich Results Test; add JSON-LD per the schema.org type that matches the page. |
170
+
171
+ ## Setting Up CI Gates
172
+
173
+ Lighthouse CI (`@lhci/cli`) is the standard way to enforce thresholds in CI. A minimal `lighthouserc.json`:
174
+
175
+ ```
176
+ {
177
+ "ci": {
178
+ "collect": {
179
+ "startServerCommand": "npm run start -- -p 3001",
180
+ "url": [
181
+ "http://localhost:3001/",
182
+ "http://localhost:3001/<key-route-1>",
183
+ "http://localhost:3001/<key-route-2>"
184
+ ],
185
+ "numberOfRuns": 3,
186
+ "settings": { "preset": "desktop" }
187
+ },
188
+ "assert": {
189
+ "assertions": {
190
+ "categories:performance": ["error", { "minScore": 0.99 }],
191
+ "categories:accessibility": ["error", { "minScore": 1.00 }],
192
+ "categories:best-practices": ["error", { "minScore": 1.00 }],
193
+ "categories:seo": ["error", { "minScore": 1.00 }],
194
+ "largest-contentful-paint": ["error", { "maxNumericValue": 2000 }],
195
+ "cumulative-layout-shift": ["error", { "maxNumericValue": 0.05 }],
196
+ "total-blocking-time": ["error", { "maxNumericValue": 100 }]
197
+ }
198
+ },
199
+ "upload": { "target": "temporary-public-storage" }
200
+ }
201
+ }
202
+ ```
203
+
204
+ Run a separate mobile config with `preset: 'mobile'` (default) and lower thresholds (0.95 perf). Run both as required CI gates.
205
+
206
+ ## Diagnosing Score Drops
207
+
208
+ When Lighthouse drops 5+ points between runs:
209
+
210
+ 1. Open the new and old reports side by side.
211
+ 2. Compare each metric numerically. Identify the metric that moved.
212
+ 3. For LCP: check the LCP element id. Did it change? Did the asset get bigger?
213
+ 4. For TBT: check the long-tasks panel in the Performance trace. Identify the new long task by its source URL.
214
+ 5. For CLS: check the "Layout Instability" filmstrip. Identify the shifting element.
215
+ 6. For FCP: check whether new render-blocking resources were added.
216
+
217
+ Variance happens. A 2-3 point swing is noise. A 5+ point swing is a regression.
218
+
219
+ ## Field vs Lab
220
+
221
+ Lighthouse runs in a controlled lab. Real users see different results (CrUX, Search Console). Always cross-reference:
222
+
223
+ - Lighthouse (lab): catches regressions before deploy.
224
+ - CrUX (field): real user 28-day p75. This is what Search Console uses for ranking signals.
225
+ - Web Vitals JS (`web-vitals` library): real-time field telemetry, sent to your own pipeline.
226
+
227
+ Pass the lab AND the field. INP especially shows up in the field but not the lab; instrument it in production.
228
+
229
+ ## Common Misconceptions
230
+
231
+ - "Lighthouse desktop is always 100." False. Heavy hydration, oversized images, and broken meta still tank desktop scores.
232
+ - "We can fix it later." Performance debt compounds. Add a budget guard now.
233
+ - "It's only a public-facing surface." Public-facing surfaces are the entry point for organic search and shape first impressions. They directly affect CrUX and rankings.
234
+ - "We minified, we're fine." Minification reduces bytes by ~30%. The bigger lever is what you ship at all (treeshake, code-split, defer hydration).
235
+ - "We have a CDN, that's enough." A CDN moves bytes faster; it does not parse, hydrate, or render faster. The client work is the bottleneck.
236
+
237
+ ## See Also
238
+
239
+ - [performance.md](performance.md) for the deep dive on each performance lever
240
+ - [accessibility.md](accessibility.md) for the deep dive on accessibility audits
241
+ - [seo.md](seo.md) for the deep dive on SEO audits
242
+ - [pre-launch.md](pre-launch.md) for the final verification checklist