Files
wehub-resource-sync e011bb26cd
Lint skills / Validate catalog structure (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:36:55 +08:00

301 lines
9.1 KiB
Markdown

# Optimization Playbook
A step-by-step playbook for common performance problems. Pick the symptom, follow the steps.
---
## Symptom: LCP failing (>2.5s)
The largest content element on the page takes too long to paint.
### Step 1: Identify the LCP element
In Chrome DevTools Performance panel:
1. Record a page load
2. Find the LCP marker
3. Note which element is the LCP (usually a hero image or large heading)
### Step 2: Optimize that specific element
**If LCP element is an image:**
- Use modern format (WebP, AVIF)
- Serve appropriately sized version (don't ship a 4000px image to a 800px container)
- Preload it: `<link rel="preload" as="image" href="hero.webp" fetchpriority="high">`
- Avoid lazy-loading the LCP image (use eager loading)
- Use `fetchpriority="high"` on the image element
**If LCP element is text/heading:**
- Ensure web fonts don't block rendering (`font-display: swap`)
- Inline critical CSS for the heading section
- Preconnect to font origin: `<link rel="preconnect" href="https://fonts.example.com">`
**If LCP is loading slowly because the page is slow overall:**
- See "Symptom: TTFB slow" below
- Reduce render-blocking resources
- Reduce total payload before LCP element
### Step 3: Measure
Re-test in Lighthouse and DevTools. Verify LCP under 2.5s on a slow connection (Slow 4G simulation in DevTools).
---
## Symptom: INP failing (>200ms)
Interactions feel sluggish. The page takes too long to respond to user input.
### Step 1: Find the slow interactions
In Chrome DevTools Performance panel:
1. Record while clicking, typing, or interacting
2. Look for long tasks (orange bars >50ms) following input events
3. Identify the function calls that take the longest
### Step 2: Reduce the work
**For event handlers:**
- Move heavy work out of the synchronous path
- Use `setTimeout` or `requestIdleCallback` to yield to the main thread
- Debounce or throttle frequent events (scroll, mouse move, resize)
**For component re-renders (React, Vue, etc.):**
- Memoize expensive computations
- Avoid creating new objects/arrays/functions in render that get passed as props
- Use virtualization for large lists (react-window, virtua, or framework equivalent)
- Profile to find which components re-render and why
**For startup hydration (SSR/SSG frameworks):**
- Defer hydration of non-critical components
- Use island architecture or partial hydration where supported
- Code-split aggressively below-the-fold
### Step 3: Verify
INP should be under 200ms at the 75th percentile of real user data.
---
## Symptom: CLS failing (>0.1)
Content jumps around as the page loads.
### Step 1: Identify the shifts
In Chrome DevTools:
1. Open Performance Insights or Performance panel
2. Look for "Layout Shifts" markers
3. Click each to see what shifted
### Step 2: Reserve space for dynamic content
**Common offenders and fixes:**
- **Images without dimensions.** Add `width` and `height` attributes (or `aspect-ratio` CSS).
- **Late-loading ads or embeds.** Reserve a fixed-size container.
- **Web fonts swapping.** Use `font-display: optional` or size-adjust descriptors.
- **Skeleton loaders that don't match final size.** Match dimensions exactly.
- **Animations triggering layout.** Animate only `transform` and `opacity`.
- **Insertion of content above existing content.** Reserve space or insert below.
### Step 3: Verify
Run Lighthouse. Real-user CLS at 75th percentile should be under 0.1.
---
## Symptom: TTFB slow (>800ms)
Server takes too long to respond.
### Step 1: Check the server response
Use WebPageTest or `curl -w "@curl-format.txt" -s -o /dev/null https://example.com` to measure just TTFB.
### Step 2: Identify the bottleneck
**Common causes:**
- **No caching at the edge.** Add a CDN. Set Cache-Control headers correctly.
- **Slow database queries.** Run EXPLAIN on slow queries. Add indexes. Eliminate N+1 patterns.
- **Slow third-party API calls in the render path.** Defer to client or precompute.
- **Heavy server-side rendering.** Profile the SSR work. Memoize what's repeated.
- **Cold starts (serverless).** Provisioned concurrency or warming pings.
### Step 3: Verify
TTFB under 600ms at the 75th percentile is the target. Under 200ms is excellent.
---
## Symptom: Bundle size too large
JavaScript payload bloating the page.
### Step 1: Audit the bundle
Run a bundle analyzer (Webpack Bundle Analyzer, source-map-explorer, etc.). Sort by size.
### Step 2: Identify the offenders
**Common bloat sources:**
- **Unused dependencies.** Tree-shaking missing or broken.
- **Whole-library imports.** `import { debounce } from 'lodash'` pulls all lodash. Use `import debounce from 'lodash/debounce'`.
- **Polyfills shipped to modern browsers.** Use differential serving.
- **Source maps in production bundle.** Should be separate files, not inline.
- **Moment.js, Lodash, jQuery.** Often replaceable with smaller alternatives or native APIs.
- **Multiple versions of the same library.** Check `npm ls [library]`. Dedupe.
### Step 3: Reduce
- Replace heavy libraries with lighter alternatives (date-fns or dayjs instead of moment, native fetch instead of axios)
- Code-split routes
- Code-split heavy components (modals, charts, editors)
- Use dynamic imports for below-the-fold features
### Step 4: Set a budget
In your build config or CI, fail builds that exceed the budget.
---
## Symptom: Image-heavy page loads slowly
### Step 1: Audit images
- Count images on the page
- Note current sizes and formats
- Note which are above-the-fold (eagerly loaded) vs below (should be lazy)
### Step 2: Optimize each
**Format:**
- Photos: WebP or AVIF (with JPEG fallback)
- Graphics: SVG or PNG (with WebP fallback)
- Animations: video (MP4/WebM) instead of GIF for size
**Sizing:**
- Generate multiple sizes via `srcset`
- Use `sizes` attribute to tell the browser the rendered size
```html
<img
src="hero-800.webp"
srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1600.webp 1600w"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1600px"
width="1600"
height="900"
alt="Description"
loading="eager"
fetchpriority="high"
/>
```
For below-the-fold:
- `loading="lazy"`
- Without `fetchpriority`
**Compression:**
- Use a build-time image pipeline (sharp, imagemin) to compress automatically
- Quality 75-85 is usually invisible to users while halving file size
---
## Symptom: Web fonts blocking text
### Step 1: Audit font loading
In DevTools Network tab, filter by font. Note:
- File size
- When they load relative to first paint
- Whether text is invisible or fallback during load
### Step 2: Optimize
- **Use `font-display: swap`** to show fallback text immediately
- **Subset fonts** to only the characters needed (Latin only is much smaller than full Unicode)
- **Self-host critical fonts** instead of CDN where possible
- **Preload critical fonts:**
```html
<link rel="preload" href="/fonts/regular.woff2" as="font" type="font/woff2" crossorigin>
```
- **Use `size-adjust`, `ascent-override`, `descent-override`** in `@font-face` to match metrics with fallback fonts (reduces CLS)
---
## Symptom: Third-party scripts dragging performance
### Step 1: Inventory third-parties
In DevTools Network tab, list all third-party domains. For each:
- Size
- Blocking or non-blocking?
- Necessary?
### Step 2: Decide on each
For every third-party script, ask: does the business value justify the performance cost?
**Common offenders:**
- Heavy analytics (audit if all pixels are necessary; consider server-side tagging)
- Customer support chat widgets (load on user intent, not on page load)
- A/B testing tools (defer non-critical experiments)
- Multiple ad networks (consolidate)
- Tag managers loading hundreds of tags
**Fixes:**
- Remove unused tags
- Defer non-critical scripts (`async` or `defer`)
- Self-host where licensing allows (avoids extra DNS lookup)
- Load chat widgets on user interaction, not on page load
- Consolidate analytics where possible
### Step 3: Set a third-party budget
Number of third-party domains, total third-party JS payload. Block-add for new third-parties without explicit performance review.
---
## Performance budget template
Set hard limits in your CI to prevent regression:
```yaml
budget:
- resourceSizes:
- resourceType: total
budget: 500 # kilobytes
- resourceType: script
budget: 200
- resourceType: image
budget: 200
- resourceType: stylesheet
budget: 50
- resourceType: font
budget: 100
- resourceType: third-party
budget: 100
- timings:
- metric: largest-contentful-paint
budget: 2500 # milliseconds
- metric: cumulative-layout-shift
budget: 0.1
- metric: total-blocking-time
budget: 200
```
Tools like Lighthouse CI, Calibre, or SpeedCurve can enforce these budgets in CI.
---
## When to call it done
A page is "performance optimized" when:
1. Real-user Core Web Vitals at 75th percentile pass thresholds
2. Lighthouse performance score >90 on representative pages
3. Performance budgets enforced in CI
4. Periodic re-audit scheduled (quarterly minimum)
Performance is not a one-time project. New code adds weight. New features add complexity. Plan for ongoing maintenance.