Search…

Scalability for frontend systems

In this series (12 parts)
  1. What frontend system design covers
  2. Rendering strategies: CSR, SSR, SSG, ISR
  3. Performance fundamentals: Core Web Vitals
  4. Loading performance and resource optimization
  5. State management at scale
  6. Component architecture and design systems
  7. Client-side caching and offline support
  8. Real-time on the frontend
  9. Frontend security
  10. Scalability for frontend systems
  11. Accessibility as a system design concern
  12. Monitoring and observability for frontends

Prerequisite: Frontend security.

A frontend that works for one team and a hundred thousand users may not survive ten teams and ten million users. The bottlenecks shift from server capacity to organizational coordination: who owns which code, how features are tested and rolled out, and how you keep bundle sizes from growing without limit.

This article covers the patterns that let frontend systems scale across teams and traffic. It builds on the broader concepts of CDN architecture and load balancing but focuses on the decisions unique to frontends.


CDN for static assets

Every frontend deployment should put a CDN between users and your origin. The CDN caches static assets (JS, CSS, images, fonts) at edge locations around the world, reducing latency from hundreds of milliseconds to single digits.

Asset hashing and immutable caching

Name every built asset with a content hash: app.d8f3a1.js. Set the cache header to Cache-Control: public, max-age=31536000, immutable. The file never changes because the name changes when the content changes.

Your HTML document is the only file that cannot be immutably cached. It references the current hashed assets. Cache it for a short duration or use no-cache with ETag revalidation.

Cache invalidation at the edge

CDN cache invalidation is either instant or eventual, depending on your provider. For critical deployments:

  1. Deploy new hashed assets to the origin.
  2. Update the HTML to reference new asset filenames.
  3. Purge the HTML cache at the CDN edge (not the assets; they have new names).

This sequence ensures users never get an HTML file that references assets the CDN has not cached yet.


Edge rendering

Edge compute platforms (Cloudflare Workers, Vercel Edge Functions, AWS CloudFront Functions) let you run code at the CDN edge. For frontends, this unlocks several patterns:

  • Personalized HTML: inject user-specific content (locale, feature flags, A/B test variants) into the HTML shell at the edge without a round trip to the origin.
  • Bot rendering: serve pre-rendered HTML to search engine crawlers while serving the SPA to browsers.
  • Redirect logic: handle locale detection, device-based redirects, and auth redirects at the edge.

Edge functions have constraints: limited CPU time (typically 10 to 50ms), no persistent connections, limited runtime APIs. They are not a replacement for backend servers. Think of them as a programmable layer in your CDN.


A/B testing at the CDN layer

Running A/B tests on the frontend traditionally means the client downloads all variants and picks one at runtime. This wastes bandwidth and can cause layout shifts.

A better approach: assign the variant at the edge.

  1. The CDN edge function reads a cookie or generates a new assignment.
  2. It rewrites the request to serve the correct HTML variant (or injects a variant identifier into the HTML).
  3. The client receives only its assigned variant.

This eliminates flicker. The user never sees the wrong variant first. It also keeps the JavaScript bundle smaller because unused variants are never downloaded.

flowchart LR
  A[User Request] --> B[CDN Edge]
  B --> C{Has variant cookie?}
  C -->|Yes| D[Serve assigned variant]
  C -->|No| E[Assign variant randomly]
  E --> F[Set variant cookie]
  F --> D
  D --> G[Return HTML with variant config]

A/B test variant assignment at the CDN edge eliminates client-side flicker and reduces bundle size.


Feature flags

Feature flags decouple deployment from release. You deploy code that is hidden behind a flag and enable it for specific users, percentages, or segments without redeploying.

Architecture

A feature flag system has three parts:

  1. Flag configuration store: a service or file that maps flag names to their state and targeting rules.
  2. SDK in the client: evaluates flags at runtime, caching the configuration and falling back to defaults.
  3. Management UI: lets product and engineering teams toggle flags without code changes.

Frontend-specific concerns

