--- name: system-type-spa description: "Domain patterns for single-page application architecture — client-side routing, state management, rendering strategies, authentication, performance, and offline support. Use when designing or evaluating a browser-based SPA, progressive web app, or rich client-side application." --- # System Type: Single-Page Application (SPA) Patterns, failure modes, and anti-patterns for browser-based single-page applications. --- ## Rendering Strategies ### Client-Side Rendering (CSR) **When to use.** Interactive applications where SEO is not critical — dashboards, admin panels, internal tools, authenticated experiences. When the team already has frontend expertise and the app's value is in interactivity, not content discovery. **When to avoid.** Content-heavy marketing sites, blogs, e-commerce product pages — anything where first meaningful paint and SEO matter. When users are on slow devices or unreliable networks. **Key decisions.** Shell loading strategy (skeleton screens vs spinners), code splitting granularity, initial bundle size budget, CDN caching for static assets. ### Server-Side Rendering (SSR) **When to use.** When first contentful paint matters for conversion. Public-facing pages that need SEO. When you need the interactivity of a SPA but can't sacrifice initial load performance. **When to avoid.** Purely authenticated apps where crawlers never see content. When the backend team can't support a Node.js rendering tier. When the added infrastructure complexity isn't justified by the SEO/performance benefit. **Key decisions.** Hydration strategy (full hydration, partial, progressive, islands), streaming vs buffered SSR, caching rendered HTML, handling authentication during SSR, server cost for rendering. ### Static Site Generation (SSG) **When to use.** Content that changes infrequently — documentation, marketing pages, blogs. When you want SPA-like navigation but with pre-built HTML for instant loads. **When to avoid.** Highly dynamic, personalized content. Pages that change per-user or per-request. Large sites with millions of pages where build times become prohibitive. **Key decisions.** Build frequency, incremental regeneration strategy, preview/draft workflow, handling dynamic sections within static pages. ### Islands Architecture **When to use.** Mostly-static pages with isolated interactive widgets. When you want to ship minimal JavaScript and hydrate only the components that need interactivity. **When to avoid.** Heavily interactive applications where most of the page is dynamic. When shared state between islands creates coupling that negates the isolation benefit. **Key decisions.** Island boundary identification, shared state between islands, framework choice (Astro, Fresh, 11ty), progressive enhancement baseline. ## Client-Side Routing ### Hash-Based Routing **When to use.** Legacy browser support. Deployments where you can't configure server-side fallback routes (e.g., static file hosting without URL rewriting). Simple applications that don't need clean URLs. **When to avoid.** Any modern application where clean URLs matter. When anchor links are needed for in-page navigation. **Key decisions.** None significant — this is a fallback strategy, not a primary choice. ### History API Routing **When to use.** The default for modern SPAs. Clean URLs, proper browser back/forward behavior, shareable deep links. **When to avoid.** Environments where the server can't be configured to serve the SPA shell for all routes (static hosting without rewrite rules). In those cases, hash routing or SSG is better. **Key decisions.** Server fallback configuration (all unmatched routes serve index.html), route-based code splitting, scroll restoration, route transition animations, route guards for authentication. ### Route-Based Code Splitting **When to use.** Any SPA beyond trivial size. Load code for a route only when the user navigates to it. Essential for keeping initial bundle size manageable. **When to avoid.** Extremely small applications where the overhead of chunking exceeds the benefit. **Key decisions.** Chunk granularity (per route, per feature, per library), preloading strategy for likely next routes, loading state during chunk fetch, handling chunk load failures (version mismatch after deployment). ## State Management ### Local Component State **When to use.** UI state that belongs to a single component — form inputs, toggles, dropdown open/closed, animation state. The default; reach for shared state only when local state creates prop drilling pain. **When to avoid.** State that multiple unrelated components need. State that must survive component unmounting. **Key decisions.** When to lift state vs. when to share it. Keeping state as close to where it's used as possible. ### Global State (Redux, Zustand, Jotai, Signals) **When to use.** State shared across many components — user session, feature flags, shopping cart, notification queue. State that must be inspectable and debuggable (Redux DevTools). State with complex update logic that benefits from centralization. **When to avoid.** Simple applications where React context or component state suffices. Using global state as a default rather than an escalation — this is the most common state management mistake. **Key decisions.** Store shape (normalized vs nested), selector granularity (over-subscribing causes re-renders), middleware for side effects, persistence strategy, hydration from SSR. ### Server State (React Query, SWR, Apollo) **When to use.** Any data that comes from a server and may be stale. These libraries handle caching, revalidation, deduplication, and background refresh — problems you will solve badly if you solve them manually. **When to avoid.** Purely local state. Offline-first applications where the server is not the source of truth. **Key decisions.** Cache invalidation strategy, stale-while-revalidate timing, optimistic updates, error retry policy, query key design (determines cache boundaries), prefetching strategy. ### URL State **When to use.** Filter selections, search queries, pagination, sort order — anything the user should be able to bookmark or share via URL. Often overlooked as a state management tool. **When to avoid.** Sensitive data. High-frequency updates (every keystroke). State that doesn't make sense to share or bookmark. **Key decisions.** Serialization format (query params vs path segments), default value handling, URL length limits, backward compatibility when URL schema changes. ## Authentication Patterns ### Token-Based (JWT) **When to use.** SPAs that call APIs on a different domain. When the backend is stateless. When you need to include claims (roles, permissions) in the token to reduce backend lookups. **When to avoid.** When you can use HTTP-only cookies instead (same-domain APIs) — cookies are harder to steal via XSS. When token revocation is a hard requirement (JWTs are valid until expiry). **Key decisions.** Storage location (memory vs localStorage — memory is safer, localStorage survives refresh), refresh token rotation, token expiry duration, silent refresh mechanism, logout propagation across tabs. ### HTTP-Only Cookie **When to use.** When the API is on the same domain (or a subdomain). The most secure browser storage mechanism — not accessible to JavaScript, eliminating the XSS token theft vector. **When to avoid.** Cross-origin API calls where cookies can't be sent. Mobile app backends where cookies are awkward. **Key decisions.** SameSite attribute (Strict vs Lax), CSRF protection (double-submit cookie or synchronizer token), cookie scope (domain, path), secure flag enforcement. ### OAuth / OIDC Flows **When to use.** Third-party login (Google, GitHub, Microsoft). Enterprise SSO. When you don't want to manage passwords. **When to avoid.** Simple applications with only email/password auth where the OAuth complexity isn't justified. **Key decisions.** Authorization Code flow with PKCE (required for SPAs — implicit flow is deprecated), redirect vs popup, state parameter for CSRF, token storage after callback, session management across tabs. ## Performance ### Bundle Size **What it costs.** Every kilobyte of JavaScript must be downloaded, parsed, and executed. On a mid-range mobile device, 1 MB of JavaScript can take 3-4 seconds to parse alone. Bundle size is the single largest performance lever for SPAs. **Key strategies.** Tree shaking (ensure ESM imports), code splitting by route and feature, dynamic imports for heavy libraries, bundle analysis (webpack-bundle-analyzer, source-map-explorer), size budgets enforced in CI, replacing heavy libraries with lighter alternatives. ### Rendering Performance **What it costs.** Unnecessary re-renders cause jank, input lag, and battery drain. React's reconciliation is fast but not free — large component trees with frequent updates are the primary source of SPA sluggishness. **Key strategies.** Memoization (React.memo, useMemo, useCallback — but measure first), virtualized lists for large datasets, debounced inputs, avoiding state updates that trigger full tree re-renders, profiler-guided optimization (React DevTools Profiler, Chrome Performance tab). ### Network Performance **What it costs.** API waterfalls — sequential requests where one fetch depends on another — are the most common cause of slow SPA page loads. Users perceive network-bound delays as the app being slow. **Key strategies.** Parallel data fetching, request deduplication, prefetching on hover/focus, cache-first patterns for stable data, skeleton screens that match actual layout, avoiding layout shift during load. ### Core Web Vitals **What it costs.** LCP (Largest Contentful Paint), INP (Interaction to Next Paint), and CLS (Cumulative Layout Shift) directly affect search ranking and user perception. SPAs historically struggle with all three. **Key strategies.** Preload critical resources, inline critical CSS, reserve space for dynamic content (prevents CLS), break long tasks to keep main thread responsive (improves INP), use loading="lazy" for below-fold images. ## Offline and PWA ### Service Workers **When to use.** Offline support, background sync, push notifications, asset precaching. Any SPA that should work (even partially) without a network connection. **When to avoid.** Applications where stale data is dangerous (financial dashboards, real-time monitoring). When the team doesn't understand service worker lifecycle — a broken service worker can cache bad code with no recovery path for users. **Key decisions.** Caching strategy per resource type (cache-first for assets, network-first for API), cache versioning and cleanup, update notification to users ("new version available"), scope management, handling service worker update when tabs are open. ### Offline Data **When to use.** Applications used in unreliable network conditions — field work, mobile, transit. Forms that should not lose data on disconnection. **When to avoid.** When the conflict resolution complexity exceeds the offline benefit. When data freshness is critical and stale data causes harm. **Key decisions.** Local storage technology (IndexedDB for structured data, localStorage for simple key-value), sync strategy (background sync API, manual queue), conflict resolution on reconnection, storage quota management, user communication about offline state. ## Common Failure Modes - **Stale deployment cache.** Users load cached JavaScript bundles that reference API contracts or chunk filenames that no longer exist. Mitigation: content-hashed filenames, versioned API endpoints, chunk load error handling that forces a page refresh. - **Memory leaks from event listeners and subscriptions.** Components mount listeners or subscriptions and never clean them up. Over time, the tab consumes gigabytes. Mitigation: cleanup in useEffect return / componentWillUnmount, WeakRef where appropriate, monitoring tab memory in production. - **White screen of death.** An unhandled JavaScript error crashes the entire component tree. Mitigation: error boundaries at route and feature level, fallback UI, error reporting to monitoring service. - **State desynchronization.** Client state diverges from server state — the user sees stale data, makes decisions on wrong information, or submits conflicting updates. Mitigation: server state as source of truth (React Query/SWR), optimistic update rollback on failure, staleness indicators. - **Infinite loading states.** A failed network request with no timeout or error handling leaves the user staring at a spinner forever. Mitigation: request timeouts, error states with retry, circuit breaker for known-failing endpoints. - **Route-level code split failures.** After a deployment, users on old cached pages try to navigate to a route whose chunk filename has changed. The dynamic import fails silently. Mitigation: chunk error detection, automatic page reload on chunk load failure, service worker precaching of route chunks. - **Authentication token expiry during use.** The user is mid-workflow when their token expires. The next API call fails, and unsaved work is lost. Mitigation: silent token refresh before expiry, queuing requests during refresh, saving form state to localStorage as backup. - **Hydration mismatch.** SSR HTML doesn't match what the client renders, causing React to discard the server-rendered DOM and re-render from scratch — negating the SSR performance benefit. Mitigation: ensure server and client render identical output, suppress hydration warnings only as last resort, avoid browser-only APIs during SSR (window, localStorage). ## Anti-Patterns - **Mega-bundle.** Shipping the entire application as a single JavaScript file. Users download megabytes of code they won't use on the current page. Split by route at minimum. - **Global state for everything.** Putting form input state, UI toggles, and animation state into Redux/Zustand. Global state is for shared, cross-cutting concerns — not a replacement for component state. - **Prop drilling avoidance via global state.** Reaching for a global store because props pass through 3 components. Composition (children, render props, context) solves this without the overhead. - **Direct DOM manipulation.** Using querySelector or innerHTML alongside a virtual DOM framework. The framework can't track changes it didn't make, leading to subtle rendering bugs. - **Client-side security.** Hiding UI elements instead of enforcing permissions on the API. Every API endpoint must validate authorization independently — the client is untrusted. - **Storing secrets in JavaScript.** API keys, service credentials, or admin tokens in client-side code. JavaScript source is fully visible to users. Use a backend proxy for authenticated third-party calls. - **SPA for content sites.** Building a blog, documentation site, or marketing page as a CSR SPA. These need SEO, fast first paint, and work without JavaScript. Use SSG or SSR. - **Ignoring accessibility.** SPAs break native browser accessibility by default — no page load announcements, focus management on route change, or semantic HTML. Accessibility must be designed in, not retrofitted. - **No loading or error states.** Assuming every API call succeeds instantly. Every async operation needs three states: loading, success, error. Skipping any of these guarantees a bad user experience. - **Over-abstracting too early.** Creating a generic DataFetcher, FormBuilder, or LayoutEngine before understanding the actual use cases. Premature abstraction in frontend code creates rigid, hard-to-debug component hierarchies.