Performance — Rules and Conventions
1. Philosophy
- User-centric metrics — Optimize for Core Web Vitals (LCP, INP, CLS), not synthetic scores.
- Lab + Field — Lab for regression detection, field (RUM) for real-user impact.
- Budget-driven — Set performance budgets. CI fails if exceeded.
- Progressive enhancement — Core content works without JS. Enhance progressively.
- Measure before optimize — Profile first. Guesswork wastes time.
2. Minimum Versions
| Technology | Minimum Version |
|---|---|
| Node.js | 22+ |
| pnpm | 11+ |
| Chrome | 120+ (DevTools) |
| Lighthouse | 11+ |
3. Measurement: Lab vs Field
| Aspect | Lab (Synthetic) | Field (RUM) |
|---|---|---|
| Tools | Lighthouse, WebPageTest, Chrome DevTools | Chrome UX Report (CrUX), web-vitals library |
| Environment | Controlled (throttled CPU/network) | Real users, real devices, real networks |
| Use case | CI regression, PR comparison | Business impact, prioritization |
| Frequency | Every PR | Continuous |
web-vitals (field)
pnpm add web-vitals
// analytics.js
import { onCLS, onINP, onLCP, onFCP, onTTFB } from "web-vitals";
function sendToAnalytics(metric) {
fetch("/analytics", {
method: "POST",
body: JSON.stringify(metric),
keepalive: true,
});
}
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
4. Core Web Vitals
| Metric | Good | Needs Improvement | Poor | Measures |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | ≤ 2.5s | 2.5–4s | > 4s | Loading |
| INP (Interaction to Next Paint) | ≤ 200ms | 200–500ms | > 500ms | Interactivity |
| CLS (Cumulative Layout Shift) | ≤ 0.1 | 0.1–0.25 | > 0.25 | Visual stability |
LCP optimization
<!-- Preload LCP image -->
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high" />
<!-- Or prioritize in HTML -->
<img
src="/hero.avif"
alt="..."
fetchpriority="high"
width="1200"
height="600"
/>
INP optimization
- Reduce main thread work — code splitting, web workers
- Avoid long tasks — yield with
scheduler.yield()orsetTimeout - Optimize event handlers — debounce, passive listeners
CLS prevention
/* Reserve space for images/ads/embeds */
img,
video,
iframe {
aspect-ratio: 16 / 9;
}
.ad-slot {
min-height: 250px;
}
/* Font loading */
@font-face {
font-display: swap;
}
5. Critical Rendering Path
Blocking resources
<head>
<!-- Critical CSS inline -->
<style>
/* critical.css */
</style>
<!-- Preload non-critical CSS -->
<link
rel="preload"
href="/styles.css"
as="style"
/>
<!-- Defer JS -->
<script src="/app.js" defer></script>
<!-- DNS prefetch for third-parties -->
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<link rel="preconnect" href="//cdn.example.com" crossorigin />
</head>
Rules
- Inline critical CSS — above-the-fold styles
- Preload key resources — LCP image, fonts, critical CSS
- Defer non-critical JS —
deferor module scripts - Preconnect third-party origins — fonts, APIs, CDNs
6. Resource Hints
| Hint | Use Case | Example |
|---|---|---|
preload |
Critical resource (same origin) | <link rel="preload" as="font" href="/font.woff2" crossorigin> |
prefetch |
Next navigation likely | <link rel="prefetch" href="/about"> |
preconnect |
Third-party origin | <link rel="preconnect" href="//fonts.gstatic.com" crossorigin> |
dns-prefetch |
DNS only (fallback) | <link rel="dns-prefetch" href="//api.example.com"> |
modulepreload |
JS modules | <link rel="modulepreload" href="/app.js"> |
Rules Resource
- Preload — LCP image, critical font, critical CSS
- Preconnect — all third-party origins used
- Prefetch — hover/intent-based for next page
- Don't over-hint — wastes bandwidth
7. Critical CSS
Extraction (build-time)
pnpm add -D critters
// vite.config.ts
import critters from "critters";
export default defineConfig({
plugins: [
critters({
preload: "swap",
pruneSource: true,
inlineThreshold: 10000,
}),
],
});
Manual approach
<head>
<style>
/* Critical: above-the-fold only */
header { ... }
.hero { ... }
.btn { ... }
</style>
<link
rel="preload"
href="/styles.css"
as="style"
/>
<noscript><link rel="stylesheet" href="/styles.css" /></noscript>
</head>
8. HTML & DOM (Compact)
Document structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Critical meta, preloads, inline critical CSS -->
</head>
<body>
<!-- Content first -->
<main>...</main>
<!-- Non-critical JS at end or deferred -->
</body>
</html>
Rules HTML & DOM
- Minimize DOM depth — flatter = faster style calc
- Reduce DOM size — < 1500 nodes, < 60 depth
- Avoid layout thrashing — batch reads/writes (see
javascriptskill)
9. CSS Performance (Compact)
Efficient selectors
/* ✅ Good */
.button {
}
.card__title {
}
/* ❌ Bad */
div > ul > li > a {
}
:global(.class) {
}
Containment
.widget {
contain: layout style paint; /* or strict */
content-visibility: auto;
contain-intrinsic-size: 1000px;
}
Animations
/* ✅ GPU-accelerated */
transform: translateX(100px);
opacity: 0.5;
/* ❌ Triggers layout */
left: 100px;
width: 200px;
Animation details: see
cssskill.
10. JavaScript Performance (Compact)
Code splitting
// Route-level
const Admin = lazy(() => import("./Admin"));
// Component-level
const HeavyChart = lazy(() => import("./HeavyChart"));
Web Workers (off-main-thread)
// worker.ts
self.onmessage = (e) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
// main.ts
const worker = new Worker(new URL("./worker.ts", import.meta.url));
worker.postMessage(data);
worker.onmessage = (e) => {
/* result */
};
Passive listeners
element.addEventListener("touchstart", handler, { passive: true });
element.addEventListener("scroll", handler, { passive: true });
JS patterns: see
javascriptskill.
11. Bundle Optimization (Reference)
Build config owned by
vite,esbuild,package-manager— this skill defines what to measure.
What to measure
| Metric | Tool | Target |
|---|---|---|
| Total JS size | vite build --mode production |
< 170 KB gzipped |
| CSS size | same | < 50 KB gzipped |
| Number of requests | DevTools Network | < 50 |
| Third-party size | Lighthouse | < 20% total |
Vite config (reference)
// vite.config.ts
build: {
target: 'es2022',
minify: 'esbuild',
cssCodeSplit: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom']
}
}
}
}
Full config: see
viteandesbuildskills.
12. Code Splitting (Reference)
Implementation in
vite/esbuild— this skill defines strategy.
Strategies
| Strategy | When |
|---|---|
| Route-based | Pages, admin sections |
| Component-based | Heavy widgets (charts, editors) |
| Vendor splitting | Stable dependencies (React, UI lib) |
| Dynamic import | On-demand features |
13. Lazy Loading
Images
<!-- Native (all modern browsers) -->
<img src="/image.avif" loading="lazy" alt="..." width="800" height="600" />
<!-- LCP image — NEVER lazy -->
<img src="/hero.avif" loading="eager" fetchpriority="high" alt="..." />
Iframes
<iframe src="/embed" loading="lazy" title="..."></iframe>
JS modules
// IntersectionObserver trigger
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
import("./HeavyComponent").then((m) => m.default.mount(entry.target));
observer.unobserve(entry.target);
}
});
});
observer.observe(document.querySelector("#heavy-component"));
14. Images (Essential)
Formats
| Format | Support | Use Case |
|---|---|---|
| AVIF | 95%+ | Best compression, photos |
| WebP | 97%+ | Fallback for AVIF |
| JPEG XL | Emerging | Future-proof |
| SVG | 100% | Icons, logos, illustrations |
Responsive images
<picture>
<source type="image/avif" srcset="/img.avif" />
<source type="image/webp" srcset="/img.webp" />
<img src="/img.jpg" alt="..." width="800" height="600" loading="lazy" />
</picture>
Rules Image
- Width/height always — prevents CLS
loading="lazy"— all below-fold imagesfetchpriority="high"— LCP image only- Serve via CDN — with
Cache-Control: immutable
15. Fonts (Essential)
@font-face {
font-family: "Inter";
src: url("/fonts/inter-var.woff2") format("woff2");
font-display: swap; /* Text visible during load */
font-weight: 100 900; /* Variable font */
size-adjust: 100.06%; /* Fallback metric override */
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
Preload
<link rel="preload" as="font" href="/fonts/inter-var.woff2" crossorigin />
Rules Fonts
- WOFF2 only — best compression
- Variable fonts — one file for all weights
font-display: swap— no invisible text- Subset — only needed glyphs (latin, cyrillic, etc.)
16. Caching (Essential)
Cache-Control headers
# Immutable assets (hashed filenames)
location ~* \.(js|css|png|jpg|avif|woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# HTML (no cache or short)
location / {
add_header Cache-Control "no-cache, must-revalidate";
}
# Fonts
location ~* \.woff2$ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin "*";
}
Rules Caching
- Hashed filenames —
app.[hash].js→ immutable cache - HTML no-cache — always revalidate
- Immutable for assets —
max-age=31536000, immutable
17. Compression (Essential)
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Brotli (better compression)
brotli on;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
Rules Compression
- Enable both gzip + brotli — brotli ~15% better
- Min 1KB — don't compress tiny responses
- CDN handles — usually automatic
18. Service Workers (Compact)
Workbox (recommended)
pnpm add -D workbox-cli
// workbox-config.js
module.exports = {
globDirectory: "dist/",
globPatterns: ["**/*.{js,css,html,ico,woff2,avif}"],
swDest: "dist/sw.js",
clientsClaim: true,
skipWaiting: true,
runtimeCaching: [
{
urlPattern: /^https:\/\/api\./,
handler: "NetworkFirst",
options: {
networkTimeoutSeconds: 10,
cacheableResponse: { statuses: [0, 200] },
},
},
{
urlPattern: /^https:\/\/cdn\./,
handler: "CacheFirst",
options: { expiration: { maxAgeSeconds: 31536000 } },
},
],
};
Registration
// main.js
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js");
}
Rules Services Workers
- Precache static assets — HTML, JS, CSS, fonts, images
- Runtime cache API —
NetworkFirstfor HTML/API,CacheFirstfor static skipWaiting+clientsClaim— immediate activation
19. Performance Budgets (CI)
Lighthouse CI
# .github/workflows/lighthouse.yml
- uses: treosh/lighthouse-ci-action@v11
with:
urls: |
https://staging.example.com
https://staging.example.com/about
budgetPath: ./lighthouse-budget.json
// lighthouse-budget.json
{
"ci": {
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"categories:accessibility": ["error", { "minScore": 0.95 }],
"categories:best-practices": ["error", { "minScore": 0.9 }],
"categories:seo": ["error", { "minScore": 0.9 }]
}
}
}
}
Bundle budget (vite)
// vite.config.ts
build: {
chunkSizeWarningLimit: 500, // KB
reportCompressedSize: true
}
Rules Performance Budgets
- CI fails on budget exceed — no merge if over
- Track trends — not just absolute values
- Per-page budgets — homepage stricter than admin
20. Methodology
Before using ANY performance pattern not documented in this skill:
- MCP Context7 (priority):
context7_resolve-library-id+context7_query-docsfor Lighthouse, web-vitals, Workbox. - Official docs: web.dev, developer.chrome.com — verify current APIs.
- Project config:
vite.config.ts,package.json, CI configs — verify against actual setup. - HARD RULE: If not in this skill AND cannot be verified against 2 authoritative sources → DO NOT USE IT. Document as assumption or risk in report to orchestrator.
21. Prohibitions
- ❌ Do not optimize without measuring — profile first
- ❌ Do not inline large CSS/JS — bloats HTML, blocks parsing
- ❌ Do not lazy-load LCP image — kills LCP
- ❌ Do not use
font-display: block— invisible text - ❌ Do not skip compression — free performance
- ❌ Do not cache HTML long-term — breaks updates
- ❌ Do not add third-party scripts without audit — audit first
- ❌ Do not animate layout properties — use transform/opacity
22. References
Note: For HTML conventions (critical rendering path), see HTML Note: For CSS conventions (animations, containment), see CSS Note: For JavaScript conventions (code splitting, workers), see JavaScript Note: For package manager conventions (build scripts), see Package Manager Note: For deploy CI/CD, see Deploy Note: For Vite build config, see Vite Note: For esbuild config, see esbuild
Last updated: 2026-08