public/js/stations/overview.js
// Station 0: the browser's process architecture, and a guided tour of what
// happens between a click and the pixels changing (§18, §22, §79).
import { h, station, panel, button, sleep, ms, bytes } from "../lib/dom.js";
import { missions, missionList } from "../lib/missions.js";
const PROCS = [
{ id: "browser", title: "Browser process", threads: ["UI thread", "IO thread"], notes: ["Tabs, address bar, permissions", "Routes input to the right renderer"] },
{ id: "network", title: "Network service", threads: ["IO thread"], notes: ["DNS, TCP/QUIC, TLS", "HTTP cache, cookies"] },
{ id: "renderer", title: "Renderer process (one per site)", threads: ["Main thread", "Compositor thread", "Raster workers"], notes: ["V8 + Blink: JS, DOM, style, layout, paint", "Sandboxed: no direct disk or network"] },
{ id: "gpu", title: "GPU process", threads: ["Viz / display compositor"], notes: ["Draws composited layers", "Talks to the OS graphics stack"] },
];
const TOUR = [
["browser", "UI thread", "1. The OS delivers your click to the browser process. It hit-tests the tab and forwards the event over IPC to that tab's renderer."],
["renderer", "Compositor thread", "2. The renderer's compositor thread gets it first. If no listener is on that spot (or listeners are passive), it could scroll on its own without the main thread."],
["renderer", "Main thread", "3. There is a click listener, so the event is queued as a task for the main thread. It runs when the current task finishes. A long task here is what INP measures."],
["renderer", "Main thread", "4. Your handler runs in V8, then the microtask checkpoint, then (at the next frame) style → layout → paint record. Same thread, one after another."],
["renderer", "Raster workers", "5. Paint produced a list of draw commands. Raster threads turn tiles of it into bitmaps (or GPU textures)."],
["renderer", "Compositor thread", "6. The compositor thread assembles layers into a compositor frame. Transform/opacity animations live here, off the main thread."],
["gpu", "Viz / display compositor", "7. The GPU process draws the frame and hands it to the OS at the next vsync, usually within 16.7ms at 60Hz."],
];
function facts() {
const nav = performance.getEntriesByType("navigation")[0];
const conn = navigator.connection;
const rows = [
["Engine (brands)", navigator.userAgentData?.brands?.map((b) => `${b.brand} ${b.version}`).join(", ") ?? navigator.userAgent.slice(0, 80)],
["Logical CPU cores", navigator.hardwareConcurrency],
["Device memory (approx)", navigator.deviceMemory ? `${navigator.deviceMemory} GB` : "not exposed"],
["Page protocol", nav?.nextHopProtocol || "unknown"],
["Secure context", String(isSecureContext)],
["Cross-origin isolated", String(crossOriginIsolated)],
["WebGPU", "gpu" in navigator ? "available" : "not available"],
["Network (estimate)", conn ? `${conn.effectiveType}, ~${conn.downlink} Mbps, rtt ~${conn.rtt}ms` : "not exposed"],
["Device pixel ratio", devicePixelRatio],
["Perf entry types", (PerformanceObserver.supportedEntryTypes ?? []).length],
];
const dl = h("dl", { class: "kv" }, rows.map(([k, v]) => [h("dt", {}, k), h("dd", {}, String(v))]));
navigator.storage?.estimate?.().then(({ usage, quota }) => dl.append(h("dt", {}, "Storage used / quota"), h("dd", {}, `${bytes(usage)} / ${bytes(quota)}`)));
if (nav) dl.append(h("dt", {}, "This page: TTFB / DOMContentLoaded"), h("dd", {}, `${ms(nav.responseStart)} / ${ms(nav.domContentLoadedEventEnd)}`));
return dl;
}
export function mount() {
const boxes = PROCS.map((p) =>
h("div", { class: "proc", "data-proc": p.id },
h("h4", {}, p.title),
h("ul", {}, p.notes.map((n) => h("li", {}, n))),
p.threads.map((t) => h("div", { class: "thread", "data-thread": t }, "🧵 ", t))),
);
const caption = h("p", { class: "tour-caption", "aria-live": "polite" }, "Press play to follow one click through the browser.");
let cancelled = false;
const play = async () => {
playBtn.disabled = true;
for (const [proc, thread, text] of TOUR) {
if (cancelled) return;
boxes.forEach((b) => b.classList.toggle("lit", b.dataset.proc === proc));
boxes.forEach((b) => b.querySelectorAll(".thread").forEach((t) => t.classList.toggle("lit", b.dataset.proc === proc && t.dataset.thread === thread)));
caption.textContent = text;
await sleep(2300);
}
boxes.forEach((b) => b.classList.remove("lit"));
caption.textContent = "That round trip is usually a few milliseconds. When it isn't, the culprit is almost always step 3 or 4: the main thread was busy.";
playBtn.disabled = false;
missions.complete("overview", "tour");
};
const playBtn = button("▶ Play the tour of a click", play, { class: "primary" });
const el = station(
"The big picture",
"A browser is a small operating system: several sandboxed processes, each with its own threads, talking over IPC. Every other station zooms into one box on this map.",
missionList("overview"),
panel("Processes and threads", h("div", { class: "arch" }, boxes), h("div", { class: "row" }, playBtn), caption),
h("div", { class: "grid-2" },
panel("Your browser, right now", facts(), h("p", { class: "note" }, "All read live from Web APIs. Cross-origin isolation is on because the server sends COOP + COEP headers.")),
panel("Where to go next",
h("ol", {},
h("li", {}, h("a", { href: "#/network" }, "URL to bytes"), ": DNS, TLS 1.3, HTTP/1.1 vs HTTP/2, measured."),
h("li", {}, h("a", { href: "#/render" }, "Rendering"), ": style → layout → paint → composite, and how to skip stages."),
h("li", {}, h("a", { href: "#/eventloop" }, "Event loop"), ": step through tasks and microtasks, then check against the real thing."),
h("li", {}, h("a", { href: "#/engine" }, "Inside V8"), ": bytecode, tiers, hidden classes, inline caches."),
h("li", {}, h("a", { href: "#/threads" }, "Threads"), ": workers, transfer, SharedArrayBuffer races."),
h("li", {}, h("a", { href: "#/storage" }, "Storage"), ": IndexedDB transactions you can watch."),
h("li", {}, h("a", { href: "#/vitals" }, "Core Web Vitals"), ": break LCP/INP/CLS and fix them."),
h("li", {}, h("a", { href: "#/debug" }, "Debugging"), ": errors, CSP reports, leaks, DevTools recipes.")))),
);
return { el, unmount: () => (cancelled = true) };
}