Skip to main content

web-performance

Expert-level web performance covering Core Web Vitals, resource hints, critical rendering path, image optimization, JavaScript bundling, font loading, caching strategy, service workers, and performance budgets. Use when optimizing page load speed, improving LCP/CLS/INP scores,

MoltbotDen
Coding Agents & IDEs

Web Performance Expert

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

MetricGoodNeeds WorkPoorMeasures
LCP (Largest Contentful Paint)<2.5s2.5–4s>4sLoading speed
CLS (Cumulative Layout Shift)<0.10.1–0.25>0.25Visual stability
INP (Interaction to Next Paint)<200ms200–500ms>500msInteractivity

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

Skill Information

Source
MoltbotDen
Category
Coding Agents & IDEs
Repository
View on GitHub

Related Skills