Improve performance-optimization skill with clearer guidance

- Distinguish synthetic (Lighthouse) vs RUM (web-vitals) measurement approaches,
  clarifying when each is appropriate: synthetic for CI regression detection,
  RUM to validate real user impact.

- Expand TTFB diagnosis from a single vague hint into a decision tree that breaks
  down each component (DNS, TCP/TLS, server processing) with specific next steps.
  Mirrors the tree in the skill and adds a dedicated checklist section.

- Fix image optimization example: the previous "GOOD" example applied loading="lazy"
  without distinguishing the LCP hero image from below-the-fold images. Hero images
  must never be lazy-loaded. New example separates both cases explicitly.

- Add art direction + resolution switching to the hero image example using <picture>
  with media queries for mobile/desktop crops and srcset for density variants.
  Mobile-first: <img src> fallback points to the mobile version.
  Covers AVIF → WebP → JPG format cascade and fetchpriority="high" for LCP.

- Correct the date-fns tree-shaking example: modern bundlers (Vite, webpack 5+)
  handle named imports automatically. The "BAD" pattern was not actually bad,
  and following it could lead to unnecessary micro-optimizations. Real gains
  come from dynamic imports and route-level code splitting, which the example
  now illustrates instead.
This commit is contained in:
Joan Leon
2026-04-08 00:25:19 +02:00
parent 8d79b5f93d
commit c2c4f56d05
2 changed files with 70 additions and 16 deletions
+8
View File
@@ -18,6 +18,14 @@ Quick reference checklist for web application performance. Use alongside the `pe
| INP (Interaction to Next Paint) | ≤ 200ms | ≤ 500ms | > 500ms |
| CLS (Cumulative Layout Shift) | ≤ 0.1 | ≤ 0.25 | > 0.25 |
## TTFB Diagnosis
When TTFB is slow (> 600ms), check each component in DevTools Network waterfall:
- [ ] **DNS resolution** slow → add `<link rel="dns-prefetch">` or `<link rel="preconnect">` for known origins
- [ ] **TCP/TLS handshake** slow → enable HTTP/2, consider edge deployment, verify keep-alive
- [ ] **Server processing** slow → profile backend, check slow queries, add caching
## Frontend Checklist
### Images
+62 -16
View File
@@ -39,13 +39,18 @@ Measure before optimizing. Performance work without measurement is guessing —
### Step 1: Measure
Two complementary approaches — use both:
- **Synthetic (Lighthouse, DevTools Performance tab):** Controlled conditions, reproducible. Best for CI regression detection and isolating specific issues.
- **RUM (web-vitals library, CrUX):** Real user data in real conditions. Required to validate that a fix actually improved user experience.
**Frontend:**
```bash
# Lighthouse in Chrome DevTools (or CI)
# Synthetic: Lighthouse in Chrome DevTools (or CI)
# Chrome DevTools → Performance tab → Record
# Chrome DevTools MCP → Performance trace
# Web Vitals library in code
# RUM: Web Vitals library in code
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
@@ -73,7 +78,10 @@ Use the symptom to decide what to measure first:
What is slow?
├── First page load
│ ├── Large bundle? --> Measure bundle size, check code splitting
│ ├── Slow server response? --> Measure TTFB, check API/database
│ ├── Slow server response? --> Measure TTFB in DevTools Network waterfall
│ │ ├── DNS long? --> Add dns-prefetch / preconnect for known origins
│ │ ├── TCP/TLS long? --> Enable HTTP/2, check edge deployment, keep-alive
│ │ └── Waiting (server) long? --> Profile backend, check queries and caching
│ └── Render-blocking resources? --> Check network waterfall for CSS/JS blocking
├── Interaction feels sluggish
│ ├── UI freezes on click? --> Profile main thread, look for long tasks (>50ms)
@@ -144,18 +152,56 @@ const tasks = await db.tasks.findMany({
#### Missing Image Optimization (Frontend)
```html
<!-- BAD: No dimensions, no lazy loading, no responsive sizes -->
<!-- BAD: No dimensions, no format optimization -->
<img src="/hero.jpg" />
<!-- GOOD: Responsive, lazy-loaded, properly sized -->
<!-- GOOD: Hero / LCP image — art direction + resolution switching, high priority -->
<!--
Two techniques combined:
- Art direction (media): different crop/composition per breakpoint
- Resolution switching (srcset + sizes): right file size per screen density
-->
<picture>
<!-- Mobile: portrait crop -->
<source
media="(max-width: 767px)"
srcset="/hero-mobile-400.avif 400w, /hero-mobile-800.avif 800w"
sizes="100vw"
type="image/avif"
/>
<source
media="(max-width: 767px)"
srcset="/hero-mobile-400.webp 400w, /hero-mobile-800.webp 800w"
sizes="100vw"
type="image/webp"
/>
<!-- Desktop: landscape crop -->
<source
srcset="/hero-800.avif 800w, /hero-1200.avif 1200w, /hero-1600.avif 1600w"
sizes="100vw"
type="image/avif"
/>
<source
srcset="/hero-800.webp 800w, /hero-1200.webp 1200w, /hero-1600.webp 1600w"
sizes="100vw"
type="image/webp"
/>
<img
src="/hero-mobile.jpg"
width="1200"
height="600"
fetchpriority="high"
alt="Hero image description"
/>
</picture>
<!-- GOOD: Below-the-fold image — lazy loaded -->
<img
src="/hero.jpg"
srcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w"
sizes="(max-width: 768px) 100vw, 50vw"
width="1200"
height="600"
src="/content.webp"
width="800"
height="400"
loading="lazy"
alt="Hero image description"
alt="Content image description"
/>
```
@@ -188,14 +234,14 @@ function TaskStats({ tasks }: Props) {
#### Large Bundle Size
```typescript
// BAD: Importing entire library
import { format } from 'date-fns';
// GOOD: Tree-shakable import (if the library supports it)
import { format } from 'date-fns/format';
// Modern bundlers (Vite, webpack 5+) handle named imports with tree-shaking automatically.
// Profile before changing import styles — the real gains come from splitting and lazy loading.
// GOOD: Dynamic import for heavy, rarely-used features
const ChartLibrary = lazy(() => import('./ChartLibrary'));
// GOOD: Route-level code splitting
const SettingsPage = lazy(() => import('./pages/Settings'));
```
#### Missing Caching (Backend)