Frontend Architecture
Reviewing a frontend''''s architecture: the order, and the cascading failures
A team escalates: the dashboard janks on every keystroke, so they file a ticket to “add code-splitting and lazy load the charts.” You profile it. The charts are not the problem. Form keystrokes are written into a global store, every subscriber re-renders on every character, and the chart subtree is one of them. Splitting the chart bundle changes nothing — the re-render fires after the chunk has loaded. The fix was three units back: keystrokes are local UI state, they never belonged in the global store. The team optimised the last layer to paper over a first-layer bug.
The whole track is one decision tree
Each unit so far taught a decision in isolation: state shape, data fetching, accessible forms, design tokens, monorepo boundaries, code splitting, build pipelines. In a real app they are not seven separate choices — they are one stack, and the stack has a direction. Lower layers constrain upper ones. State shape decides your re-render blast radius; tokens decide whether a rebrand is a config change or a migration; monorepo boundaries decide your CI time; code splitting and the build pipeline decide what actually ships to the browser.
The senior move when reviewing a new frontend is not to read it top-down (file structure, then components, then state) but bottom-up by failure cost. The cheapest bugs to fix are in the pipeline; the most expensive are in the state shape, because everything downstream inherits them. So you evaluate the layers in the order where an error does the most damage, and you stop optimising a layer the moment you realise the real bug lives below it.
The cascade: where each layer’s failure surfaces
The trap in the Hook is the defining pattern: a symptom appears in one layer, but the cause is in a lower one. Re-render jank looks like a rendering or bundle problem, so teams reach for memoization and code-splitting — the upper layers — and burn weeks. The cause was a state-shape decision: high-frequency state (keystrokes) hoisted into a global store, so every subscriber re-renders on every change. No amount of React.lazy fixes that, because the wasted render runs after the chunk arrives.
Read the cascade the other way too. A missing token system surfaces as rebrand pain: a marketing rebrand that should be a one-line swap of primitive values becomes a hunt-and-replace through hundreds of files. Atlassian and every mature design system structure tokens in three layers — primitives (the raw palette), semantics (what a color means: text-danger, surface-raised), and component values. Rebrand = swap primitives. Dark mode = swap semantics, components untouched. Teams that hardcoded #1a73e8 everywhere instead pay weeks; one documented 200-component dark-mode migration took two days with a semantic token layer and would have taken weeks of find-and-replace without it.
| Layer (review order) | Symptom it produces when wrong | Why no upper layer can fix it |
|---|---|---|
| 1. State shape | Re-render storms, drifting derived values, stale-after-mutation views | Code-splitting/memo run after the wasted render; the blast radius is set by where state lives |
| 2. Data fetching | Request waterfalls, slow LCP, double fetches | A faster bundle still serialises N round-trips if the fetch graph is sequential |
| 3. Tokens | Rebrand pain, inconsistent UI, dark-mode rewrite | Hardcoded values aren’t a build problem; the source of truth is missing |
| 4. Monorepo boundaries | Build-time explosion, cross-package coupling, slow CI | Bundlers obey your dependency graph; bad boundaries rebuild everything on any change |
| 5. Code splitting | Huge initial bundle, slow Time-to-Interactive | Only fixable here — but only if layers 1–4 are sound |
| 6. Build pipeline | Dead code shipped, no tree-shaking, slow CI feedback | Cheapest to fix; a config change, not an architecture change |
Monorepo boundaries are the build-time multiplier
The layer most often blamed for slow CI — “the build is slow, buy faster runners” — is usually a boundary problem, not a hardware problem. If your packages have no real boundaries (everything imports everything, one giant shared package every app depends on), then any change invalidates the cache for the whole graph and CI rebuilds everything. The fix is structural: clean package boundaries plus affected-only execution, so a one-package change builds one package.
The numbers are stark. Mercari reported a ~50% cut in Turborepo task duration and ~30% in total CI job time after adding remote caching and tuning their workflow. Documented GitHub Actions monorepo setups hit a 12× CI reduction by combining affected-only execution, remote cache, and a dynamic matrix; a 15-minute build drops to 2–3 minutes when only one package changed and the rest are cache hits, and remote caching can eliminate 80–90% of total build time for a large team because every CI run shares work instead of recomputing it. But the biggest lever is upstream of any cache: affected-only execution. Running 4 packages instead of 45 beats any caching optimisation, and you only get there if the boundaries are real. A faster bundler can’t rescue a dependency graph where every leaf depends on the trunk.
Why this works
Why review bottom-up instead of by file structure? Because the layers form a dependency chain, and a fix applied above the real cause is wasted work that also hides the bug. The Hook team’s “add code-splitting” ticket would have shipped, the chart chunk would have lazy-loaded, the jank would have remained, and the postmortem would have blamed the wrong layer. Reviewing in failure-cost order means you find the lowest broken layer first and fix there — so the symptom and every other symptom it spawned disappear at once.
The build pipeline is the cheapest layer — fix it last, not first
The pipeline (bundler config, tree-shaking, minification, the CI integration) is where teams love to start because it feels measurable: turn on a flag, watch a number drop. Chrome DevTools’ Coverage panel routinely shows 20–30% of shipped JavaScript never executes on a given page — real, recoverable bandwidth and parse time. That’s worth doing. But it’s the last thing a senior reviews, because it’s a config change, not an architecture change, and config is cheap to fix later. Optimising the pipeline while the state shape is broken is the engineering equivalent of waxing a car with a seized engine: the surface looks faster, the machine isn’t.
The discipline is to let the failure-cost order drive the review: confirm state shape and the fetch graph (which together own your interactivity and LCP — target LCP under 2.5 s), then tokens (rebrand/theming cost), then boundaries (CI time), and only then split bundles and tune the pipeline. Each upper layer assumes the ones below it are sound; invert the order and you optimise symptoms.
A new internal tool: one app, a small design team that rebrands roughly yearly, a 5-engineer team, ~12 packages planned, charts that are heavy but rarely opened. Pick the architecture that fits.
A dashboard re-renders on every keystroke in a search box. A teammate proposes code-splitting the charts to fix it. What's the senior read?
CI takes 15 minutes for a one-line change in one of 12 packages. What attacks the root cause?
Order the layers a senior evaluates when reviewing a new frontend (most-expensive-to-fix first):
- 1 State shape — derived vs stored, server cache vs client state, colocation (sets the re-render blast radius)
- 2 Data fetching — waterfalls vs parallelism, what owns LCP
- 3 Design tokens — primitive→semantic layering, so rebrand/dark-mode is a swap not a migration
- 4 Monorepo boundaries — clean packages + affected-only CI, the build-time multiplier
- 5 Code splitting — route/component chunks, lazy-load the heavy and rare
- 6 Build pipeline — tree-shaking, dead-code removal, CI integration (cheapest, fix last)
- 01A team blames a janky dashboard on missing code-splitting. Walk through how you'd find the real layer and why their fix wouldn't work.
- 02Why does a senior review architecture bottom-up by failure cost instead of top-down by file structure, and which layers are most vs least expensive?
The whole track collapses into one rule: frontend layers cascade, so a senior reviews them bottom-up by failure cost, not top-down by file structure. State shape is most expensive because it sets the re-render blast radius — high-frequency state in a global store causes re-render storms that no code-splitting or memoization can fix, because they run after the wasted render. Above it sits the fetch graph (waterfalls own your LCP), then tokens (a primitive→semantic layer makes a rebrand a value swap instead of a multi-week file hunt — two days vs weeks for a dark-mode migration), then monorepo boundaries (the build-time multiplier; clean boundaries plus affected-only execution and remote cache turned a 15-minute build into 2–3 minutes and cut Mercari’s task time ~50%). Code splitting and the build pipeline come last because they’re the cheapest layers — config, not architecture. The recurring trap is treating a symptom in an upper layer as a bug in that layer; the durable fix is always at the lowest broken layer, and reviewing in failure-cost order is how you find it.