MDN Web Docs · Crash Course

Web Performance in Depth

Everything you need to know about making fast, efficient web experiences — from how browsers work to advanced optimization strategies.

Modules 13 Topics
Level Beginner → Advanced
Source MDN Web Performance Guides
01 · Foundations

Performance Fundamentals

Performance means efficiency. A fast app delivers its content quickly and responds to user interactions without delay. Poor performance directly costs you users — research consistently shows that slower pages lose visitors, conversions, and engagement.

Core Principle Performance is about minimizing the time to first paint, the time to interactive, and keeping interactions below 50ms so they feel instant.

The Three Pillars

Loading Performance
How fast assets arrive from the network. Influenced by file size, server response, caching, and network conditions.
🎞
Rendering Performance
How fast the browser turns bytes into pixels. Influenced by DOM size, CSS complexity, and JavaScript blocking.
🎯
Perceived Performance
How fast it feels. Often more important than raw speed. Skeleton screens and optimistic UI can transform perception.
🔧
Runtime Performance
Performance during use — scroll jank, animation stutter, heavy JavaScript execution that freezes the UI thread.

02 · Foundations

How Browsers Work

Understanding how browsers process a URL into visible pixels is the foundation of all performance work. Every optimization maps back to shortening or parallelizing this pipeline.

The Full Page Load Pipeline

01
DNS Lookup
02
TCP Connect
03
TLS Handshake
04
HTTP Request
05
Server Response
06
Parse HTML
07
Build DOM
08
Fetch + Parse CSS
09
Build CSSOM
10
Render Tree
11
Layout
12
Paint
13
Composite
Key Insight JavaScript execution blocks the HTML parser. An unoptimized <script> tag in <head> halts everything until the JS downloads, parses, and executes. Use defer or async.

The browser uses a preload scanner — a secondary parser that looks ahead in the HTML stream to fetch resources like CSS, fonts, and images before the main parser reaches them.


03 · Foundations

Critical Rendering Path

The Critical Rendering Path (CRP) is the sequence of steps to convert HTML + CSS + JS into pixels. Optimizing it means reducing the number of blocking steps and getting the first paint to happen as fast as possible.

The Four Key Objects

🌳
DOM
Document Object Model. Built from HTML. Each node is an element. JavaScript can modify it dynamically.
🎨
CSSOM
CSS Object Model. Built from stylesheets. CSS is render-blocking — the browser won't paint until the CSSOM is complete.
🌲
Render Tree
DOM + CSSOM combined. Only includes visible nodes. display: none elements are excluded.
📐
Layout
Calculates position and size of each element. Also called "reflow". Expensive — avoid triggering it in loops.

Render-Blocking Resources

By default, CSS is render-blocking and JS is both parser-blocking and render-blocking. Minimize both.

Scripts — use defer or async
<!-- Blocks parsing entirely (avoid) -->
<script src="app.js"></script>

<!-- Downloads in parallel, executes after HTML parsed -->
<script src="app.js" defer></script>

<!-- Downloads in parallel, executes immediately when ready -->
<script src="analytics.js" async></script>
CSS — use media queries to hint non-blocking sheets
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="print.css" media="print">
<link rel="stylesheet" href="large.css" media="(min-width:1024px)">

04 · Foundations

Understanding Latency

Latency is the time for a data packet to travel from source to destination. It's measured in milliseconds and is determined by physical distance and network hops — you cannot compress it to zero.

What Affects Latency

  • Geographic distance — packets travel at ~2/3 the speed of light through fiber
  • Number of network hops — each router adds ~1–5ms
  • Protocol overhead — TCP handshakes, TLS negotiations add round trips
  • Network congestion — shared infrastructure causes variable latency
  • Last-mile connection — WiFi, 4G, and cable all have different base latencies
Practical Impact A user in Sydney hitting a London server will have ~250ms base latency per round trip. That's before any processing. Use CDNs to put content closer to users.

Reducing Latency

  • Use a CDN — serve assets from edge nodes close to users
  • Enable HTTP/2 or HTTP/3 — multiplexing eliminates head-of-line blocking
  • Use Connection: keep-alive — reuse TCP connections
  • Implement service workers — serve cached responses with ~0ms latency
  • Enable QUIC/HTTP3 — reduces handshake round trips significantly

