Learn Frontend Engineering
How user intent becomes interactive, accessible, performant pixels. Thirty-five modules, from what the browser actually is to debugging a production frontend.
Browser Fundamentals
6 lessonsThe browser is a runtime, not a document viewer: networking, parsers, a JavaScript engine, a layout engine, a paint system, a compositor, storage and a security sandbox, split across processes that fail independently.
Not a document viewer with scripting bolted on: a sandboxed application platform with a scheduler, a memory model, a renderer and a security boundary.
URL to pixels: resolution, connection, request, streaming HTML, DOM and CSSOM, style, layout, paint, composite — and what blocks what.
Browser, renderer, network, GPU and utility processes — why a page can hang without taking the browser with it, and what that means for your code.
Intent, event, state, logic, DOM, network, layout, paint, pixels, feedback — the chain every lesson in this domain is a zoom into.
Scheme, host and port together form the unit of trust — and nearly every confusing browser restriction is that boundary being enforced.
The boundary that keeps this domain honest: the browser-executed application and the interaction a person feels — and where everything else properly lives.
HTML & Semantics
6 lessonsStructured content with behaviour attached. What a `button` gives you that a clickable `div` silently takes away — keyboard, focus, defaults, assistive technology, tooling.
Choosing an element selects a role, a place in the tab order, a set of keyboard defaults, an activation behaviour and an entry in the accessibility tree. `div` selects none of them.
Landmarks, headings and source order are the navigation system most of your users never see — and the one CSS can silently disagree with.
Activation behaviour, form participation, implicit submission, the top layer and constraint validation — the platform features most component libraries reimplement, more slowly and less completely.
Charset, viewport, lang, title and the link relations decide how the rest of the document is decoded, sized, named, shared and blocked — before a single pixel of body content exists.
Replaced elements bring their own dimensions, their own bytes and their own decode memory — and nearly every problem they cause is fixed with an attribute rather than with code.
Nobody sets out to write forty nested divs. They arrive one justified wrapper at a time, and the bill is paid in accessibility, style recalculation and JavaScript you had to write yourself.
HTML Parsing & Script Loading
6 lessonsBytes to tokens to a tree, streaming as it arrives — and what `script`, `defer`, `async` and `type=module` actually do to that stream.
Bytes become characters become tokens, through a specified state machine that has no fatal errors and that the tree builder can reach in and reconfigure.
Tokens become a DOM through insertion modes and a stack of open elements — which is why the tree the browser built is frequently not the markup you wrote.
The parser starts on the first chunk and never waits for the last one — which makes time to first byte, flush behaviour and document order performance decisions rather than server details.
A classic `<script>` suspends tree construction because the script may write into the document at that exact point — and it also waits for pending stylesheets it may never touch.
Three genuinely different orderings: `defer` keeps document order and runs before DOMContentLoaded, `async` runs whenever it arrives with no order guarantee, and modules are deferred by default.
A second, lightweight reader runs ahead of the real parser looking for URLs to fetch — and almost every modern loading pattern accidentally hides resources from it.
The DOM
6 lessonsThe browser's live object model of the document: nodes, attributes, listeners, computed style, layout boxes and the accessibility semantics derived from all of it.
A tree of live objects the parser built from your markup and script has since diverged from — each node carrying attributes, properties, listeners, computed style, a layout box and an accessibility role.
Which changes invalidate style, which force layout, which repaint and which the compositor can absorb — and why the honest answer to most of them is "it depends what else is on the page".
Some queries return a snapshot, some return a view that keeps changing under you, and some are not questions about the tree at all — they force layout to answer.
Focus, selection, caret position, scroll, animation progress and uncontrolled input values live on the node object — so whether an update reuses a node or replaces it is a visible product decision.
A second tree attached to a host element: styles do not cross, queries do not cross, events retarget — and id-based accessibility relationships break in ways nothing warns you about.
Removing a node from the document frees one reference, not the node — and because nodes point at their parents, siblings and children, one surviving reference retains an entire subtree.
CSS & CSSOM
6 lessonsCascade, specificity, inheritance and computed style as a resolution algorithm you can reason about, rather than a fight you win with `!important`.
Stylesheet bytes become an indexed object model before they become style: how rules are stored for fast lookup, why CSS blocks rendering, and what each runtime way of changing style actually costs.
Origin and importance, then context, then element-attached styles, then layers, then specificity, then order — a six-step sort that decides every property on every element, in that order.
A three-part tuple compared left to right, not a score. What counts, what deliberately counts as zero, and how `:where()` and `:is()` turn specificity into something you choose.
From declared value to cascaded, specified, computed, used and actual value — which properties inherit, what `inherit`, `initial`, `unset` and `revert` each mean, and why reading style back can force layout.
Inherited properties holding a token stream, substituted at computed-value time — which makes them dynamic, scopeable per element, and capable of invalidating a very large subtree at once.
How matching actually works, why selector performance is almost never the bottleneck in a modern engine, and what the real cost is: how many elements a change forces the browser to restyle.
The Rendering Pipeline
6 lessonsDOM plus CSSOM to style to layout to paint to composite — and the invalidation rules that decide which of those stages a given change actually costs you.
DOM plus CSSOM become computed styles, then geometry, then paint commands, then a composited frame — and most updates re-run only part of that.
Selector matching, cascade resolution and inheritance turn every rule you shipped into exactly one computed value per property, per element.
A change does not recalculate the document — it dirties a set of elements. The size of that set is the cost, and your selectors are what decide it.
The table you should be able to reconstruct from first principles: which stages each common change invalidates, and why the honest answer is so often "it depends".
`contain` is a promise you make to the engine — nothing inside this box affects anything outside it — and in exchange, invalidation stops at the boundary.
Ask the engine to skip rendering work for content nobody is looking at yet — and take on responsibility for its size, its scrollbar and whether anyone can find it.
Layout
7 lessonsGeometry: the box model, normal flow, flexbox, grid, positioning, intrinsic sizing, overflow — and the read-write-read pattern that forces the browser to compute it all again.
Every element is four nested edges — content, padding, border, margin — plus a sizing mode that decides which of them `width` is actually describing.
What the browser already does before you choose a layout mode: block and inline formatting, margins that merge, and the moment a box becomes a scroll container.
A distribution algorithm, not a property list: base sizes, free space, grow and shrink along the main axis, alignment along the cross axis.
The container declares tracks and lines; items are placed into the cells between them. Rows can finally align to columns, because the layout — not the content — owns the sizes.
Taking a box out of flow: which ancestor it is positioned against, and why `z-index: 9999` still loses to a header with `z-index: 1`.
What a box wants to be when nobody tells it: min-content, max-content, fit-content — and the `min-width: auto` rule that makes flex and grid items overflow.
Read a layout property, write a style, read again: each read forces the browser to recompute the geometry you just invalidated, synchronously, inside your loop.
Paint & Compositing
6 lessonsPaint commands, layers, the compositor thread and the frame budget. Why `transform` and `opacity` are often cheaper to animate, stated as a mechanism rather than a slogan.
Painting does not produce pixels directly: it records an ordered list of drawing commands per layer, which a rasteriser later turns into bitmaps.
A layer is a separately rasterised surface the compositor can transform and blend on its own thread — powerful, conditional, and paid for in memory.
Why `transform` and `opacity` can be driven by the compositor without the main thread — stated as a mechanism, with the conditions under which it is simply not true.
A display refreshing 60 times a second gives roughly 16.7ms per frame — a useful baseline that shrinks on faster displays and is shared with the browser's own work.
Scrolling is handled by the compositor when it can be, and by the main thread when your code forces it — which is why one listener can make a whole page feel broken.
Promoting everything with `will-change` trades main-thread paint for GPU memory and per-frame compositing — and past a small number of layers that trade inverts.
The Browser Event Loop
7 lessonsCall stack, task, microtask checkpoint, rendering opportunity. The scheduling model that explains why a synchronous loop freezes the page and a promise chain does not yield a frame.
One call stack, task queues, a microtask checkpoint that drains to empty, and a rendering opportunity between tasks — the model that predicts any ordering puzzle.
Where tasks come from, why each one runs to completion, and why `setTimeout(fn, 0)` is neither zero nor a promise about when.
Drained to empty after every callback, including microtasks queued during the drain — which is exactly why an unbounded promise chain freezes a page with no long task to blame.
A frame can only be produced between tasks, after the microtask checkpoint — which is the mechanical reason a promise chain never lets the browser paint.
One task that runs long blocks input, rendering and accessibility-tree updates at the same time — which is why "the page froze" is one symptom with one cause.
Script, DOM, style, layout, event dispatch and the accessibility tree share one thread — and knowing what is not on it is what makes moving work possible.
Breaking work into pieces the loop can get between — with honest limits: zero is not zero, idle callbacks are not everywhere, and the newer scheduling APIs are not yet universal.
Workers & Off-Main-Thread Work
5 lessonsMoving CPU work off the thread that owns the DOM: message passing, structured clone, transferables, and the isolation requirements the sharper primitives carry.
A second JavaScript realm with its own event loop and its own heap, and no access to the document at all — which is the design, not a missing feature.
`postMessage` is a one-way, asynchronous, unbounded queue with no return value — every request/response protocol on top of it is one you wrote.
Cloning copies — cost proportional to size, paid synchronously on both threads. Transferring moves ownership — near-free, and the sender loses access entirely.
`SharedArrayBuffer` gives two threads one block of memory — and requires COOP and COEP headers that most pages cannot adopt without breaking their third-party embeds.
Workers cost startup, message overhead, a second bundle and much harder debugging — and most slow frontends are not CPU-bound, so a worker fixes nothing.
Browser Events
6 lessonsDispatch, capture, target, bubble. Delegation, default actions, pointer and keyboard input, and why cancelling the wrong thing breaks scrolling or accessibility.
Hit-test to a target, build the propagation path, then walk it: capture down, at target, bubble up — with the path frozen before any listener runs.
One listener on a container instead of one per row: fewer registrations, no rebinding after a re-render, and a matching step you now own.
Two operations that share nothing: one cancels what the browser was about to do, the other stops the event travelling. Reaching for the wrong one breaks somebody else's feature, silently.
One event model for mouse, touch and pen — plus pointer capture, gesture cancellation, and the unrelated CSS property that shares the name.
Why Enter and Space activate a native button on different events, why a clickable div gets neither, and why `key` and `code` answer different questions.
A `touchstart` or `wheel` listener can hold a scroll hostage until it has run. `{ passive: true }` is a promise not to cancel — and browsers now assume it in places, which changes what your code does.
Forms
6 lessonsThe platform's oldest interactive component: input types, native validation, submission, focus order, labels — and what you inherit for free before any framework state exists.
A `<form>` ships with submit-on-Enter, validation, autofill, password-manager integration and a label/control relationship. Most custom forms are a worse reimplementation of it.
The right `type`, `inputmode` and `autocomplete` change the on-screen keyboard, the autofill offer and the validation the browser runs — a large UX win for one attribute.
The Constraint Validation API gives you checks, states and messages for free — then runs out at styling, wording and timing. Replacing it means re-implementing what it did well.
Either your framework owns every keystroke or the DOM does and you read at submit. A real trade-off between per-keystroke render cost and reactive capability — not a rule.
GET versus POST is a semantic choice with cache and history consequences; `FormData` and `enctype` decide what goes on the wire; and preventing the second submit is your job.
An error must be programmatically tied to its field, announced when it appears, reachable by focus and expressed in more than colour. Red text is not an error state.
Accessibility
8 lessonsDOM plus semantics plus ARIA becomes an accessibility tree that assistive technology reads. Keyboard operation, focus management and announcement are engineering requirements, not a later pass.
The browser derives a second tree from the DOM — role, name, state and relationships — and hands it to the platform. That tree, not your markup and not your pixels, is what assistive technology reads.
The first rule of ARIA is not to use ARIA. A native element brings role, focusability, keyboard behaviour and default actions; ARIA brings a label in a tree and nothing else.
Every interaction must be reachable, understandable and completable with a keyboard alone — because the keyboard is also the switch device, the voice command, the braille display and the screen reader.
Focus is a single pointer into the document that the browser maintains for you — until your application replaces the DOM underneath it. Then it becomes yours to move, contain and restore.
ARIA has a small set of rules that exist because each one describes a real way people break pages. The underlying one: a wrong ARIA attribute is worse than no ARIA at all, because the browser will faithfully repeat your mistake.
Nothing announces itself. A DOM change away from the user's focus is silent unless it happens inside a region the assistive technology was already watching — and announcing everything is its own failure.
Perceivability is measurable and it is a user preference. Contrast has a computed value, colour must never be the only carrier of meaning, and motion, contrast and target size are all things the operating system already knows about the person using your page.
The lab: modal, menu, tabs, accordion and form, each written as a contract — semantics, keys, focus and announcement — because a component that does not state these has not specified its behaviour at all.
Responsive Design
6 lessonsAdapting to available space and user conditions: fluid layout, media and container queries, responsive images, CSS pixels versus device pixels, and typography that survives translation.
Percentages, `min()`, `max()`, `clamp()` and the intrinsic behaviour of flex and grid express continuous adaptation; a breakpoint is what you reach for when the change genuinely cannot be continuous.
Width is one axis. Colour scheme, reduced motion, contrast, pointer precision, hover capability and orientation are separate questions — and the preference ones are accessibility features, not theming.
A component should respond to the space it was given, not to the viewport it happens to be inside — which is why `container-type` establishes containment, and why that containment has real layout consequences.
`srcset` and `sizes` for resolution switching, `<picture>` for art direction and format, `loading` and `decoding` for scheduling — and `width`/`height` or `aspect-ratio` always, so the browser can reserve the space.
CSS pixels, device pixels and the ratio between them; the layout viewport versus the visual viewport; the one viewport meta tag worth writing — and why suppressing zoom is an accessibility failure, not a layout fix.
Fluid type with `clamp()` that still honours the user's font size, a measure defined in characters rather than pixels, and layouts that survive a translation two-thirds longer than the English it was designed around.
State
7 lessonsLocal, form, URL, server, auth, cached and persistent state are different things with different owners. Most frontend bugs are a state-ownership answer nobody wrote down.
Local UI, form, URL, server, authentication, cached and persistent state have different owners, lifetimes and truths. Calling them all "state" is the first bug.
A five-criterion decision — shareable, local-only, server-authoritative, distant, or must-survive-reload — that resolves most state placement questions before a library is chosen.
Filters, tabs, pagination and the selected item belong in the address bar far more often than teams assume — because that is the only state store the browser itself restores.
If a value can be computed reliably from state you already hold, storing it separately creates a second owner — and duplicated state is state that can disagree with itself.
The server, the client cache, component state and the URL can all hold a version of the same fact. When they disagree, the only question that matters is which one is authoritative.
A form is a staging area for a mutation that has not happened yet: the user is authoritative until submit, dirtiness is the tracked fact, and the record is only replaced when the server agrees.
What survives a reload, a tab discard, a browser restart and a new device are four different questions — and anything you persist is a schema you now have to migrate.
Component Architecture
6 lessonsBoundaries drawn by responsibility, state ownership and reuse — plus the contract a component owes: inputs, events, slots, behaviour and accessibility.
Six forces decide where a component ends — responsibility, state ownership, composition, reuse, render cost and accessibility — and "this file is getting long" is not one of them.
Inputs, outputs, slots, behaviour and accessibility are all part of the API. The a11y half is the half that gets left implicit, and that is where components break.
Children are the mechanism that stops a component growing a prop for every possible variation. Compound components and scoped slots are what you reach for when the frame needs to talk to the filling.
Three ways to get a value from where it lives to where it is needed. Each buys something and each charges for it, and none of them is the default answer.
Indirection with no behaviour: a component per div, props that only pass through, and a stack ten frames deep to render one button.
Framework work and browser work are two different bills. Re-running a component is cheap; mutating the DOM, invalidating style and forcing layout are not — and most components are not your bottleneck.
Frameworks & Reactivity
8 lessonsReact, Vue, Svelte, Solid and Angular as different answers to the same questions: how change is detected, how much work is compile-time, and what reaches the DOM.
Re-run and diff, tracked proxies, compile-time analysis, fine-grained signals, zone-based change detection: five ways of finding out that something changed.
State and props go in, the component function runs and returns a description of the UI, a reconciler compares descriptions, and the difference becomes DOM calls.
Reactive state records who read it. A component re-renders when a reactive value it actually read during its last render has changed — not because its parent did.
A compiler reads your component, works out which parts of the markup depend on which values, and emits code that updates exactly those parts.
The component function runs once. Signals hold values with subscriber lists, and setting one re-runs only the small computations that read it — each writing to the node it owns.
An integrated framework rather than a view library: components and templates, dependency injection, routing, forms and a build system, shipped and versioned together.
Deciding what changed between one UI state and the next comes down to identity: which node in the new list is the same thing as which node in the old one.
There is no winner. There are constraints — team, existing code, hiring, rendering strategy, bundle budget, support horizon — and the framework that fits the most of yours.
Client-Side Routing
6 lessonsThe URL is application state — shareable, bookmarkable and restorable. Route matching, nesting, history, scroll restoration and the loading boundaries in between.
Intercept the link, match the URL, swap the view — and inherit every job the browser was quietly doing during a real navigation.
How a path becomes a route: segmentation, patterns, specificity, ranking versus ordering, and the nested match chain that renders a page.
Path params identify, query params refine — and both are untrusted strings that have to be parsed, validated and canonicalised before anything renders.
The session history stack, `pushState` versus `replaceState`, `popstate`, and why intercepting navigation without handling back is the most common router bug there is.
The browser does this well and single-page applications break it: `history.scrollRestoration`, restoring on back but not on a forward navigation, and the async data problem that puts you in the wrong place.
Where the spinner belongs: the subtree a fallback is allowed to replace, why nesting them matters, and why one boundary at the top blanks the whole page on every navigation.
Data Fetching & Server State
7 lessonsRequest, loading, success or error, render — and the parts that only show up in production: cancellation, deduplication, retries, pagination and background refresh.
Request, loading, success or error, render — and the fact that `fetch` resolves happily for a 500, has no deadline, and hands you a body you still have to parse.
An interaction has at least four states and usually five. The missing failure branch is the most common defect in frontend code, and empty is not loading.
`AbortController`, unmount, and the `AbortError` that must never be shown to a user — because abandonment is not failure.
In-flight coalescing and caching are two different mechanisms with two different windows. Confusing them is why "we have a cache" does not stop the thundering herd on mount.
Retrying a non-idempotent mutation is a correctness bug wearing resilience as a costume. Retry only what is safe, with backoff, jitter and a cap.
Offset gives you jump-to-page and gives you duplicates under insertion. Cursor gives you stability and takes away page numbers. Pick from the UI you owe, not from the API you were given.
It has freshness, a cache, invalidation, refetching, synchronisation and an authority that lives somewhere else. Copy it into component state and it starts diverging immediately.
Client Cache & Optimistic UI
6 lessonsQuery keys, freshness, staleness and revalidation; updating the interface before the server has agreed, and reconciling honestly when it does not.
A key, an entry, a freshness state and a revalidation rule — the cache your application owns, which is not the browser's HTTP cache and does not obey its headers.
The key is the identity of the data. Deduplication, hits, cross-contamination and the blast radius of every invalidation are all decided by what you put in it.
Render what you have, fetch what is current, write it in — and take responsibility for the fact that the page changed under the person reading it.
Updating the interface before the server has agreed: a claim the client makes on the authority's behalf, acceptable exactly when the rollback is honest.
The server rarely just says yes or no. It says something slightly different — and reverting your prediction is usually the wrong answer to that.
"cat" then "car": A leaves first, B returns first, A returns last and overwrites the newer result with the older one. The fix is a rule about which response is allowed to win.
Real-Time UI
6 lessonsPolling, long polling, SSE and WebSocket compared by what they cost — plus reconnect, ordering, duplicate delivery and the resynchronisation nobody prototypes.
Polling, long polling, SSE and WebSocket compared by direction, connection cost, infrastructure friction, reconnection, framing and what the browser already does for you.
One-way, text, and reconnecting by default: what `EventSource` does for you, how `Last-Event-ID` closes a gap, and the per-origin connection limit that only bites in the fifth tab.
Bidirectional, framed, and no longer HTTP — which means heartbeats, reconnection, backoff, auth on connect, message schema and versioning are now yours.
Exponential backoff with jitter, why a server restart without jitter produces a synchronised stampede, and how to say "reconnecting" to a user without saying it a hundred times.
At-least-once is the normal case: events arrive twice, arrive late, and arrive in an order nobody promised. The UI's job is to be idempotent per event.
The disconnect left a hole. Replay from a cursor or refetch a snapshot — but never resume as if nothing was missed, which is what everybody ships first.
Frontend Authentication
6 lessonsRepresenting identity, sending credentials safely, surviving session expiry and rendering authorization-aware UI — while the backend stays the only authority.
Four jobs — represent identity, send credentials safely, survive expiry, render authorization-aware UI — and one job that is never yours: enforcement.
A genuine trade-off with no universal winner: unreadable-but-automatic against readable-but-explicit, and the attributes that decide what each one actually costs you.
What the interface does when the credential dies mid-session: silent refresh, five simultaneous 401s that must produce one refresh, and the difference between expired and revoked.
Render what the user can actually do, so the interface is honest — then let the server refuse the request anyway, and stop leaking the existence of what they cannot see.
One session, several documents, no shared memory: logging out in one tab has to reach the others, and the tab that missed the message is still rendering a logged-in UI.
Send the user back to what they were trying to reach — and never redirect to a URL somebody handed you in a query parameter.
Frontend Security
9 lessonsXSS, CSRF, the same-origin policy, CORS, CSP, clickjacking, third-party scripts and supply chain — the browser-side half of a problem the server cannot solve alone.
The origin as the unit of trust, the renderer as a sandbox, and four separate mechanisms answering four separate questions — mixing them up is why security fixes so often do nothing.
Embedding is allowed, reading is not — and the gap between "the request was sent" and "your code may see the answer" is where most browser security confusion lives.
Untrusted content becomes executable content. Your framework already escapes text interpolation — so every XSS in a modern application is at the exact place someone opted out.
Escaping and sanitization are different operations solving different problems — and when you genuinely must render HTML, allowlist it, at render time, with something you did not write.
The browser attaches credentials to requests automatically, including ones another site caused. That helpfulness is the vulnerability, and it is why the defence has to be explicit.
A browser policy about whether script may read a cross-origin response. Not authentication, not a firewall, and not relevant to anything that is not a browser — plus the error message that lies to you.
A browser-enforced allowlist for what your page may execute and load. It caps the damage of an injection you missed — and `unsafe-inline` in the script directive turns the whole thing off.
Your interface, rendered inside someone else's page, with their content on top. The defence is one response header — and the wrong version of it locks out keyboard users instead.
A script tag grants full authority over your page. There is no partial trust, and the dependency you never chose is running in the same context as your login form.
Browser Storage
6 lessonsCookies, localStorage, sessionStorage, IndexedDB and Cache Storage: lifetime, synchronicity, capacity and exposure. Four different answers to "where does this live".
Cookies, localStorage, sessionStorage, IndexedDB and Cache Storage compared on the six axes that actually decide: lifetime, scope, capacity, synchronicity, automatic transmission and exposure.
The only browser store the network sends for you. That single property explains the convenience, the size limits, the scoping attributes and the class of attack built on top of it.
A synchronous, string-only, origin-scoped map. The convenience is real, and so is the fact that every read and write blocks the thread that owns rendering.
Asynchronous, structured, transactional and versioned — a real database in the browser, with a schema you own and a migration path that is where real applications break.
A script-controlled store of Request/Response pairs. A different layer from the browser's HTTP cache, with different rules, and the reason a service worker can answer a request with no network at all.
Two properties decide everything here: anything script can read, every script on the origin can read — and nothing in the browser is durable storage.
Service Workers & Offline
6 lessonsA programmable proxy between the page and the network: install, activate, fetch, update — plus offline mutation queues and the conflict resolution they imply.
Install, activate, fetch, update — and the waiting worker that quietly leaves users running last week's code for days.
The worker sits between the page and the network and can answer from cache, from the network, or with a response it invents — which is exactly why it can brick your site.
Cache-first, network-first, stale-while-revalidate, cache-only, network-only: five answers, each right for something and each with a characteristic way of going wrong.
Honesty is the whole lesson: say what is available, what is stale, what is queued, and what will happen on reconnect — because a UI that pretends to be online is worse than one that admits it is not.
Change offline, persist locally, reconnect, sync, resolve conflicts — and the last step is a product decision, not a technical default.
What the manifest declares, what makes a site installable, and why an installed app with no offline story is just a bookmark with an icon.
Performance & Web Vitals
9 lessonsLoading, interaction responsiveness and visual stability measured on real devices, with the main-thread and memory work behind each one. Measure before optimising, always.
Lab profiles and field data are two instruments answering two different questions. Which browser signal to reach for, and why a local recording is a hypothesis rather than evidence.
The main content has to be discovered, requested, delivered and unblocked before it can paint. Late is usually a discovery or a blocking problem, not a byte problem.
A slow tap is three separable delays: waiting for the thread, running the handler, and producing the frame that shows the result. Each has a different fix.
Content moves because something arrived after layout had already been decided. Reserve the space before the content exists, and the shift never happens.
Bytes are only the download. Parse, compile, execute and retain all cost more, they all scale with the device, and execution competes with rendering for the one thread that can paint.
The two heaviest things on most pages, and the two most often shipped at the wrong size, in the wrong format, discovered too late, and without any space reserved for them.
The app is fine on load and slow an hour later. Something is being retained on every interaction and released on none.
A hundred thousand rows of data, about thirty rows of DOM. It is the right answer for large lists and it costs you real things.
Trading recomputation for memory and an invalidation problem. Sometimes clearly worth it; applied everywhere, a net loss with extra bugs.
Critical Path & Delivery
7 lessonsThe waterfall from HTML to first content: blocking resources, resource hints, HTTP caching, CDNs and content-hashed assets — the network half of how fast a page feels.
The set of resources that must arrive and be processed before the browser can paint meaningful content — a dependency graph, not a byte count.
Bars are not the point. The staircase is: a resource that could not start until another finished is a dependency your page structure created, and often one you can delete.
Three different mechanisms produce one symptom. CSS blocks painting, synchronous scripts block parsing, and scripts wait on pending stylesheets — and each has a different fix.
`preconnect`, `dns-prefetch`, `preload`, `modulepreload` and `prefetch` are five different jobs — and a hint that guesses wrong costs more than no hint at all.
What the browser does before it makes a request at all: freshness, `no-cache` versus `no-store`, validators, revalidation, and `immutable`.
Build output to an edge near the user: what the cache key is made of, why invalidation is a worse tool than versioning, and why edge caching is mostly about latency rather than bandwidth.
`app.a3f19c.js` is a caching strategy, not a naming convention: you never invalidate a URL, you stop referencing it — with consequences for code splitting and for tabs that have been open all week.
Build Tooling & Bundling
11 lessonsModules to dependency graph to transforms to chunks. Code splitting, tree shaking, TypeScript, polyfills versus transpilation, source maps, and what your users download.
Entry points become a dependency graph, the graph is transformed, and the graph is cut into chunks. Every bundler is an implementation of those three ideas.
Vite, Webpack, Rollup, esbuild and SWC as implementations of the same graph-and-chunks model, separated by dev-server strategy, build speed, plugin surface and output control.
Cutting one graph into an initial chunk plus route and feature chunks — and the two failures on either side of it: shared code duplicated, and a waterfall of tiny chunks.
Deferring routes, components, images and expensive modules until they are needed — and the loading state, the flash and the error path that every deferral creates.
Static module analysis can allow unused exports to be dropped — and five specific things routinely stop it: side effects, a wrong `sideEffects` flag, CommonJS interop, barrel re-exports and dynamic access.
Reading a treemap: attributing bytes to modules, finding the dependency nobody meant to add, and separating "large" from "large and on the critical path".
Minification rewrites the artifact and is permanent. Compression encodes the response and is undone by the browser. They compose, they are configured in different places, and they are not alternatives.
Static structure known before execution versus a runtime function call that returns an object — and why the first is what makes tree shaking and named-export analysis possible at all.
Transpilation rewrites syntax an engine cannot parse. A polyfill supplies an API an engine does not have. Neither can do the other's job.
The mapping from the bundle that shipped back to the source you wrote. Without one, a production stack trace names a minified letter on line 1.
Parse, type check, emit. The types are erased before anything runs, so nothing they promised is enforced at the network boundary.
Rendering Strategies
9 lessonsCSR, SSR, SSG, streaming SSR, islands and server components as points on a curve between where work happens and when the page becomes interactive.
The server sends a shell, the browser downloads a bundle, runs it, then asks for data. Everything a person can read sits behind that chain.
The server fetches the data and renders the markup per request, so content arrives early. Interactivity does not arrive with it.
Render every page once at build time and serve the files from an edge. The cheapest thing to serve, and the hardest thing to keep current.
The client attaches behaviour and state to markup that already exists. Until it finishes, the page looks finished and is not.
The client renders something different from what the server sent. The framework cannot quietly pick a winner, because it does not know which one is right.
Ship JavaScript only for the regions that are genuinely interactive. The saving is real; the cost is a component model that must declare its boundaries.
Send the page in pieces as each part becomes ready. One slow region stops holding the whole document hostage — and once the first byte is out, you cannot take it back.
Components that run only on the server and never ship. A framework-specific answer to "how much of this tree needs to be code in the browser at all".
There is no winner. There are seven questions, and a real application usually answers them differently for different routes.
Frontend Testing
6 lessonsChoosing the level by what it can actually prove: pure logic, component behaviour, critical user flow, visual appearance, and the accessibility checks a tool cannot finish.
Pure logic, component behaviour, critical user flow, visual appearance and accessibility are five different observations. The level is chosen by what the failure would look like, not by a pyramid.
The cheapest, fastest and most reliable tests you will ever write — and the reason to get logic out of components so that it can be tested this way at all.
Given a rendered component, when the user interacts, then visible behaviour changes. Querying by role and accessible name is both better practice and an accessibility check you get for free.
A real browser over the flows you cannot ship broken — signup, login, checkout, payment, upload, critical navigation — and an honest account of what that costs.
The only level that observes what the browser painted — and a genuinely high-maintenance one, paid for in baselines and false positives from fonts, animation and dynamic content.
Automated rules catch a real but limited fraction of accessibility defects — and the ones they cannot decide are the ones that stop people using the product. Keyboard and screen-reader passes are not optional.
Frontend Observability
6 lessonsErrors, failed requests, vitals and interaction latency from real users on real devices — and the privacy obligations that come attached to every one of them.
Capturing exceptions and unhandled rejections with enough context to act on — the route, the release, the breadcrumbs — and separating real signal from extensions, bots and opaque cross-origin noise.
Field measurement from the machines your users actually own — how it differs from synthetic testing, why the two disagree, how to sample, and why watching the median hides the problem.
Loading, interaction responsiveness and visual stability measured on real devices — what each observer actually records, why field values differ from lab values by design, and why the boundaries are not yours to memorise.
Requests that never arrived, timeouts, DNS and TLS failures, opaque CORS errors, offline users and navigations that cancel in flight — the failures with a zero server error rate.
Replay records whatever was on the screen — including things you did not intend to record. Masking is opt-out shaped and fails open, so the governance question comes before the engineering one.
Attributing a regression to a deploy when clients update on their own schedule: tagging every signal with a release, reading the adoption curve, and comparing cohorts instead of time windows.
Frontend Debugging
6 lessonsReproduce, then work the layers: network, console, DOM and CSS, application state, performance, memory. A method, not a tour of a devtools panel.
Reproduce, work the layers in order, narrow by bisection, fix, and then prove the signal moved. The method is the skill; the panels are interchangeable.
Seven panels, seven questions. Learn which question each one answers and what it structurally cannot tell you, and the UI can change underneath you without costing you anything.
Read the waterfall for its shape, not its totals. Then learn the three ways the network panel lies: the CORS error that is a 500, the cache row that is not a hit, and the request that is missing because a service worker answered it.
Separate a frame that took too long from a frame that was never attempted, then find which pipeline stage the change is actually costing — with the forced-layout warning, paint flashing and layer borders as your three cheapest instruments.
A wrong value on screen is the end of a sequence, not a snapshot. Reconstruct the transitions, then answer the question that resolves most of these bugs: which copy of this data is authoritative?
Three snapshots across a repeated cycle, then follow the retainer chain to whatever is holding the thing that should have gone. Growth is not a leak until you can say what is keeping it alive.
Frontend Architecture
7 lessonsMPA, SPA, SSR app, static site and micro frontends; design systems, internationalization, BFFs, deployment — and the fact that web clients never update atomically.
MPA, SPA, SSR application, static site and micro frontends compared on team shape, content versus application, discoverability, interactivity depth and deployment independence — with no universal winner.
An MPA gets history, scroll, focus reset, back and forward, and per-page code loading from the browser. A SPA must rebuild all of it — and buys preserved state and richer transitions in return.
Independently owned and deployed frontend boundaries. The driver is almost always organisational rather than technical, the benefit lands on teams, and the cost lands on users.
Tokens to components to patterns to applications: a versioned product with a public API, accessibility built in at the component layer, and an adoption problem that decides whether it is leverage or a cost centre.
Named values in three layers — primitive, semantic, component. The semantic layer is the one that makes a re-theme a set of value swaps instead of a rewrite.
Not a translation table bolted on later: plural rules, locale-aware formatting, text that grows, and a layout that has to work in both directions.
Instant, timezone, locale, display — four separate things. Most date bugs come from collapsing them, and most of the rest come from string manipulation.
Production Frontend
7 lessonsShipping to browsers you do not control: deployment, feature flags, analytics, uploads, the API contract you consume — and the fact that a web client never updates atomically.
Source to build to artifact to CDN to a browser you do not control — and the deploy that breaks the tabs already open.
Production is running client v1, client v2 and backend v3 at the same time. There is no moment at which the frontend updates.
Evaluating a flag in a browser: the three states every flag has, the flicker while it loads, the flagged-off code still sitting in the bundle, and the rule that a flag is never an authorization boundary.
A named event with a stable schema answers a question. A firehose of clicks answers none — and carries personal data you did not mean to send.
Progress, cancellation, retry, validation and preview — per file, not per form — plus the pre-signed URL that keeps a two-gigabyte video out of your application server.
Pagination style decides whether infinite scroll is even possible. Granularity decides whether a screen is one request or nine. The error model decides whether you can say anything useful when it fails.
A server the frontend team owns, sitting between the browser and several backend services. Why a client asks for one — and what the team signs up for by running it.
Agentic Frontends
6 lessonsStreaming tokens, tool progress, citations, cancellation and partial failure — plus the rule that a model suggesting an action is never the same as the action being authorized.
The model runs elsewhere. The client is a UI for an operation that streams, takes tools, fails halfway and must never be trusted with authority.
Tokens arrive dozens of times a second on a thread that also has to paint. What you do per chunk decides whether the answer feels alive or the page feels broken.
Observable states — searching, retrieving, calling, processing — reported honestly. Not a dramatisation of thinking the system cannot actually show you.
A source the user can actually check, and an answer that admits what it is missing. Both are interface problems before they are model problems.
Cancellation is two-sided and retry is not free: a run that already called a tool has already changed something.
The model proposes; structured validation, a permission check and the backend decide. The UI shows only what was actually confirmed.