Install
Run this where your agent runs. ClawHub downloads the skill into its workspace.
npx clawhub@latest install web-performanceDocumentation
Performance is a feature. A 1-second delay in page response reduces conversions by 7%. Google's
Core Web Vitals are ranking signals. Users on mobile networks with mid-range devices represent
the median experience — optimizing for your MacBook Pro on fiber means most users see a broken
experience. Expert performance means measuring first, optimizing the bottleneck, and verifying
real-user impact.
Core Mental Model
Performance problems fall into three categories: network (too many resources, too large, wrong
priority), parsing/execution (JavaScript blocking render, slow main thread), and visual
instability (layout shifts). Each Core Web Vital maps to one of these. Fix them in order:
eliminate render-blocking, load critical resources first, defer everything else, and measure
real users (RUM), not just synthetic benchmarks.
Core Web Vitals Targets
| Metric | Good | Needs Work | Poor | Measures |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | <2.5s | 2.5–4s | >4s | Loading speed |
| CLS (Cumulative Layout Shift) | <0.1 | 0.1–0.25 | >0.25 | Visual stability |
| INP (Interaction to Next Paint) | <200ms | 200–500ms | >500ms | Interactivity |
LCP Optimization
The LCP element is usually a hero image, above-fold heading, or background image.
<!-- 1. fetchpriority="high" — tell browser this is the LCP image -->
<img
src="/hero.webp"
fetchpriority="high"
loading="eager"
decoding="async"
alt="Agent Platform Hero"
width="1200"
height="600"
/>
<!-- 2. Preload the LCP image (especially if CSS background or lazy-loaded) -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
<!-- 3. Preload with srcset for responsive LCP -->
<link
rel="preload"
as="image"
imagesrcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w"
imagesizes="(max-width: 768px) 100vw, 50vw"
/>
CLS Prevention
/* Always set width and height on images (or use aspect-ratio) */
img {
width: 100%;
height: auto;
aspect-ratio: 16 / 9; /* reserves space before image loads */
}
/* Avoid inserting content above existing content */
/* ❌ Banner that pushes content down */
.late-loading-banner { position: static; } /* causes shift */
/* ✅ Reserve space or use position:fixed */
.notification-bar { min-height: 48px; } /* pre-reserve space */
/* Font CLS: use font-display:optional or size-adjust */
@font-face {
font-family: "Inter";
src: url("/fonts/inter.woff2") format("woff2");
font-display: optional; /* never causes layout shift (may use system font) */
/* or font-display: swap + size-adjust: 103% to match fallback metrics */
}
Resource Hints
<!-- preconnect: establish connection to critical third-party origins early -->
<!-- Use for: Google Fonts, CDN, API origin you'll definitely fetch from -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://api.moltbotden.com" crossorigin />
<!-- dns-prefetch: cheaper than preconnect, just resolves DNS -->
<!-- Use for: analytics, social embeds, non-critical third parties -->
<link rel="dns-prefetch" href="https://analytics.example.com" />
<!-- preload: load resource with high priority, use before browser discovers it -->
<!-- Use for: LCP image, critical font, render-blocking CSS -->
<link rel="preload" as="font" href="/fonts/inter-400.woff2" crossorigin />
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
<link rel="preload" as="script" href="/critical.js" />
<!-- prefetch: low-priority fetch for likely next navigation -->
<!-- Use for: next page resources, hover-predicted links -->
<link rel="prefetch" href="/agents/page-2" />
<!-- ⚠️ Don't preload everything — defeats the purpose -->
<!-- Preload only critical resources on the current page's hot path -->
Critical Rendering Path
<!-- 1. Inline critical CSS (above-fold styles, <14KB) -->
<head>
<style>
/* Only styles needed to render above-the-fold content */
body { margin: 0; background: #0f172a; color: #f8fafc; font-family: system-ui; }
.hero { min-height: 100svh; display: flex; align-items: center; }
.nav { height: 60px; display: flex; align-items: center; padding: 0 1rem; }
</style>
<!-- 2. Non-critical CSS: load async (media trick) -->
<link
rel="stylesheet"
href="/styles/below-fold.css"
media="print"
onload="this.media='all'"
/>
<noscript><link rel="stylesheet" href="/styles/below-fold.css" /></noscript>
</head>
<!-- 3. Scripts: defer or async -->
<script src="/analytics.js" async></script> <!-- non-critical, no dependency -->
<script src="/app.js" defer></script> <!-- critical, preserve order -->
<script type="module" src="/main.js"></script> <!-- always deferred -->
Image Optimization
<!-- Responsive image with art direction and modern formats -->
<picture>
<!-- Mobile: square crop, WebP -->
<source
media="(max-width: 768px)"
srcset="/agent-hero-mobile.avif 400w, /agent-hero-mobile.avif 800w"
type="image/avif"
sizes="100vw"
/>
<!-- Desktop: wide crop, WebP -->
<source
srcset="/agent-hero-desktop.webp 800w, /agent-hero-desktop.webp 1600w"
type="image/webp"
sizes="(max-width: 1200px) 100vw, 1200px"
/>
<!-- Fallback -->
<img
src="/agent-hero-desktop.jpg"
alt="AI Agent Platform"
width="1200"
height="600"
loading="eager"
fetchpriority="high"
decoding="async"
/>
</picture>
<!-- Below-fold images: lazy load -->
<img
src="/agent-card.webp"
alt="Agent Card"
loading="lazy"
decoding="async"
width="400"
height="300"
/>
# Generate AVIF + WebP from source images (sharp or squoosh)
npx squoosh-cli --avif '{"cqLevel":33}' --webp '{"quality":85}' *.jpg
# Or with sharp in Node.js
sharp("hero.jpg")
.avif({ quality: 50 })
.toFile("hero.avif");
JavaScript Bundle Optimization
// 1. Code splitting with dynamic import (React lazy)
import { lazy, Suspense } from "react";
const AgentAnalytics = lazy(() => import("./AgentAnalytics"));
const AgentSettings = lazy(() => import("./AgentSettings"));
// 2. Route-based code splitting (Next.js App Router does this automatically)
// Each page in app/ is a separate bundle
// 3. Preload on hover for anticipated navigation
function NavLink({ href, children }) {
return (
<a
href={href}
onMouseEnter={() => {
// Preload the chunk when user hovers
import(`../pages/${href}`);
}}
>
{children}
</a>
);
}
// 4. Tree-shaking: named imports only
import { debounce } from "lodash-es"; // ✅ tree-shakeable
import _ from "lodash"; // ❌ imports all of lodash
// 5. Bundle analysis
// npx @next/bundle-analyzer (Next.js)
// npx vite-bundle-visualizer (Vite)
// npx webpack-bundle-analyzer stats.json (webpack)
// vite.config.ts — manual code splitting
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ["react", "react-dom"],
charts: ["recharts", "d3"],
editor: ["@codemirror/view", "@codemirror/state"],
},
},
},
},
};
Font Loading Strategy
/* 1. Preload the most critical font weight (regular text) */
/* In <head>: <link rel="preload" as="font" href="/fonts/inter-400.woff2" crossorigin /> */
/* 2. font-display strategy */
@font-face {
font-family: "Inter";
src: url("/fonts/inter-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap; /* show fallback immediately, swap when loaded */
/* font-display: optional — no layout shift (may skip custom font on slow connections) */
}
/* 3. Size-adjust to prevent CLS with font-display:swap */
@font-face {
font-family: "Inter-Fallback";
src: local("Arial"); /* use system font as base */
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
size-adjust: 107%; /* scale to match Inter's metrics */
}
body {
font-family: "Inter", "Inter-Fallback", system-ui, sans-serif;
}
/* 4. Variable fonts: one file for all weights */
@font-face {
font-family: "Inter";
src: url("/fonts/inter-variable.woff2") format("woff2-variations");
font-weight: 100 900;
font-display: swap;
}
Caching Strategy
Resource type | Cache-Control | Notes
───────────────────────┼──────────────────────────────────┼─────────────────
HTML | no-cache | Always revalidate
Hashed JS/CSS bundles | max-age=31536000, immutable | Never changes
Images (stable) | max-age=2592000, stale-while-revalidate=86400 |
API responses | no-store or max-age=60, s-maxage=300 | Depends on sensitivity
Service worker | no-cache | Always fresh
Fonts | max-age=31536000, immutable | Versioned by filename
# Nginx: aggressive caching for hashed assets
location ~* \.(js|css|woff2)$ {
add_header Cache-Control "max-age=31536000, immutable";
}
# HTML: always revalidate
location / {
add_header Cache-Control "no-cache";
}
# API: short cache with stale-while-revalidate
location /api/ {
add_header Cache-Control "max-age=60, stale-while-revalidate=300";
}
Service Worker: Cache-First Strategy
// sw.js — cache-first for assets, network-first for API
const CACHE_VERSION = "v1";
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const API_CACHE = `api-${CACHE_VERSION}`;
const STATIC_ASSETS = ["/", "/offline.html", "/app.css", "/app.js"];
self.addEventListener("install", event => {
event.waitUntil(
caches.open(STATIC_CACHE).then(cache => cache.addAll(STATIC_ASSETS))
);
});
self.addEventListener("activate", event => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== STATIC_CACHE && k !== API_CACHE).map(k => caches.delete(k)))
)
);
});
self.addEventListener("fetch", event => {
const { request } = event;
const url = new URL(request.url);
if (url.pathname.startsWith("/api/")) {
// Network-first for API
event.respondWith(
fetch(request)
.then(response => {
const clone = response.clone();
caches.open(API_CACHE).then(cache => cache.put(request, clone));
return response;
})
.catch(() => caches.match(request))
);
} else {
// Cache-first for static assets
event.respondWith(
caches.match(request).then(cached => cached || fetch(request))
);
}
});
Performance Budget
// .performance-budget.json
{
"resourceSizes": [
{ "resourceType": "script", "budget": 300 },
{ "resourceType": "total", "budget": 800 },
{ "resourceType": "image", "budget": 500 },
{ "resourceType": "font", "budget": 100 }
],
"timings": [
{ "metric": "first-contentful-paint", "budget": 1500 },
{ "metric": "interactive", "budget": 3500 },
{ "metric": "speed-index", "budget": 2000 }
]
}
# Lighthouse CI to enforce budget in CI
npx lhci autorun --collect.url=https://staging.moltbotden.com
Chrome DevTools Performance Interpretation
Key metrics to look for in Performance tab:
──────────────────────────────────────────
FCP First Contentful Paint → Yellow bar in "Timings"
LCP Largest Contentful Paint → Green bar in "Timings"
TBT Total Blocking Time → Sum of "Long Tasks" (>50ms) in main thread
CLS Cumulative Layout Shift → Layout events in "Experience" row
Long Tasks (>50ms blocks main thread):
→ Look for red/orange blocks in "Main" thread
→ Click to see which script is responsible
→ Code-split or defer that script
Network waterfall:
→ Look for render-blocking requests (start after HTML parse, end before FCP)
→ Look for sequential requests that could be parallelized
→ Look for oversized resources (image, JS chunk)
Anti-Patterns
<!-- ❌ No width/height on images (causes CLS) -->
<img src="hero.jpg" alt="..." />
<!-- ✅ -->
<img src="hero.jpg" width="1200" height="600" alt="..." />
<!-- ❌ Lazy loading the LCP image -->
<img src="hero.jpg" loading="lazy" />
<!-- ✅ -->
<img src="hero.jpg" loading="eager" fetchpriority="high" />
<!-- ❌ Preloading too many resources -->
<link rel="preload" href="/font-400.woff2" as="font" crossorigin />
<link rel="preload" href="/font-700.woff2" as="font" crossorigin />
<link rel="preload" href="/font-italic.woff2" as="font" crossorigin />
<!-- Only preload the single font variant you use above the fold -->
Quick Reference
LCP: fetchpriority="high" on LCP image, preload if in CSS, server fast HTML
CLS: width+height on images, reserve space for dynamic content, font-display:optional
INP: debounce event handlers, break up long tasks, avoid blocking main thread
Resource hints: preconnect (connections), preload (current page), prefetch (next page)
Images: AVIF>WebP>JPEG/PNG, srcset+sizes, loading=lazy below fold
JS bundles: dynamic import(), manualChunks in Vite, analyze with bundle visualizer
Fonts: preload woff2, font-display:swap, size-adjust fallback, variable fonts
Caching: HTML=no-cache, hashed assets=immutable 1yr, API=short maxage
Service worker: cache-first static, network-first API, activate cleans old caches
Budget: set budget, measure in CI with Lighthouse CI, fail on regression