hermes-hub/.agents/skills/frontend-design/performance.md

210 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Performance — Speed Is Design
> A beautiful page that loads in 5 seconds reads as broken. Speed is not engineering garnish — it is part of the aesthetic. Quiet, fast, immediate: the same words that describe good design describe good performance. Agents over-ship: three fonts, a framework, and a chat widget for a page that could be HTML and 40KB of CSS. This file is the counterweight.
---
## Budgets (decide before building)
| Metric | Budget | Why |
|---|---|---|
| **LCP** | < 2.5s (mobile, 4G throttled) | The "is this page real?" moment |
| **INP** | < 200ms | Interaction feels instant, not sluggish |
| **CLS** | < 0.1 | Nothing jumps while reading |
| Page weight marketing page | < 1 MB, and < 300 KB on the wire critical path | Respect the visitor |
| Page weight content page | < 500 KB | Text is cheap; bloat is chosen |
| Fonts | 2 families, 4 files total, ~300 KB | See below |
| JS mostly-static page | 50 KB, or **none** | If CSS can do it, CSS does it |
If a requirement breaks the budget, say so and cut the requirement don't ship the slow version silently.
---
## Fonts (the #1 agent-made slowdown)
The full setup is in `typography.md` §Loading Fonts. The floor:
```html
<link rel="preload" href="/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin>
```
```css
@font-face {
font-family: 'Inter';
src: url('/fonts/InterVariable.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: swap;
unicode-range: U+0000-00FF; /* subset to what you actually use */
}
```
Rules:
- **Variable font > family of static weights.** One file, every weight.
- Load **woff2 only.** No ttf, no eot, no woff fallback chain from 2015.
- `font-display: swap` (or `optional` for non-critical faces) — invisible text is a broken page.
- Preload **only** the display face used above the fold. Preloading everything defeats preloading.
- Google Fonts is acceptable for demos; self-host for production — privacy, one fewer origin, no third-party CSS chain.
- **Fallback metrics** kill the swap "jump" (`size-adjust`, `ascent-override`) — CLS goes to near zero:
```css
@font-face {
font-family: 'Inter-fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
}
```
---
## Images
Agents love full-bleed PNGs. Kill them:
1. **Format:** AVIF > WebP > JPEG. PNG only for flat graphics that SVG can't do.
2. **Responsive:** every content image ships `srcset` + `sizes`:
```html
<img src="/work/cover-800.avif"
srcset="/work/cover-400.avif 400w, /work/cover-800.avif 800w, /work/cover-1600.avif 1600w"
sizes="(max-width: 768px) 100vw, 50vw"
width="800" height="533"
alt="Halftone studio — shelving system installed for Mira Almeida, Lisbon"
loading="lazy" decoding="async">
```
3. **Reserve space:** `width` + `height` attributes (or CSS `aspect-ratio`) on **every** image. Unreserved images are the top cause of CLS.
4. **Lazy-load below the fold; never lazy-load the LCP image.** The hero image gets the opposite treatment:
```html
<link rel="preload" as="image" href="/hero-1600.avif" fetchpriority="high">
```
5. Hero/background images ≤ 200 KB after compression. If it can't compress, it should be CSS or SVG — see `imagery.md`.
6. `prefers-reduced-data` exists; treat giant decorative media as optional, not mandatory.
---
## CSS
- **One stylesheet** for a marketing page, hand-written, token-driven (`color.md`, `layout.md`). It will be smaller than any utility purge.
- No `@import` chains (serialized downloads). `<link rel="stylesheet">` in `head`, once.
- The examples in `examples/` embed CSS in a single HTML file for portability. **In production, split it out** — page cacheability matters from visitor two onward.
- Critical CSS is a last resort for heavy pages, not a default. A 30KB stylesheet doesn't need inlining logic.
- `content-visibility: auto` on long below-the-fold sections is free render speed:
```css
.section { content-visibility: auto; contain-intrinsic-size: auto 600px; }
```
---
## JavaScript — Ship None If You Can
Ask in order:
1. **Does this need JS at all?** Menus (`<details>`), accordions (`<details>`), carousels (scroll-snap), tabs (radio inputs), dialogs (`<dialog>`), theme toggle (no — server/inline), hover reveals (CSS).
2. If yes — **progressive enhancement**: the content works with JS disabled, JS upgrades it.
3. If a framework is already justified by the brief (real app state, product UI), fine — but a landing page in a SPA is slop with extra steps.
Rules when JS is used:
```html
<script type="module" src="/app.js"></script> <!-- module = deferred by default -->
```
- `defer` / `async` / `type="module"` — never a blocking `<script>` in `head`.
- One file beats five on first load; five beat one after first visit (cache). For demos: one.
- No spinner for operations under 300ms — see perceived performance below.
- Event handlers on scroll/input: `passive: true` where you don't `preventDefault()`; debounce real work.
- No JS "framework CDN + await hydration" for a static page. HTML is already interactive.
---
## Third Parties (the silent budget killers)
| Third party | Real cost | Decision |
|---|---|---|
| Chat widget | 300 KB1.5 MB, main-thread | Marketing page: a link to email/open chat. Never autoload |
| Analytics | 10100 KB | One script, deferred, or server-side |
| Font CDN | Extra origin + CSS chain | Self-host in production |
| Map embed | 1 MB+ | Screenshot + link, or static map tiles |
| Video embed | 1 MB+ on "view" | Facade: poster image + click-to-load |
| A/B tool | Blocking script | Question the tool |
Every third-party script is a budget decision. Add one = remove weight somewhere else.
---
## Core Web Vitals in Practice
**LCP** — usually the hero headline or hero image.
- Nothing blocking it: fonts preloaded, hero image `fetchpriority="high"`, no render-blocking JS.
- No lazy-load, no `display:none` at mobile then swap.
**CLS** — movement after render.
- Every image/video/iframe has reserved dimensions.
- Fonts: `font-display: swap` + fallback metrics (above).
- No banners/modals injected on load. Nothing slides in from the top.
**INP** — interaction latency.
- Handlers do one small thing; heavy work is chunked (`requestIdleCallback`) or in a worker.
- Debounce input-driven recalculation; don't re-render lists on every keystroke past what's visible.
- Animations stay on `transform`/`opacity` (`motion.md` §Performance) so the main thread is free.
---
## Perceived Performance (the design half)
- **< 100ms:** feels instant do the thing, show nothing.
- **100300ms:** still instant no spinner needed.
- **300ms1s:** show *something real*: skeleton of actual layout, button "Working…" state.
- **> 1s:** progress with meaning (steps, not a liar's progress bar); keep the page usable.
- Skeletons must **match final layout** (`components.md` §States) — wrong-shaped skeletons cause their own CLS.
- Optimistic UI for reversible actions (toggle on immediately, reconcile after).
---
## Measuring (never guess)
1. **Lighthouse** (DevTools, mobile, throttled) — LCP/INP/CLS + the page-weight waterfall.
2. **PageSpeed Insights** — lab + real-user field data when available.
3. **WebPageTest** — 4G Moto G profile for the honest truth.
4. DevTools Network tab, "Disable cache," throttled — count requests and KB **before** being told to.
The examples in `examples/` should each score 95+ on Performance/Best-Practices out of the box. If a change drops it below 90, the change needs a reason.
---
## Performance Anti-Patterns
| ❌ Don't | ✅ Do |
|---|---|
| React/Vue SPA for a static landing page | HTML + CSS, JS where it earns its bytes |
| Blocking `<script>` in `<head>` | `type="module"` / `defer` |
| Full-bleed 3 MB PNG hero | Compressed AVIF/WebP ≤ 200 KB, or CSS/SVG composition |
| Lazy-loading the hero image | Preload + `fetchpriority="high"` |
| Images without `width`/`height` | Dimensions or `aspect-ratio`, always |
| Nine font files in four families | ≤ 2 families, variable, woff2, subset |
| `@import`-chained CSS | One `<link>` stylesheet |
| Spinner for a 150ms action | Nothing — it's already done |
| Chat widget autoloading on a landing page | Link; load on intent |
| Tracking pixels accumulated "just in case" | One deferred analytics script |
| Page "works" only after hydration | Progressive enhancement |
| Deciding speed is "later, optimization" | Budgets are decided before building |
---
## Ship Gate
- [ ] LCP < 2.5s, CLS < 0.1, INP < 200ms (throttled mobile)
- [ ] Total transfer < budget (1 MB marketing / 500 KB content)
- [ ] 4 font files, all woff2, swap + fallback metrics
- [ ] Every image: format, srcset, dimensions, correct loading strategy
- [ ] No blocking JS; JS justified per feature
- [ ] Third parties enumerated and costed
- [ ] Tested on throttled 4G, not just the dev machine
See also: `typography.md` §Loading Fonts, `imagery.md` (cheaper visuals), `motion.md` §Performance, `checklist.md` §Edge Cases.