Skip to content
The Web Platform (DOM, Events, Workers, Storage, APIs)

Platform & Language

Listen 0%
Speed

04 · The Web Platform (DOM, Events, Workers, Storage, APIs)

The set of browser-provided APIs your JavaScript actually calls. Frameworks abstract most of this, but every abstraction leaks, and the senior engineer knows what’s underneath: the DOM, the event system, the fetch/streams stack, the worker threads, and the storage layer.


Positioning

01 covered how the browser renders; 02 covered the language. This file is the API surface between them — what JavaScript can do to the page and the environment. React is, fundamentally, a system for producing DOM mutations efficiently; understanding the platform makes React (and its escape hatches) legible.


Deep dive

1. The DOM and CSSOM as programmable trees

  • The DOM is a live, in-memory tree of Nodes (Element, Text, Comment, DocumentFragment). It is language-agnostic (a W3C interface) implemented in C++ in the engine and exposed to JS via bindings — so DOM access crosses a boundary and isn’t free.
  • Traversal/manipulation: querySelector/querySelectorAll (static NodeList), getElementById, .children/.parentNode, closest(), matches(). Build off-DOM with DocumentFragment to batch insertions (one reflow instead of N).
  • <template> holds inert DOM you clone; the basis of efficient list rendering without a framework.
  • The CSSOM is the parallel tree for styles; getComputedStyle() reads resolved values (and forces layout — see thrashing in 01).
  • React’s virtual DOM is a userland diffing layer that batches and minimizes these (expensive) real-DOM operations. The platform’s own answer to imperative DOM pain is increasingly Web Components + declarative templates.

2. The event system

  • Flow: capture phase (root → target), target, bubble phase (target → root). addEventListener(type, fn, { capture, once, passive, signal }).
  • Delegation: attach one listener on a container and use event.target/closest() to handle many children. Scales better than per-node listeners and survives dynamic content. (React historically delegated to the root; since React 17 it attaches to the app root container, not document.)
  • passive: true for scroll/touch listeners tells the browser you won’t preventDefault, so it can scroll without waiting on JS — a real INP/scroll-jank win.
  • signal ties a listener’s lifetime to an AbortController — clean removal of many listeners at once.
  • Custom events: new CustomEvent('thing', { detail, bubbles, composed }) — the decoupled communication primitive, important for micro frontends and Web Components crossing shadow boundaries (composed: true).
  • event.preventDefault() (stop default action) vs stopPropagation() (stop the flow) vs stopImmediatePropagation() (also stop sibling listeners) — distinct and often confused.

3. Fetch, Streams, and request lifecycle

  • fetch(url, init) returns a Promise<Response>. It does not reject on HTTP errors (404/500) — only on network failure. Always check res.ok.
  • init carries method, headers, body, credentials ('include' for cross-origin cookies), mode (cors/no-cors/same-origin), cache, and signal for cancellation.
  • The body is a ReadableStream; res.json(), res.text(), res.blob(), res.arrayBuffer() consume it once. You can read the stream manually for progressive rendering (this underlies SSR streaming and RSC payloads — see 07).
  • Streams API (ReadableStream/WritableStream/TransformStream) enables backpressure-aware pipelines — decoding, on-the-fly transforms, server-sent token streams for AI UIs.
  • For real-time: EventSource (Server-Sent Events, one-way server→client, auto-reconnect, simple) vs WebSocket (bidirectional, binary-capable). Compared in 18.

4. Workers: escaping the single thread

JS is single-threaded per context, but the platform gives you real OS threads via workers — each with its own event loop, no shared scope, communicating by message passing (structured clone) or SharedArrayBuffer (true shared memory, gated behind COOP/COEP headers for Spectre safety).

  • Dedicated Web Worker — offload CPU-heavy work (parsing, crypto, image processing, heavy reducers) so the main thread stays responsive (protects INP). postMessage in/out; transfer large buffers with Transferable Objects to avoid copying.
  • Shared Worker — one worker shared across tabs/windows of the same origin (e.g., a single websocket multiplexed to many tabs).
  • Service Worker — a programmable network proxy that sits between the page and the network. Powers offline (Cache API), background sync, push notifications, and precaching. It’s event-driven, has no DOM access, and its lifecycle (install → activate → fetch) is the trickiest part. The foundation of PWAs.
  • Worklets (Paint/Audio/Animation) — tiny, high-priority workers for the rendering/audio pipeline.
// main thread
const worker = new Worker(new URL('./heavy.js', import.meta.url), { type: 'module' });
worker.postMessage({ rows });
worker.onmessage = (e) => render(e.data);

// heavy.js — runs off the main thread; never blocks rendering or input
self.onmessage = ({ data }) => self.postMessage(crunch(data.rows));

5. The Observer family (efficient, async, off the hot path)

Polling and scroll handlers are the old, janky way. The platform provides observers that batch callbacks and run them efficiently:

  • IntersectionObserver — “is this element in (or near) the viewport?” Powers lazy-loading images, infinite scroll, and visibility analytics without scroll listeners.
  • ResizeObserver — element-size changes (container queries before CSS had them; responsive components).
  • MutationObserver — DOM-tree changes (async microtask-timed); for integrating with third-party DOM or detecting injected nodes.
  • PerformanceObserver — surfaces performance entries (LCP, CLS, long tasks, INP) — the API behind the web-vitals library (see 15).

6. Storage layer

