How do you architect large front ends (MFs) with SSR/ISR/CSR?
Front-End Developer
answer
Architect the app as a federated, typed mono-repo: a shell (SSR/ISR) hosts route-level micro-frontends via Module Federation (or import maps) with versioned UI kit and tokens. Render on the server with identical data shims and deterministic feature flags; defer islands to CSR with partial hydration. Share a headless design system; tree-shake per route. Stabilize markup with consistent serializers to avoid hydration mismatches.
Long Answer
A production-scale front end that supports SSR, ISR, and CSR without hydration drift rests on two pillars: (1) a single source of truth for UI contracts (design tokens + headless components) and (2) deterministic data/rendering on server and client. Around these, run a shell plus route-level micro-frontends so teams ship independently while sharing the system safely.
1) Topology: shell + federated routes
The shell owns HTML, routing, and data prefetch; it lazily mounts per-route bundles. Expose route entries via Module Federation or import maps. Each remote exports a small API (render(props), getData(ctx), head()) so the shell prefetches data and critical CSS while bundles stay isolated.
2) Design system & styles
Publish a headless UI kit and a theme layer driven by design tokens (CSS vars) from one package. Extract CSS per route (critical CSS inlined by the shell) to avoid runtime CSS on the server. Freeze token names; remotes compose primitives rather than invent styles.
3) Data parity for SSR/ISR/CSR
Define a data interface per route: getData(ctx) runs server-side and returns serializable JSON with explicit nulls, locale, tz, and feature flags. Serialize with stable codecs. Inline JSON in HTML and rehydrate with the same parser. CSR navigation uses the same contract. For eventually consistent APIs, render those bits as islands hydrated later.
4) Islands: hydrate only what moves
Mark interactive widgets as islands; leave the rest static. For browser-only signals (viewport/media query), render a deterministic stub on the server and upgrade on the client. Share formatter libs/locales on both sides.
5) Versioning & shared deps
Pin framework singletons (react, router, i18n) with MF shared config; publish the design system as its own semver’d package. Shell CI validates remotes against contracts. If a remote lags, serve its last good version from a CDN manifest.
6) Route-level code splitting & prefetch
Each route builds its own chunk graph. The shell prefetches likely next routes on idle and inlines only that route’s critical CSS. Use Early Hints/Link headers to start JS/CSS early.
7) Observability & guardrails
Emit per-route web vitals, hydration timings, and mismatch counters. In CI, compare server renderToString vs client hydrate snapshots; gate releases on visual and a11y checks.
8) Security & resilience
Set a strict CSP and isolate remotes by subpath/subdomain with locked CORS. Statically register remotes; never eval arbitrary URLs. Use SRI for CDN assets. Keep feature flags server-driven for SSR/ISR so the first view matches.
9) Team model & DX
Each route team owns its remote, tests, and alerts. Shared RFCs govern tokens and contracts. Ship via a canary ring by switching a manifest pointer; roll back instantly. A local dev proxy composes shell + remote.
Result: micro-frontends where they help, one design language, SSR/ISR/CSR delivered safely, and hydration mismatches avoided by deterministic data, tokens, and serialization.
Table
Common Mistakes
Over-microfrontending: every feature becomes a remote, creating a chatty, distributed monolith. Letting teams ship their own tokens or CSS runtimes, causing visual drift and duplicate bytes. Server renders nondeterministic data (timestamps, random IDs), so the client rehydrates different markup. Hydrating the whole page instead of islands, inflating JS and hurting INP. No pinned singletons—two Reacts at runtime. Relying on runtime CSS for SSR, yielding class name mismatches. Prefetching everything (waste) or nothing (slow nav). No mismatch telemetry or CI snapshots; bugs surface in prod. Loading remotes via eval/unchecked URLs, breaking CSP. Client-only feature flags on SSR/ISR pages, so first paint diverges. Different locales/formatters on server vs client; number/date drift. Sharing UI as source per team instead of a versioned package, leading to dependency chaos. Missing rollback for a bad remote; no manifest switch to revert instantly.
Sample Answers
Junior:
I’d keep a single app with route-level code splitting and a shared design system. SSR renders HTML with data from one API; the client reuses the same JSON to hydrate. I’d hydrate only widgets that need interaction and use a CDN for static assets.
Mid:
We use a shell + a few route micro-frontends. The design system is headless with CSS-var tokens. Each route exposes render(props) and getData(ctx); the shell inlines critical CSS and serialized data. Singletons (React/router) are pinned. We prefetch likely routes and add mismatch counters in telemetry.
Senior:
Contracts first: tokens, headless components, and a data schema per route. Module Federation exposes /catalog, /cart, etc. SSR/ISR use the same codecs to serialize data; islands hydrate only where needed. CSP + SRI + static remote registry keep supply chain tight. CI runs server-vs-client snapshot tests, and releases use a manifest canary for instant rollback. We pin API versions, and if a remote lags, the shell serves its last good build while alerting the owning team.
Evaluation Criteria
Architecture fit: Clear rationale for shell + route-level micro-frontends; or a modular monolith with a path to MF later. Ownership per route and minimal public APIs (render/getData/head).
Design system: Single package of headless components and design tokens; token discipline and per-route CSS extraction. Critical CSS in shell.
Data parity: One data contract per route, stable serialization (same codecs), and proof that SSR/ISR/CSR use the same shape.
Hydration: Islands strategy; browser-only stubs upgraded on the client; avoidance of whole-page hydration.
Shared deps: Pinned singletons; versioning for remotes; compatibility checks in CI.
Performance: Route-level code splitting, prefetch heuristics, Early Hints/Link; metrics for LCP/INP.
Security/ops: CSP/SRI, static registry for remotes, locked CORS, instant rollback via manifest.
Observability: Per-route vitals + mismatch counters; server vs client snapshot tests.
Red flags: Many tiny MFs, ad-hoc styles, client-only flags on SSR pages, two Reacts at runtime, no rollback.
Preparation Tips
Create a tiny demo: shell app + two route remotes (/catalog, /cart) with Module Federation. Publish a design tokens package and a headless button/input; extract per-route CSS and inline critical CSS in the shell. Implement getData(ctx) for each route; serialize with a stable codec and test SSR → hydrate using the same JSON. Add an island (cart badge) that hydrates after idle; keep the rest static. Pin singletons (react/router) and break the build if duplicates appear. Wire Early Hints/Link headers and a simple prefetch heuristic (hover/viewport). Instrument vitals + a mismatch counter; break CI on diffs between server renderToString and client hydrate. Lock CSP/SRI; load remotes only from a static registry. Practice a manifest canary + rollback. Document contracts and a short runbook for adding a new route team. Add i18n with the same locale data server/client; prove numbers/dates match. Test an eventually consistent field rendered as an island. Set up a local dev proxy to compose shell + remote without the full repo.
Real-world Context
Marketplace: Moved from a single SPA to shell + two route MFs. Shared tokens + critical CSS cut CSS by 35% and LCP by 28%. Hydration mismatches dropped to near zero after adopting stable JSON and island widgets for price/stock.
SaaS dashboard: Headless design system shipped as one package; teams stopped copying CSS-in-JS. Pinned React/router; a CI check blocked duplicate versions. Route prefetch on idle dropped median route switch by 45%.
News site: ISR for article pages with server-driven flags; client subscribed post-hydrate. Strict CSP/SRI and a static remote registry passed security review. Manifest canary caught a bad analytics remote; rollback was instant.
Retail: Mismatch counter surfaced a locale bug (server en-US, client en-GB). Fixing codecs and unifying formatters ended the drift; INP improved after island-only hydration of carousels. B2B portal: Observability exposed a heavy chart remote; splitting vendor libs per route and Early Hints cut JS by 200KB and p95 nav by 120ms. A fire-drill validated rollback and CDN manifest promotion for all remotes.
Key Takeaways
- Centralize a headless design system and tokens; compose, don’t fork styles.
- Enforce one data contract per route with stable serialization.
- Hydrate islands only; keep the rest static to protect INP.
- Pin shared singletons and version remotes; release via manifest canary.
- Measure vitals + mismatches; fix before users see them.
Practice Exercise
Scenario:
You’re leading a large front end with three product areas (catalog, cart, account). You must support SSR/ISR, route-level code splitting, a shared design system, and avoid hydration mismatches while enabling two teams to ship independently.
Tasks:
- Build a shell that renders HTML, routing, and critical CSS. Expose /catalog and /cart as Module Federation remotes. Provide a static remote registry and CSP/SRI.
- Create a tokens package (colors/spacing/typography/motion) and headless components (Button, Input, Card). Extract per-route CSS; inline critical CSS for the current route.
- Define getData(ctx) for each route. Serialize with stable codecs (Date → ISO string); embed JSON in the shell, and rehydrate with the same parser. Prove CSR navigation reuses the same contract.
- Convert two widgets to islands (cart badge, price ticker). Hydrate them on idle; keep the rest static. Add a browser-only media-query stub that upgrades on the client.
- Pin singletons (react/router/i18n). Add CI that fails on duplicate frameworks; snapshot test server render vs client hydrate for each route.
- Implement prefetch heuristics (hover/viewport) and 103 Early Hints or Link headers. Capture LCP/INP and a hydration-mismatch counter per route.
- Release via a manifest canary: send 5% of users to a new /cart remote; include a one-click rollback. Document the runbook and ownership map.
Deliverable:
A repo and a 1-page brief explaining contracts, telemetry, canary/rollback, and how the design prevents hydration drift in SSR/ISR/CSR.