Initial render flicker. If flags are fetched asynchronously after the page loads, the UI may flash between states. Solutions:

  • Embed the flag values in the HTML (set at the edge or server-side).
  • Use a loading state that does not commit to either variant.
  • Bootstrap from a cached value and update when the fresh evaluation arrives.

Bundle size. Do not ship dead code behind flags forever. Set expiration dates on flags and enforce cleanup in CI. A lint rule that flags uses of expired feature flags keeps the codebase honest.


Micro-frontends

Micro-frontends extend the microservices idea to the frontend: each team owns a slice of the UI and deploys it independently.

What they solve

  • Independent deployments: team A ships a new checkout flow without waiting for team B’s dashboard changes.
  • Technology diversity: one team can use React while another uses Svelte. In practice, this creates more problems than it solves.
  • Isolated failures: a bug in one micro-frontend does not crash the entire application (if properly sandboxed).

What they cost

  • Shared dependencies: if each micro-frontend bundles its own React, the user downloads it multiple times. Dependency sharing through import maps or module federation reduces this but adds complexity.
  • Consistent UX: a shared design system is mandatory. Without it, each team drifts toward its own button styles and spacing.
  • Integration overhead: you need a shell application that composes micro-frontends at runtime or build time. Both approaches have trade-offs.
flowchart TD
  Shell[App Shell / Container]
  Shell --> MF1[Team A: Product Catalog]
  Shell --> MF2[Team B: Shopping Cart]
  Shell --> MF3[Team C: User Profile]
  Shell --> DS[Shared Design System]
  MF1 --> DS
  MF2 --> DS
  MF3 --> DS

  subgraph "Independently Deployed"
      MF1
      MF2
      MF3
  end

Micro-frontend composition. Each team deploys independently but shares a design system through the app shell.

Composition strategies

StrategyDescriptionTrade-off
Build-time integrationMicro-frontends are npm packages composed at buildTight coupling at build, loose at runtime
Runtime via iframesEach micro-frontend in an iframeStrong isolation, poor UX integration
Runtime via JSModule federation or import mapsGood UX, complex shared state
Edge-side includesServer or CDN composes HTML fragmentsGood for static content, limited interactivity

Module federation (Webpack 5 or Rspack) is the most common runtime approach. It lets micro-frontends share dependencies and expose components to each other at runtime.


Bundle size governance

JavaScript is the most expensive resource on the web, byte for byte. It must be downloaded, parsed, compiled, and executed. A 500KB bundle on a fast laptop takes 3 seconds on a mid-range phone.

JavaScript parse and compile time scales non-linearly on mobile devices. Every KB matters.

Governance strategies

Bundle budgets. Set size limits per route in your build tool. bundlesize, size-limit, or Webpack’s performance config can fail CI if a bundle exceeds its budget.

Code splitting. Split by route at minimum. Dynamic import() defers loading of heavy features (chart libraries, rich text editors) until the user needs them.

Tree shaking. Ensure your bundler eliminates unused exports. This requires ES module syntax (import/export) and sideEffects: false in package.json for libraries.

Dependency auditing. Run npx bundlephobia <package> before adding a dependency. A 2KB utility that pulls in 200KB of transitive dependencies is not worth it.

Import analysis in CI. Tools like webpack-bundle-analyzer or source-map-explorer generate reports that visualize what is in your bundles. Review these on every PR that touches dependencies.


Putting it together

At scale, frontend deployment is not “upload files to a server.” It is a pipeline:

  1. Build with content hashing and tree shaking.
  2. Enforce bundle budgets in CI.
  3. Deploy assets to CDN with immutable caching.
  4. Deploy HTML with feature flag configuration.
  5. Assign A/B test variants at the edge.
  6. Monitor Core Web Vitals to catch regressions.

Each step catches a different class of problem. Bundle budgets catch bloat before it ships. Immutable caching prevents stale asset bugs. Edge-side variant assignment eliminates flicker. Monitoring closes the loop by proving the system works in production.


What comes next

A system that scales to millions of users must also work for all of them. Accessibility as a system design concern covers how to build accessibility into your component architecture and testing pipeline so it does not become an afterthought.

Start typing to search across all content
navigate Enter open Esc close