05 · Measurement

Timing Guidelines: How Long is Too Long?

Human perception defines performance budgets. These are the thresholds research has established for different interaction types:

Content Load Signal
1s
Indicate content will load. Longer feels broken.
Animation Frame
16.7ms
60fps. Exceeding this causes visible jank.
Input Response
50–200ms
User input must feel immediate. 200ms max.
Main Thread Idle
50ms
Tasks over 50ms are "long tasks" — they block input.
Core Web Vitals Google's Core Web Vitals provide specific measurable targets: LCP (Largest Contentful Paint) under 2.5s, INP (Interaction to Next Paint) under 200ms, and CLS (Cumulative Layout Shift) under 0.1.

06 · Loading

Lazy Loading

Lazy loading defers loading of non-critical resources until they're actually needed — typically when they scroll into view. It shortens the critical rendering path by reducing initial page weight.

Native HTML Lazy Loading

Images & iframes
<!-- Browser-native, no JS needed -->
<img src="hero.jpg" loading="eager" alt="...">  <!-- above fold -->
<img src="photo.jpg" loading="lazy" alt="...">  <!-- below fold -->
<iframe src="map.html" loading="lazy"></iframe>

Intersection Observer (JS)

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

document.querySelectorAll('[data-src]').forEach(img => {
  observer.observe(img);
});
What to lazy load Images below the fold · Iframes (maps, embeds) · Heavy JavaScript modules · Non-critical CSS · Web fonts for body text
What NOT to lazy load Hero images · Above-the-fold content · LCP element · Critical CSS · Web fonts used in headers

07 · Loading

Speculative Loading

While lazy loading defers work, speculative loading does the opposite — it preemptively fetches resources the user is likely to need next. When a guess is right, the navigation feels instantaneous.

Resource Hints Hierarchy

From weakest hint to strongest action
<!-- Resolve DNS only (very cheap, use liberally) -->
<link rel="dns-prefetch" href="https://fonts.googleapis.com">

<!-- DNS + TCP + TLS (moderate cost) -->
<link rel="preconnect" href="https://api.example.com">

<!-- Fetch + cache the resource (use for certain resources) -->
<link rel="prefetch" href="/next-page.html">

<!-- Fetch + execute immediately, highest priority -->
<link rel="preload" as="font" href="font.woff2" crossorigin>
<link rel="preload" as="image" href="hero.avif">

<!-- Speculatively render entire next page (Chrome 108+) -->
<script type="speculationrules">
  { "prerender": [{ "source": "list", "urls": ["/shop"] }] }
</script>
Speculation Rules API The newest approach — it lets you prerender entire pages in a hidden tab. When the user navigates, they see an already-rendered page. Reduces perceived navigation to near-zero.

08 · Loading

DNS Prefetch

Every unique hostname requires a DNS lookup before the browser can connect. On slow networks, this can take 20–120ms. DNS prefetch resolves these in the background, eliminating that delay when the resource is actually requested.

<!-- Prefetch DNS for third-party origins -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link rel="dns-prefetch" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://analytics.provider.io">

<!-- For origins you'll connect to soon, use preconnect instead -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
Tip Use preconnect for your top 1–2 most critical third-party origins (e.g. font CDN). Use dns-prefetch as a fallback for browsers that don't support preconnect, and for all other third-party origins you'll eventually need.

09 · Loading

Optimizing Startup Performance

Startup is your first impression. A long startup makes users think the app crashed. The goal: get something visible and interactive on screen as fast as possible.

Key Strategies

  • Code splitting — split your JS bundle; load only what's needed for the initial view
  • Tree shaking — eliminate unused code from bundles at build time
  • Inline critical CSS — put above-the-fold styles in a <style> tag in <head>
  • Preload the LCP image — use <link rel="preload"> for your hero image
  • Minimize render-blocking JS — defer everything not needed for first render
  • Server-side rendering (SSR) — send pre-rendered HTML so browser can paint immediately
  • Reduce server response time — target under 200ms TTFB (Time to First Byte)
