From URL to Pixels
When you type a URL and press Enter, the browser kicks off a precise sequence of steps to turn raw bytes into a visible, interactive page. This is the critical rendering path — the minimum work the browser must do before the first pixel appears.
DOM and CSSOM
The HTML parser reads bytes, decodes characters, tokenizes tags, and builds the DOM (Document Object Model) — a tree of nodes. CSS is parsed in parallel into the CSSOM (CSS Object Model). Neither tree alone is enough to render; the browser merges them into a Render Tree that contains only visible nodes with their computed styles.
Layout, Paint, Composite
Layout (also called "reflow") calculates the exact position and size of every element in the viewport. Paint fills in pixels — text, colors, images, borders, shadows — producing paint records. Compositing takes independently painted layers (e.g., elements with transform, opacity, or will-change) and combines them on the GPU, which is why CSS transforms are cheaper than layout-triggering properties like width or top.
<script> tag without async or defer halts HTML parsing entirely. The browser must download, parse, and execute the script before it can continue building the DOM. This is why scripts belong at the bottom of <body> or use defer.
Parsing to Machine Code
Every major browser ships its own JavaScript engine. They all follow the same general pipeline but differ in optimization strategies and JIT compilation tiers.
| Engine | Browser | Interpreter | Optimizing JIT |
|---|---|---|---|
| V8 | Chrome, Edge, Node.js | Ignition | TurboFan |
| SpiderMonkey | Firefox | Baseline Interpreter | Warp (Ion replacement) |
| JavaScriptCore | Safari, Bun | LLInt | DFG → FTL |
The engine first parses JavaScript into an Abstract Syntax Tree (AST), then compiles it to bytecode for the interpreter. Functions that are called frequently ("hot") get promoted to an optimizing JIT compiler that produces much faster machine code. If the JIT's assumptions break (e.g., a variable changes type), the engine deoptimizes — falls back to the interpreter and tries again.
V8 Internals
V8 uses hidden classes (also called "shapes" or "maps") to give dynamically-typed objects a structure the engine can optimize against. Objects created the same way share the same hidden class, enabling fast property access via fixed offsets instead of dictionary lookups.
Inline caching records the hidden class seen at a property access site. On subsequent calls, if the hidden class matches, V8 skips the lookup entirely — going straight to the cached offset. This is why consistent object shapes matter for performance.
// Good — consistent shape
function Point(x, y) { this.x = x; this.y = y; }
// Bad — different property order = different hidden class
const a = {}; a.x = 1; a.y = 2;
const b = {}; b.y = 2; b.x = 1; // different hidden class from 'a'
Concurrency in a Single Thread
JavaScript runs on a single thread, yet handles async I/O, animations, and user input without blocking. The secret is the event loop — a continuous cycle that processes tasks in a specific priority order.
Microtasks vs Macrotasks
Microtasks include Promise.then(), queueMicrotask(), and MutationObserver. After the call stack empties, all queued microtasks run to completion before the browser can paint or process the next macrotask.
Macrotasks include setTimeout, setInterval, requestAnimationFrame, and I/O callbacks. The browser picks one macrotask per loop iteration, then drains microtasks again.
console.log('1 - sync');
setTimeout(() => console.log('4 - macrotask'), 0);
Promise.resolve()
.then(() => console.log('2 - microtask'))
.then(() => console.log('3 - microtask'));
// Output: 1, 2, 3, 4
Promise.then() calls will freeze the page. Long-running async work should yield to the main thread using setTimeout or scheduler.yield().
Web Workers
For truly CPU-intensive work, Web Workers run JavaScript on a separate OS thread. They communicate with the main thread via postMessage() and have no access to the DOM. SharedArrayBuffer enables shared memory between workers with Atomics for synchronization — useful for parallel computation, WASM workloads, and off-main-thread data processing.
Connections, Prefetch, and Service Workers
Browsers are sophisticated HTTP clients with built-in optimizations for reducing latency and reusing connections.
Connection Pooling
Browsers maintain a pool of persistent (keep-alive) TCP connections per origin — typically 6 connections per host in HTTP/1.1. HTTP/2 multiplexes all requests over a single connection, eliminating head-of-line blocking at the HTTP level (though TCP-level HoL blocking remains, which HTTP/3 with QUIC solves).
Resource Hints
Establishes TCP + TLS to an origin early. Use for known third-party origins like fonts or analytics.
Fetches a resource at low priority for future navigation. Great for next-page assets. Stored in the HTTP cache.
Fetches a resource at high priority for the current page. Use for late-discovered critical assets like fonts or hero images.
Resolves DNS for an origin ahead of time. Cheaper than preconnect — good for origins you might need.
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" href="/critical.css" as="style">
<link rel="prefetch" href="/next-page.js">
Service Workers
A Service Worker is a programmable network proxy that sits between the browser and the network. It intercepts fetch events and can serve responses from cache, transform requests, or implement offline-first strategies. Service Workers run on a separate thread, survive page reloads, and are the foundation of Progressive Web Apps (PWAs).
HTTP Cache Headers
The HTTP cache is the browser's first line of defense against unnecessary network requests. Two mechanisms work together: freshness (is the cached copy still valid?) and validation (has the resource changed on the server?).
| Header | Type | How It Works |
|---|---|---|
Cache-Control | Freshness | Directives like max-age=3600, no-cache, immutable. The primary caching mechanism. |
Expires | Freshness | Absolute date. Legacy — overridden by Cache-Control when both present. |
ETag | Validation | Hash of the resource. Browser sends If-None-Match; server returns 304 if unchanged. |
Last-Modified | Validation | Timestamp. Browser sends If-Modified-Since; server returns 304 if unchanged. |
app.a1b2c3.js), use Cache-Control: max-age=31536000, immutable. The filename hash ensures cache-busting on updates, and immutable tells the browser to skip revalidation entirely — no conditional requests, no 304s, just instant cache hits.
Core Web Vitals and Optimization
Google's Core Web Vitals are the standardized metrics for measuring real-user experience. They directly influence search ranking and represent loading, interactivity, and visual stability.
Largest Contentful Paint. Time until the largest visible element renders. Target: < 2.5s. Optimize with preloaded hero images, critical CSS, and fast server response.
Interaction to Next Paint. Latency from user input to visual update (replaced FID in 2024). Target: < 200ms. Reduce with smaller event handlers and yielding to the main thread.
Cumulative Layout Shift. Total unexpected layout movement. Target: < 0.1. Fix with explicit width/height on images and reserved space for dynamic content.
TTFB (Time to First Byte)
TTFB measures the time from request start to the first byte of the response. It captures DNS, TCP, TLS, and server processing time. While not a Core Web Vital, poor TTFB (> 800ms) makes hitting LCP targets nearly impossible. Optimize with CDNs, edge computing, and server-side caching.
Critical CSS
Extract the CSS needed for above-the-fold content and inline it in <head>. Load the rest asynchronously. This eliminates the render-blocking stylesheet fetch and lets the browser paint immediately after parsing HTML.
<head>
<!-- Inline critical CSS -->
<style>
/* above-the-fold styles only */
body { margin: 0; font-family: sans-serif; }
.hero { height: 100vh; background: #1a1a1a; }
</style>
<!-- Load rest async -->
<link rel="preload" href="/full.css" as="style"
onload="this.rel='stylesheet'">
</head>
Code Splitting
Instead of shipping one massive JavaScript bundle, split code by route or component. Modern bundlers (Webpack, Vite, esbuild) support dynamic import() to load chunks on demand. Combined with tree shaking (removing dead exports), this keeps initial load fast and defers non-critical code.
srcset. Lazy-load below-the-fold images with loading="lazy". Set fetchpriority="high" on LCP images. Avoid layout thrashing by batching DOM reads and writes.
Test Yourself
display: none are excluded.setTimeout(..., 0).<link rel="preload"> tells the browser to fetch a resource at high priority for use in the current page. prefetch is for future navigations (low priority), and preconnect only sets up the connection without fetching content.If-None-Match with the cached ETag value. If the server's current ETag matches, it responds with 304 Not Modified (no body), saving bandwidth. If-Modified-Since is the validation header used with Last-Modified timestamps.transform and opacity only affect the compositing stage — the GPU moves or fades the pre-painted layer. Changing width or top triggers layout recalculation followed by repaint, which is far more expensive.