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(staticNodeList),getElementById,.children/.parentNode,closest(),matches(). Build off-DOM withDocumentFragmentto 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 in01). - 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, notdocument.) passive: truefor scroll/touch listeners tells the browser you won’tpreventDefault, so it can scroll without waiting on JS — a real INP/scroll-jank win.signalties a listener’s lifetime to anAbortController— 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) vsstopPropagation()(stop the flow) vsstopImmediatePropagation()(also stop sibling listeners) — distinct and often confused.
3. Fetch, Streams, and request lifecycle
fetch(url, init)returns aPromise<Response>. It does not reject on HTTP errors (404/500) — only on network failure. Always checkres.ok.initcarriesmethod,headers,body,credentials('include'for cross-origin cookies),mode(cors/no-cors/same-origin),cache, andsignalfor 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 — see07). - 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 in18.
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).
postMessagein/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 theweb-vitalslibrary (see15).
6. Storage layer
| API | Sync? | Size | Type | Use for |
|---|---|---|---|---|
| Cookies | sync | ~4 KB | string | Auth tokens (HttpOnly), sent every request |
| localStorage | sync (blocks) | ~5 MB | string | Small prefs; avoid hot paths |
| sessionStorage | sync | ~5 MB | string | Per-tab ephemeral state |
| IndexedDB | async | large (quota-based) | structured | The real client DB; offline data, caches |
| Cache API | async (promise) | quota | Request/Response | Service-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 Elements —
class X extends HTMLElement { connectedCallback() {} }, register withcustomElements.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 API —
pushState/replaceState, the newNavigation 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 (see01and05).- 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
fetchrejects on 404/500 — it doesn’t; checkres.ok. - Consuming a response body twice (it’s a one-shot stream) — clone or read once.
localStorageon 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.claimweren’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
- Event capture vs bubble vs target; what does delegation buy you?
- Does
fetchreject on HTTP 500? How do you cancel a fetch? (No;AbortController.) - When would you reach for a Web Worker vs a Service Worker vs a Shared Worker?
- How do you implement infinite scroll without a scroll listener? (
IntersectionObserver.) - Compare localStorage, IndexedDB, and the Cache API.
- What problem does Shadow DOM solve, and why does it matter for design systems / micro frontends?
- SSE vs WebSocket — when each?
- What is a Transferable Object and why does it matter for worker performance?
- Explain the service-worker lifecycle and a common stale-cache bug.
Recommendations
- Prefer observers over scroll/resize/poll handlers; prefer
passivelisteners. - 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 passsignalfor 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
01-browser-engines-and-rendering.md— what DOM mutations cost; the render loop these APIs feed.02-javascript-deep-dive.md— promises/AbortController/event loop these APIs build on.05-react-internals-and-patterns.md— React as a DOM-mutation engine;useSyncExternalStorebridges platform stores.09-micro-frontends.md&11-design-systems.md— Web Components / Shadow DOM as cross-framework substrate.15-performance-and-core-web-vitals.md—PerformanceObserverand theweb-vitalslibrary.