Browser, DOM, modules & tooling
Questions in this set 10
- 01Walk me through what happens when you type a URL and press enter.
- 02Explain defer vs async vs a plain <script>.
- 03CommonJS vs ES modules.
- 04localStorage vs sessionStorage vs cookies vs IndexedDB.
- 05Explain CORS. Why does my request fail with "No 'Access-Control-Allow-Origin' header"?
- 06What is the difference between innerHTML, textContent and innerText?
- 07How do you avoid layout thrashing?
- 08What is the difference between Map/Set and plain objects/arrays?
- 09What are Web Workers and Service Workers?
- 10How does JavaScript garbage collection work, and what causes leaks?
Walk me through what happens when you type a URL and press enter.
The canonical whiteboard question. Structure the answer in stages and go as deep as they push:
- URL parse → scheme, host, path. Browser checks HSTS preload (may force https).
- DNS — browser cache → OS cache → resolver → root/TLD/authoritative. Returns an IP (or a CDN's anycast IP).
- TCP handshake (or QUIC over UDP for HTTP/3), then TLS — ClientHello/ServerHello, certificate validation against the trust store, key exchange, ALPN negotiates HTTP/2 or /3. TLS 1.3 does this in one round trip, zero with resumption.
- HTTP request → CDN edge / load balancer → app server; response with status, headers (
cache-control,content-encoding,set-cookie), body. - Render: parse HTML → DOM; CSS → CSSOM (render-blocking); scripts block the parser unless
defer/async/type=module. DOM + CSSOM → render tree → layout (geometry) → paint → composite (GPU layers). - Post-load:
DOMContentLoadedafter parsing + deferred scripts;loadafter all subresources; hydration if it is an SSR framework app.
Mention the metrics that follow from this: TTFB (steps 1-4), FCP, LCP (largest element painted), CLS (layout stability), INP (interaction responsiveness).
Explain defer vs async vs a plain <script>.
| downloads | executes | order preserved | blocks parser | |
|---|---|---|---|---|
<script> |
immediately | immediately | yes | yes |
async |
parallel | as soon as it lands | no | briefly, on execution |
defer |
parallel | after parsing, before DOMContentLoaded |
yes | no |
type="module" |
parallel | deferred by default | yes | no |
async for independent things (analytics); defer for app code that needs the DOM and depends on order. Scripts at the end of <body> are the legacy version of defer.
CommonJS vs ES modules.
CJS (require/module.exports) is synchronous, dynamic (you can require conditionally), and resolves at runtime — its exports are a mutable object. ESM (import/export) is static: the dependency graph is known before evaluation, which enables tree-shaking, cyclic-import handling via live bindings, and top-level await.
import { a } from "./m.js"; // hoisted, static, live binding
const m = await import("./m.js"); // dynamic import: async, code-splitting pointPractical friction to mention: you can import CJS from ESM (default-only interop), but not require ESM synchronously (Node 22+ can, under conditions); __dirname does not exist in ESM (import.meta.dirname); and package "type": "module" plus exports maps decide which one a file is.
localStorage vs sessionStorage vs cookies vs IndexedDB.
| size | lifetime | sent to server | API | |
|---|---|---|---|---|
| cookie | ~4 KB | Expires/Max-Age |
yes, every request | string |
| localStorage | ~5-10 MB | until cleared | no | sync, string |
| sessionStorage | ~5-10 MB | per tab | no | sync, string |
| IndexedDB | large (quota-based) | until cleared | no | async, structured |
Security: never store a session token in localStorage — any XSS reads it. Use an HttpOnly; Secure; SameSite=Lax cookie so JavaScript cannot touch it. localStorage is also synchronous and blocks the main thread; for anything sizeable use IndexedDB (or a wrapper like idb).
Explain CORS. Why does my request fail with "No 'Access-Control-Allow-Origin' header"?
The same-origin policy blocks a page from reading responses from a different origin (scheme+host+port). CORS is the server's way to opt in. For "non-simple" requests (custom headers, PUT/DELETE, Content-Type: application/json) the browser first sends an OPTIONS preflight; the server must answer with Access-Control-Allow-Origin/Methods/Headers, and Access-Control-Max-Age to cache that answer.
Key points that separate a good answer:
- CORS is enforced by the browser, on the response. The request often reaches your server and executes — so it is not a substitute for authorisation. curl and server-to-server calls are unaffected.
- For cookies you need
credentials: "include"on the client andAccess-Control-Allow-Credentials: truewith an explicit origin (*is rejected) on the server. - The fix is a server config change, never a frontend one. A dev proxy (Vite's
server.proxy) sidesteps it locally by making the request same-origin.
What is the difference between innerHTML, textContent and innerText?
innerHTML parses markup — an XSS hole with untrusted data. textContent gets/sets raw text of all nodes including hidden ones, and is fast. innerText is layout-aware (respects CSS visibility, collapses whitespace) and therefore triggers reflow when read — a real performance bug in loops. Default to textContent.
How do you avoid layout thrashing?
Reading a layout property (offsetHeight, getBoundingClientRect, scrollTop, getComputedStyle) forces the browser to flush pending style/layout work. Interleaving reads and writes in a loop causes a synchronous layout per iteration.
// BAD: read/write/read/write -> forced synchronous layout each time
els.forEach(el => { el.style.height = el.offsetHeight * 2 + "px"; });
// GOOD: batch reads, then batch writes
const heights = els.map(el => el.offsetHeight);
els.forEach((el, i) => { el.style.height = heights[i] * 2 + "px"; });Also: animate transform/opacity (compositor-only, no layout or paint), use content-visibility/will-change judiciously, and prefer IntersectionObserver and ResizeObserver over scroll/resize handlers that measure.
What is the difference between Map/Set and plain objects/arrays?
Map keys can be any type (including objects), preserves insertion order, has a real size, and is optimised for frequent adds/deletes; objects have string/symbol keys, a prototype chain (watch for __proto__ and prototype-pollution), and are better for records and JSON. Set gives O(1) membership and dedup ([...new Set(arr)]). WeakMap/WeakSet hold keys weakly — the right tool for attaching metadata to DOM nodes or objects without leaking them.
What are Web Workers and Service Workers?
Web Worker: a background thread with no DOM access, communicating via postMessage (structured clone) — for CPU work like parsing, compression, image processing. Service Worker: a proxy between the page and the network, event-driven, for offline caching, background sync and push. It has a lifecycle (install → activate → fetch), can serve stale content if you get the caching strategy wrong, and must be served over HTTPS. Do not put a service worker on a site until you understand cache invalidation — a bad one can pin users to a broken build.
How does JavaScript garbage collection work, and what causes leaks?
A mark-and-sweep collector traces from roots (globals, stack, active closures); anything unreachable is freed, with generational/incremental refinements in V8. Common leaks: forgotten timers and setInterval, listeners not removed on teardown, closures holding large objects, detached DOM nodes still referenced by JS, and unbounded caches/arrays used as logs. Find them with Chrome DevTools Memory → heap snapshots taken before/after an action, comparing retained size and looking for "Detached HTMLDivElement".