APISync?SizeTypeUse for
Cookiessync~4 KBstringAuth tokens (HttpOnly), sent every request
localStoragesync (blocks)~5 MBstringSmall prefs; avoid hot paths
sessionStoragesync~5 MBstringPer-tab ephemeral state
IndexedDBasynclarge (quota-based)structuredThe real client DB; offline data, caches
Cache APIasync (promise)quotaRequest/ResponseService-worker offline assets

Wrap IndexedDB with idb (Jake Archibald) or Dexie — the raw API is event-based and painful. Respect the Storage quota and navigator.storage.estimate().

7. Web Components (the platform’s component model)

Framework-independent components built from three specs:

  • Custom Elementsclass X extends HTMLElement { connectedCallback() {} }, register with customElements.define('x-thing', X). Lifecycle callbacks mirror framework lifecycles.
  • Shadow DOM — encapsulated subtree with scoped styles (attachShadow({ mode: 'open' })), <slot> for composition. True style/markup isolation — relevant for micro frontends and design systems that must work across frameworks (09, 11).
  • HTML Templates<template>/<slot> for declarative, inert markup.

Web Components interoperate with React, Vue, Angular — making them the lingua franca for cross-framework design systems (e.g., a tokens-driven component library shipped once, consumed everywhere). Libraries like Lit make them ergonomic.

8. Other platform capabilities worth knowing

  • History/Navigation APIpushState/replaceState, the new Navigation API (cleaner SPA routing primitive).
  • Web Crypto (crypto.subtle) — hashing, signing, key derivation; the secure way to do crypto (used in MFE message-bus integrity, token handling).
  • structuredClone() — deep clone honoring the structured-clone algorithm (handles Maps, Dates, typed arrays; not functions).
  • requestAnimationFrame / requestIdleCallback / scheduler.postTask / scheduler.yield — cooperative scheduling primitives (see 01 and 05).
  • Permissions, Geolocation, Notifications, Clipboard, File System Access, WebRTC, WebGL/WebGPU, WebAssembly — capability APIs; WASM in particular lets you run Rust/C++ at near-native speed in the browser (heavy compute, codecs, the engines behind some bundlers).
  • <dialog>, Popover API, View Transitions API — modern declarative UI primitives that replace a lot of JS (View Transitions enables animated DOM-state changes and SPA-like page transitions; React 19.2 exposes <ViewTransition>).

Worked example: lazy-load images and infinite scroll without scroll handlers

const io = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    if (!entry.isIntersecting) continue;
    const img = entry.target;
    img.src = img.dataset.src;     // swap in the real source
    io.unobserve(img);             // one-shot
  }
}, { rootMargin: '200px' });       // start loading 200px before visible

document.querySelectorAll('img[data-src]').forEach((img) => io.observe(img));

No scroll listener, no layout thrash, batched callbacks — the platform-native pattern that frameworks’ loading="lazy" and virtualization libraries build on.


Pitfalls & gotchas

  • Assuming fetch rejects on 404/500 — it doesn’t; check res.ok.
  • Consuming a response body twice (it’s a one-shot stream) — clone or read once.
  • localStorage on the hot path — synchronous, blocks the main thread; use IndexedDB.
  • Forgetting to remove listeners/observers — leaks (tie to AbortController/cleanup).
  • Service-worker lifecycle confusion — stale caches because activate/skipWaiting/clients.claim weren’t handled.
  • Heavy synchronous work on the main thread when a Web Worker would keep the UI responsive.
  • Shadow DOM event retargeting — events look like they come from the host unless composed: true.

Interview / self-test questions

  1. Event capture vs bubble vs target; what does delegation buy you?
  2. Does fetch reject on HTTP 500? How do you cancel a fetch? (No; AbortController.)
  3. When would you reach for a Web Worker vs a Service Worker vs a Shared Worker?
  4. How do you implement infinite scroll without a scroll listener? (IntersectionObserver.)
  5. Compare localStorage, IndexedDB, and the Cache API.
  6. What problem does Shadow DOM solve, and why does it matter for design systems / micro frontends?
  7. SSE vs WebSocket — when each?
  8. What is a Transferable Object and why does it matter for worker performance?
  9. Explain the service-worker lifecycle and a common stale-cache bug.

Recommendations

  • Prefer observers over scroll/resize/poll handlers; prefer passive listeners.
  • Move CPU-bound work to Web Workers to protect INP.
  • Use IndexedDB (via idb/Dexie) for anything beyond tiny string prefs.
  • Validate and check res.ok; always pass signal for cancelable requests (pairs with React Query/Server Components).
  • Lean on modern declarative primitives (<dialog>, Popover, View Transitions) before writing JS.
  • Tie every subscription/listener/observer to a cleanup (component unmount / AbortController).

Books & references

  • MDN Web Docs — the authoritative reference for every API here; the platform’s textbook.
  • web.dev (Chrome team) — best practical guides on Service Workers/PWAs, Streams, View Transitions, and the Observer APIs.
  • “Going Offline” — Jeremy Keith. The clearest book on Service Workers and offline-first.
  • “Web Components in Action” — Ben Farrell; plus lit.dev docs for the ergonomic modern approach.
  • “JavaScript: The Definitive Guide” (Flanagan) — has thorough platform-API chapters.
  • whatwg.org specs (DOM, Fetch, HTML, Streams) — primary sources when MDN isn’t precise enough.

Connections

Frontend Deep-Dive Library · content is the single source of truth.