What are the fundamentals of frontend system design covering components, state, and performance?
Component structure: composition over inheritance, props down + events up, small focused components, design system primitives + domain compositions. State: local first (useState), lift only when shared, server state lives in a cache (React Query), global UI state in context or Zustand. Performance: SSR/SSG for first paint, code split per route, virtualize long lists, memoize on profiled-slow paths, lazy-load below fold, CDN + cache aggressively. Measure with RUM, set budgets in CI.
Three pillars of a robust frontend architecture.
1. Component structure
Principles:
- Composition over inheritance —
<Card><Card.Header /><Card.Body /></Card>instead of class hierarchies. - Props down, events up — parents pass data via props, children signal via callbacks. Avoid mutating parent state from children directly.
- Small focused components — one responsibility each. A 500-line component is a refactor signal.
- Layered architecture:
- Primitives — design system: Button, Input, Modal. Generic, reusable, accessible.
- Compositions — domain-specific: ProductCard, OrderForm. Built from primitives.
- Pages / routes — page-level layouts, data wiring.
- App shell — providers, error boundaries, global UI.
Common antipatterns:
- Prop drilling 5 layers deep → use context or move state.
- One giant component handling 6 concerns → split.
- Tight coupling between primitives and domain → primitives should be generic.
2. State management
State has several flavors, each with its own home:
| State type | Where it lives |
|---|---|
| Local UI (toggle, input) | useState in the component |
| Form | React Hook Form / Formik / Zod-validated useState |
| Shared UI (theme, modal open) | Context, or a small store (Zustand, Jotai) |
| Server data (API responses) | React Query / RTK Query / SWR — caching + dedup + refetch |
| URL / route | Router (params, search params) — links shareable |
| Persistent client | localStorage / IndexedDB for offline |
| Auth | Cookie (httpOnly) + a session context |
Rules of thumb:
- Start local; lift only when proven shared.
- Server data is not application state — let the cache library own it.
- Avoid putting derived state in state — derive in render or use
useMemo. - Global stores should be small and split (theme separate from auth separate from preferences) — fewer re-renders.
- URL is state — searchable filters belong in query params, not Redux.
3. Performance
First paint:
- SSR/SSG for content pages → fast LCP + SEO.
- Code split per route → smaller initial JS.
- Preload critical resources (LCP image, fonts).
- Inline critical CSS, defer the rest.
In-app responsiveness (INP):
- Don't block the main thread with sync rendering of large lists — virtualize.
- Web workers for CPU-heavy tasks (parse, image manipulation).
useTransitionfor non-urgent updates so input stays snappy.- Debounce inputs that drive fetch / re-render.
Caching:
- Long max-age + immutable on hashed assets.
- React Query / RTKQ for API responses.
- Service worker for offline + custom strategies.
- Edge cache HTML where possible.
Bundle:
- Tree-shake; per-function imports.
- Audit dependencies quarterly; replace heavy with light (moment → dayjs).
- Set a budget in CI (
size-limit, Lighthouse CI).
Render perf:
- Memoize on profiled-slow paths, not as a default.
- Stable references where children are memoized (useCallback, hoist).
- Lift state down to minimize re-render blast radius.
Measurement is non-negotiable
- RUM (web-vitals → analytics) for real users.
- Lighthouse CI for PR regressions.
- Custom marks for product-critical flows.
- Bundle analyzer quarterly.
- Segment by device, network, geo, route.
Anti-patterns to avoid
- Global state for local concerns → re-render cascades.
- Memoizing everything → overhead without payoff.
- Server data in Redux → reinventing caching badly.
- CSR for content pages → bad SEO + slow LCP.
- One mega-bundle → slow TTI.
- No CI perf guard → regressions accumulate.
Decision flow for a new feature
- Render mode: who's the audience (public/auth)? SEO needed? → SSG / SSR / CSR.
- State: where does data live (server / client / both)? Local or shared?
- Components: pull from design system; compose domain UI on top.
- Data: React Query for fetches; cache key, invalidation tags, mutations.
- Perf budget: estimated JS added; preload/defer plan; LCP/INP target.
- A11y: keyboard, screen reader, contrast.
- Testing: unit for logic; integration for user flows; smoke E2E for critical paths.
Mental model
Architecture is constraints. Pick the rendering strategy, the state model, and the perf budget up front, and the rest of the system is much easier to keep coherent as it grows. Most pain in large frontends comes from not having made these decisions, not from making the wrong ones.
Follow-up questions
- •How do you decide between Context and Zustand for shared state?
- •What's the right level of code splitting?
- •When does memoization actually help?
- •How do you keep a design system in sync with feature work?
Common mistakes
- •Global state for local concerns — re-render blast radius.
- •Server data in Redux — reinvents React Query badly.
- •Memoizing everything — net negative.
- •Mega-bundle for the whole app — slow TTI.
- •CSR for content pages — bad SEO + slow first paint.
- •No CI perf guard — regressions creep in.
Performance considerations
- •Architecture decisions compound. Choosing SSR vs CSR, monolithic vs split bundle, and where state lives all set the perf ceiling. Once made, they're hard to reverse. Pick deliberately and measure.
Edge cases
- •Cross-team monorepo: shared design system + per-team app code, with strict module boundaries.
- •Multi-tenant: theming + localization at the routing layer.
- •Offline-first: service worker + IndexedDB + sync queue.
- •Real-time collab: CRDT or OT layer alongside React state.
- •Microfrontends: shared dependencies via module federation.
Real-world examples
- •Next.js + RSC + React Query + Tailwind: a common modern stack.
- •Linear: CSR + heavy IndexedDB sync + Zustand for state, optimized for snappy in-app feel.
- •Vercel/Stripe marketing sites: SSG + edge cache + minimal client JS.
- •Notion: SPA shell + lots of caching + custom collab engine.