The 3-Second Rule 53% of mobile users abandon a page that takes longer than 3 seconds to load. Treat startup performance as a product-critical metric, not a nice-to-have.

10 · Animation

Animation Performance & Frame Rate

Smooth animation requires 60 frames per second — meaning each frame has only 16.7ms for the browser to calculate layout, paint, and composite. Miss the budget and you get jank.

CSS vs JS Animations

CSS Animations ✓ Prefer These

  • Run on the compositor thread (off main thread)
  • transform and opacity are GPU-accelerated
  • No JavaScript overhead
  • Browser can optimize and skip frames gracefully

JS Animations — Use Carefully

  • Run on the main thread by default
  • Can be blocked by other JS execution
  • Use requestAnimationFrame — never setInterval
  • Web Animations API gives JS with compositor benefits

The Compositing Secret

Animating transform and opacity only triggers the composite step — no layout, no paint. This is why they're the only "free" properties to animate.

Properties and their rendering cost
/* ✅ Compositor only — free to animate */
transform: translateX(100px);
opacity: 0.5;

/* ⚠️ Triggers paint — moderate cost */
color, background-color, box-shadow, border-radius;

/* ❌ Triggers layout (reflow) — expensive */
width, height, top, left, margin, padding, font-size;
will-change Hint Use will-change: transform to promote an element to its own compositor layer before animation begins. This eliminates promotion cost at animation start. Use sparingly — each layer uses GPU memory.


12 · Measurement

RUM vs. Synthetic Monitoring

Two complementary approaches to measuring performance in production. Use both for a complete picture.

Real User Monitoring (RUM)

  • Measures actual users on real devices/networks
  • Captures the long tail (slow devices, rural users)
  • Best for spotting long-term regressions
  • Requires traffic — no data for new pages
  • Examples: SpeedCurve, Datadog RUM, web-vitals.js

Synthetic Monitoring

  • Controlled, scripted tests from fixed locations
  • Works before launch — no users needed
  • Best for catching regressions in CI/CD
  • Consistent baseline for comparisons
  • Examples: WebPageTest, Lighthouse CI, Calibre
Best Practice Use synthetic monitoring in CI/CD to catch regressions before they ship. Use RUM to understand real-world impact after deployment. Treat p75 and p95 percentiles as your targets, not just the median.

13 · Process

Performance Budgets

A performance budget is a hard limit that prevents regressions from shipping. It makes performance a first-class engineering constraint, not an afterthought.

Types of Budgets

📦
Quantity Budgets
Max JS bundle size, max image weight, max number of HTTP requests, max total page weight.
Timing Budgets
Max Time to Interactive, max LCP, max FCP. Directly tied to user experience metrics.
📊
Rule-Based Budgets
Lighthouse performance score must stay above 90. Catches regressions that don't have explicit metric targets.
🔁
Relative Budgets
No feature can increase bundle size by more than 5kb without sign-off. Scales with team growth.
Enforcing budgets in webpack
// webpack.config.js
module.exports = {
  performance: {
    maxAssetSize: 250000,       // 250kb per asset
    maxEntrypointSize: 400000,  // 400kb entrypoint
    hints: 'error',             // fail the build, not just warn
  }
};
Starting Point Budget your JS to under 170kb compressed for the critical path. This leaves room for HTML, CSS, fonts, and images within a 300kb total initial load — enough to be fast even on 3G.

→ Quick Reference

Performance Checklist

Your launch checklist for web performance fundamentals:

  • Use defer or async on all non-critical scripts
  • Inline critical CSS; load the rest asynchronously
  • Add loading="lazy" to all below-fold images and iframes
  • Preload the LCP image with <link rel="preload" as="image">
  • Use dns-prefetch / preconnect for third-party origins
  • Enable HTTP/2 or HTTP/3 on your server
  • Compress all text assets with Brotli or gzip
  • Serve images in WebP or AVIF format
  • Set aggressive cache headers for static assets
  • Only animate transform and opacity
  • Use requestAnimationFrame for JS-driven animations
  • Code-split your JS bundle; load only what's needed
  • Measure with Lighthouse CI in your CI/CD pipeline
  • Set and enforce performance budgets per page type
  • Monitor real users with RUM tooling in production