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

8.9 KiB
Raw Permalink Blame History

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:

<link rel="preload" href="/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin>
@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:
@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:
<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">
  1. Reserve space: width + height attributes (or CSS aspect-ratio) on every image. Unreserved images are the top cause of CLS.
  2. Lazy-load below the fold; never lazy-load the LCP image. The hero image gets the opposite treatment:
<link rel="preload" as="image" href="/hero-1600.avif" fetchpriority="high">
  1. Hero/background images ≤ 200 KB after compression. If it can't compress, it should be CSS or SVG — see imagery.md.
  2. 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:
.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:

<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.