public/js/main.js
// The lab's router: one ES module per station, loaded with dynamic import()
// on first visit (§16). Vitals and error capture start here, at page load.
import { h, $, ms } from "./lib/dom.js";
import { missions, MISSIONS } from "./lib/missions.js";
import "./lib/vitals.js";
import "./lib/errors.js";
export const STATIONS = [
{ id: "overview", icon: "🗺️", title: "The big picture" },
{ id: "network", icon: "🌐", title: "URL to bytes: TLS & HTTP/2" },
{ id: "render", icon: "🎨", title: "The rendering pipeline" },
{ id: "eventloop", icon: "🔁", title: "The event loop" },
{ id: "engine", icon: "⚙️", title: "Inside V8" },
{ id: "threads", icon: "🧵", title: "Workers & threads" },
{ id: "storage", icon: "🗄️", title: "IndexedDB & storage" },
{ id: "vitals", icon: "📈", title: "Core Web Vitals" },
{ id: "debug", icon: "🐞", title: "Debugging lab" },
];
const stage = $("#stage");
let unmount = null;
function renderNav(active) {
$("#station-nav").replaceChildren(
...STATIONS.map((s, i) => {
const p = missions.progress(s.id);
return h("li", {},
h("a", { href: `#/${s.id}`, "aria-current": s.id === active ? "page" : false },
h("span", { class: "num" }, String(i).padStart(2, "0")),
h("span", { class: "icon", "aria-hidden": "true" }, s.icon),
h("span", { class: "label" }, s.title),
h("span", { class: `badge ${p.done === p.total ? "complete" : ""}`, title: "missions done" }, `${p.done}/${p.total}`)));
}),
);
const all = missions.progress();
$("#progress-fill").style.width = `${(all.done / all.total) * 100}%`;
$("#progress-text").textContent = `${all.done} of ${all.total} missions`;
}
async function route() {
const id = location.hash.replace(/^#\/?/, "") || "overview";
const meta = STATIONS.find((s) => s.id === id) ?? STATIONS[0];
renderNav(meta.id);
unmount?.();
unmount = null;
const t0 = performance.now();
// Each station is its own module: watch them arrive in the Network panel.
const mod = await import(`./stations/${meta.id}.js`);
const loadMs = performance.now() - t0;
const view = await mod.mount({ meta, loadMs });
stage.replaceChildren(view.el);
unmount = view.unmount ?? null;
document.title = `${meta.title} · Browser Lab`;
stage.focus({ preventScroll: true });
stage.scrollTo?.(0, 0);
// A User Timing mark: it shows up in DevTools' Performance panel (§83).
performance.mark("station-shown", { detail: { station: meta.id, loadMs } });
}
addEventListener("hashchange", route);
missions.onChange(() => renderNav(location.hash.replace(/^#\/?/, "") || "overview"));
$("#reset-missions").addEventListener("click", () => missions.reset());
const nav = performance.getEntriesByType("navigation")[0];
$("#proto").textContent = `${nav?.nextHopProtocol || "?"} · ${location.protocol === "https:" ? "TLS" : "plain-text"} · ${ms(nav?.responseStart)} TTFB`;
route();
// Expose a tiny API for the Playwright suite and for poking around in the console.
window.lab = { missions, MISSIONS, STATIONS };