SPAs ship large JavaScript bundles that block the main thread during hydration. The browser must download, parse, compile, and execute the bundle before the page becomes interactive. Solutions: code-splitting, server-side rendering, partial hydration, or migrating to a meta-framework (Next.js, Nuxt, Remix, Astro).
Single-page applications have a fundamental performance challenge: the browser receives mostly an empty HTML shell, then must download, parse, and execute a large JavaScript bundle before the page is functional. This pattern is hostile to all three Core Web Vitals.
**The specific failure modes:**
**Poor LCP** because the visible content doesn't render until JavaScript executes and React/Vue/Angular reconciles the virtual DOM. On mid-tier mobile, this is often 4–8 seconds for an unoptimized SPA.
**Poor INP** because hydration competes with user interactions. While the browser is hydrating, your click handler is queued. If hydration takes 3 seconds, user interactions during that window have 3-second INP.
**Poor CLS** because content swaps in as components hydrate, often in a different layout than the server-rendered (or empty) shell.
**Why this happens:**
Four heavyweight steps occur before an SPA is interactive:
1. **Download** — your bundle.js (often 500KB to 3MB compressed) 2. **Parse** — the JavaScript engine parses the bundle (hundreds of milliseconds on mobile) 3. **Compile** — V8 compiles the JavaScript to bytecode and optimized machine code 4. **Execute** — the framework renders, attaches event listeners, and hydrates
On a fast laptop, this happens in 200ms. On a 3-year-old budget Android, it can take 4 seconds.
**Solutions, in order of impact:**
**1. Code splitting (mandatory).**
Don't ship one giant bundle. Split by route, feature, or even component. The user only downloads what they need for the current page.
- **React:** `React.lazy()` + `Suspense`, or use a meta-framework that handles this automatically (Next.js, Remix) - **Vue:** `defineAsyncComponent`, or Nuxt for automatic route-level splitting - **Angular:** lazy-loaded modules via the router
Typical impact: 30–50% bundle size reduction → 30–50% LCP improvement.
**2. Server-side rendering (SSR) or static site generation (SSG).**
Generate the initial HTML on the server, ship complete HTML to the browser, then hydrate. The user sees content immediately even before JavaScript loads.
- **Next.js, Remix, Nuxt, SvelteKit, Astro** all handle this automatically - **Migrating an existing SPA** typically takes 4–12 weeks for a 25–50 page site
Typical impact: LCP drops from 4s+ to under 1.5s for most pages.
**3. Partial / islands hydration (advanced).**
Hydrate only the interactive parts of the page. Static content stays as plain HTML.
- **Astro** is built around this pattern - **Qwik** takes it further with "resumability" (no hydration at all; pick up from server state on demand) - **React 19+** Server Components extend this pattern to React
**4. Defer non-critical JavaScript.**
Third-party scripts (analytics, chat, A/B testing) shouldn't load synchronously with your bundle. Use `async`/`defer`, load on user interaction, or load on idle.
**5. Reduce dependency weight.**
Audit your bundle:
- **bundle-analyzer** (webpack, rollup, vite plugins) shows what's in your bundle - Common bloat: lodash imported as full library instead of cherry-picked utilities; moment.js (replace with date-fns or native Intl); jQuery still bundled in modern stacks; multiple icon libraries
Typical impact: 100–500KB savings, equivalent to 1–3 seconds on slow mobile.
**The migration math:**
A typical Canadian mid-market business with a CRA (Create React App) SPA experiencing CWV failures has three migration options:
1. **Migrate to Next.js or Remix.** 4–8 week project. Largest performance gain (typically pass CWV after migration). Recommended for most. 2. **Migrate to Astro for marketing pages.** 2–4 week project. Best for content-heavy sites with limited interactivity. 3. **Stay on SPA + aggressive optimization.** 2–4 week project. Code splitting + dependency reduction + hydration optimization. Often gets to "passes CWV" but more brittle than full SSR.
For most marketing sites in 2026, the right answer is meta-framework migration. SPAs were a 2018 architectural choice that has aged poorly for content sites.
- **What is INP and how do I fix poor INP scores?** — Interaction to Next Paint — measures how quickly your page responds to user input. Should be under 200ms (good) or under 500ms (acceptable). Replaced FID in March 2024. Most pages with poor INP have heavy JavaScript event handlers or excessive third-party scripts blocking the main thread. - **How do I fix a poor Largest Contentful Paint (LCP) score?** — LCP should be under 2.5 seconds on mobile. Five fixes that work for 90% of sites: (1) optimize and preload your hero image, (2) eliminate render-blocking resources above the fold, (3) use a CDN, (4) enable HTTP/2 or HTTP/3, (5) reduce server response time (TTFB) under 600ms. - **What's a good Core Web Vitals score in 2026?** — All three metrics in the 'Good' threshold (LCP <2.5s, INP <200ms, CLS <0.1) at the 75th percentile of mobile users over the trailing 28 days. About 40% of websites achieve this in 2026 — passing all three is a meaningful competitive edge. - **Lab vs field data — which one does Google actually use?** — Field data (real user measurements) is what Google uses for ranking. Lab data (synthetic Lighthouse runs) is for debugging only. A site can have perfect Lighthouse scores and still fail Core Web Vitals if real users experience poor performance.