The browser parses HTML into the DOM and CSS into the CSSOM, combines them into the render tree, runs Layout (geometry), Paint (pixels), and Composite (assemble layers). JS can mutate the DOM/CSSOM, so a synchronous <script> blocks parsing; CSS blocks rendering and (without async) blocks the script after it. This is the Critical Rendering Path.
Category
Browser Internals
Event loop, V8, GC, rendering pipeline, paint and layout.
34 questions
HTML → DOM, CSS → CSSOM, combined into the Render Tree → Layout (geometry) → Paint (pixels) → Composite (layers). JS can block parsing; CSS blocks rendering. Optimizing the CRP = minimize/defer blocking resources, inline critical CSS, reduce bytes — the foundation of fast first paint.
URL → DNS lookup → TCP + TLS handshake → HTTP request → server responds with HTML → browser parses HTML→DOM, CSS→CSSOM, runs JS → render tree → layout → paint → composite (the Critical Rendering Path). Know the network phase AND the rendering phase, and what blocks what.
Click → DOM event dispatched (capture→target→bubble) → handler runs on the main thread → validation/state updates → fetch() called → browser builds the request, applies CORS/credentials/CSP checks, may do a preflight → DNS lookup → TCP handshake → TLS handshake → HTTP request bytes sent. The JS handler returns long before the response; the response callback is queued as a microtask.
HTML → DOM. CSS → CSSOM. DOM + CSSOM → Render tree. Layout (compute geometry). Paint (rasterize). Composite (layers). Each step blocks the next on the main thread. Optimize by minimizing render-blocking CSS, deferring non-critical JS, sizing media, and isolating animations to transform/opacity for compositor-only paint.
Browser does DNS → TCP → TLS → HTTP, then parses HTML into the DOM. CSS builds the CSSOM (render-blocking). DOM + CSSOM → render tree → layout → paint → composite. Synchronous JS blocks the parser; defer/async unblock it.
CRP: bytes → DOM (HTML parser) + CSSOM (CSS parser) → render tree (matched rules + DOM nodes) → layout → paint → composite. CSSOM is built from all CSS — render-blocking until complete. Specificity, cascade, and inheritance are resolved here. Avoid `@import`, ship minimal critical CSS, defer non-critical styles.
Critical rendering path = HTML parse → CSSOM build → DOM + CSSOM merge into render tree → layout → paint. Blockers: render-blocking CSS (parser-blocking and render-blocking), parser-blocking sync `<script>` in head, large fonts (FOIT), heavy synchronous JS. Fixes: minimal critical CSS inline, `defer`/`async` scripts, font-display: swap, code split, preconnect.
Load lifecycle events: DOMContentLoaded (DOM parsed, before images/stylesheets finish), load (everything including subresources done), beforeunload/unload/pagehide (leaving), readystatechange. Plus the modern way: Performance API / PerformanceObserver and Core Web Vitals (LCP, FCP, CLS) for real measurement.
Reflow (layout): recalculating element geometry — positions and sizes. Repaint: redrawing pixels (colors, visibility) with geometry unchanged. Reflow is more expensive and triggers a repaint; repaint alone doesn't trigger reflow. ('Rework' isn't a real browser term.) Composite-only props (transform/opacity) skip both.
**Reflow (layout)**: recompute box geometry — triggered by anything that changes size/position (width, font-size, DOM insertion, viewport resize). Expensive. **Repaint**: redraw pixels without re-layout — triggered by color/visibility changes. Cheap-ish. **Composite-only** (transform/opacity on a GPU-promoted layer) avoids both. Animate with transform + opacity for 60fps.
Events propagate in three phases: capture (document → target), target, bubble (target → document). `addEventListener(fn)` defaults to bubble; pass `{ capture: true }` for capture. Stop with `stopPropagation()` (further ancestors only) or `stopImmediatePropagation()` (same-element siblings too). Some events don't bubble (`focus`, `blur`) — use `focusin`/`focusout`.
Events flow capture (top → target) → target → bubble (target → top). Delegation puts ONE listener on a parent and uses event.target to handle children. Saves memory and works on dynamically added nodes.
Events propagate in three phases: capture (root → target), target, bubble (target → root). Delegation = listen on a single ancestor instead of N children; use `event.target.closest(selector)` to identify the actual hit. Cheaper memory, works for dynamic children. `stopPropagation` short-circuits; `preventDefault` is unrelated (cancels the default action).
Delegation: one listener on a common ancestor handles events for many children. Works because events bubble up. Pros: fewer listeners (memory, setup cost), works for dynamically added children. Pattern: `element.addEventListener('click', e => { const item = e.target.closest('[data-id]'); if (!item) return; … })`.
Use modern DOM APIs (`querySelector`, `closest`, `dataset`, `classList`), prefer event delegation, batch DOM writes after reads to avoid layout thrash, scope styles via CSS classes/custom properties not inline, clean up listeners with AbortController on teardown. Keep DOM operations declarative-ish: template via `<template>` or DOMParser, swap entire subtrees instead of micromanaging.
Three client-side storage mechanisms with different lifetimes, scopes, capacities, and access models. localStorage persists indefinitely (~5MB, same-origin), sessionStorage clears on tab close, cookies (~4KB) are sent on every HTTP request and are the only one usable by the server.
Pick by need: cookies (sent to server, small, can be HttpOnly/Secure for auth); localStorage (persistent key-value, ~5MB, JS-only, sync API); sessionStorage (same but per-tab session); IndexedDB (large structured async storage, indexable, transactions). Tokens → HttpOnly cookies. UI prefs → localStorage. Big or structured data → IndexedDB.
Cookies are sent on every request and can be HttpOnly/Secure — right for auth tokens. localStorage is JS-readable and persists indefinitely — right for non-sensitive prefs. sessionStorage is JS-readable and dies with the tab — right for ephemeral wizard/draft state.
No — localStorage is scoped by **origin** (scheme + host + port). `https://a.com` and `https://b.com` see different storage. But same origin includes subdomains only if you configure document.domain (deprecated) or use postMessage from an embedded iframe of the original origin. Caveat: XSS on the original site reads it freely.
Three unrelated terms. Cache-Control: HTTP header dictating if/how long a response is cached. ETag: a content fingerprint for conditional revalidation (304 Not Modified). DocumentFragment: a lightweight, off-DOM container for batching DOM nodes so insertion triggers one reflow.
A service worker is a script the browser runs in the background, separate from the page, that intercepts network requests and can serve from cache, the network, or both. Enables offline mode, custom caching strategies (cache-first, network-first, stale-while-revalidate), background sync, push notifications. Lifecycle: install → activate → fetch event. Constraints: HTTPS only, no DOM access, single-threaded JS, scope = path of registration. Powers PWAs.
A service worker is a background script that proxies network requests for its scope. Use cases: offline support, asset caching for instant repeat loads, background sync of queued mutations, push notifications. Lifecycle: install → waiting → activate → fetch interception. Update gotcha: the new SW activates only after old tabs close — handle with skipWaiting + a 'reload to update' UX.
A service worker is a background JS thread that intercepts network requests for an origin. Pair with the Cache API to serve responses offline, push notifications, and background sync. Lifecycle: install → activate → fetch.
A PWA is a web app that uses a service worker, web app manifest, and HTTPS to behave like a native app — installable, offline-capable, push notifications, fast repeat loads. Benefits: reach + install without an app store, offline resilience, one codebase. Limits: less OS integration than native.
PWAs add: installable to home screen (manifest), offline + cached shell (service worker), background sync for queued mutations, push notifications, faster repeat loads via cache-first asset strategies, share targets, and OS-level integration. Best wins: instant repeat load and resilient-on-flaky-network. Cost: SW complexity and update gotchas.
Options: the `storage` event (fires in OTHER tabs when localStorage changes), the BroadcastChannel API (purpose-built tab-to-tab messaging), or a SharedWorker. Common uses: sync auth/logout, theme, cart. Watch ordering, the originating tab not receiving its own event, and serialization.
The browser will discard or crash the tab. Chrome shows the 'Aw, Snap!' page or silently kills background tabs to reclaim memory (tab discarding). Tabs in different processes don't take each other down (Site Isolation). Common causes: leaks (event listeners, detached DOM, big closures), unbounded caches, large textures/canvases, huge in-memory data.
V8 runs a generational, incremental, mostly-concurrent mark-and-sweep collector. GC is triggered by heap-size thresholds — minor GC (young generation, Scavenge) fires often and is fast; major GC (Mark-Compact, full heap) fires on memory pressure or idle. You can't manually trigger it from JS, only influence it by dropping references and avoiding accidental retainers (closures, listeners, detached DOM, globals).
SPAs leak memory when references survive after a route or component unmounts. Detached DOM, event listeners, timers, and closures are the usual suspects.
Identify: confirm growth in Chrome DevTools Memory tab — take heap snapshots over time, compare, look for detached DOM nodes and growing object counts; use the Performance monitor / allocation timeline. Common causes: uncleared timers/intervals, un-removed event listeners, lingering subscriptions, closures holding large objects, caches without eviction, detached DOM. Fix: clean up in effect teardown / componentWillUnmount, use WeakMap, bound caches.
V8 uses a generational, mostly-concurrent GC: young objects in a fast scavenger, survivors promoted to the old generation collected by mark-sweep-compact.
V8 builds hidden classes (maps) per object shape. `delete` mutates the shape and forces the object into slow dictionary mode, evicting it from inline-cache fast paths.
Reach for the right tool for the bug class: logical bugs → DevTools breakpoints + React DevTools; perf → Performance/Profiler; network → Network tab + replays; production → source maps, error tracking, Replay/Sentry.