{"id":"8gh03vqah3onu0c","title":"React SPA Overhead: The Real Cost Beyond Hello World","slug":"react-spa-overhead-real-cost","summary":"Create React App made SPAs feel free. Hydration, routers, and data libraries add up. I measure before I recommend React for marketing pages.","imageUrl":"https://briancrabtree.me/images/journal-react-spa-overhead-real-cost.webp","category":"React","date":"2026-01-26T18:00:00.000Z","featured":false,"likes":33,"author":"Brian Crabtree","content":"<h2>The baseline is not zero</h2>\n\n<p>React 19 improved many paths, but you still ship a runtime, a reconciler, and your app code. Hello world is a lie teams budget against. Production adds react-router, state libraries, form kits, and icon packs before anyone writes a feature.</p>\n\n<p>I compare against static HTML plus sprinkles. If the page is mostly content with one accordion, you do not need a root render. If it is a logged-in trading desk, you do.</p>\n\n<p>Strict Mode double rendering in dev surprises teams measuring local perf. I teach that lab prod builds matter for bundle size, not Strict Mode alone.</p>\n\n<pre><code>// Initial bundle cost — measure on 3G, not just Wi-Fi\nnpm run build &amp;&amp; npx vite-bundle-visualizer</code></pre>\n\n<p><figure>\n  <img src=\"/images/journal-inline-react-spa-overhead.webp\" alt=\"Bundle size comparison chart of React SPA versus static HTML page weight\" width=\"1200\" height=\"675\" loading=\"lazy\" />\n  <figcaption>Hello World hides the real cost — measure your production bundle on 3G.</figcaption>\n</figure></p>\n\n<h2>Hydration tax on marketing URLs</h2>\n\n<p>Search and social crawlers see HTML first; users see HTML then JavaScript replaces behavior. If the hero waits on useEffect, LCP slips. I prerender shells or SSR the above-fold tree so the headline and image exist in the first byte stream without waiting on bundle parse.</p>\n\n<p>Hydration mismatch warnings are symptoms of shipping different HTML server versus client. Fix the data source, do not patch with suppressHydrationWarning as a lifestyle.</p>\n\n<p>Server Components are not free complexity. They require Node deploy path and clear client boundaries. Small marketing sites on static hosts may never justify that ops surface.</p>\n\n<h2>Routing and data on the client</h2>\n\n<p>Client routers download route chunks after boot. That is fine behind login; on public homepages it delays secondary routes and sometimes prefetch libraries you never configured. I lazy-load with intent: hover on nav links, not import everything in App.tsx.</p>\n\n<p>Fetching JSON in useEffect after paint creates waterfalls. I colocate loaders with routes, use server components or static loaders where the stack allows, and parallelize independent requests. One waterfall costs more than one extra kilobyte of gzip.</p>\n\n<p>I measure hydration time in Performance panel, not only bundle KB. A 80 KB bundle that hydrates in 40ms beats 40 KB that hydrates in 200ms due to layout thrash.</p>\n\n<h2>State libraries you might not need</h2>\n\n<p>useState and context cover most marketing sites. Global stores shine when many distant components mutate shared realtime data. I remove Redux from brochure sites and watch TBT drop. React Compiler reduces manual memo noise but does not remove store subscription overhead.</p>\n\n<p>Forms are the hidden weight: react-hook-form plus zod plus UI kit is heavier than native form actions with server validation in Next or Remix patterns. Pick the stack that matches your validation surface.</p>\n\n<p>Legacy class components still appear in enterprise forks. Migration to function components is separate from deciding whether the route should be static.</p>\n\n<h2>When React still wins</h2>\n\n<p>Dense dashboards, drag-and-drop builders, and collaborative editors need a component model. React ecosystem depth for accessibility primitives, testing, and hiring pool matters for product companies. I am not anti-React; I am anti-React by default on static content.</p>\n\n<p>This site uses React where interaction density justifies it, with prerender and deferred Firebase to keep lab scores honest.</p>\n\n<p>Micro-frontends multiply SPA overhead. I challenge those architectures early with total JS weight math across all federated remotes.</p>\n\n<h2>Alternatives without dogma</h2>\n\n<p>Astro islands, Enhance, or plain Vite multi-page apps ship less JavaScript for blogs and docs. Next with server components can cut client JS if you discipline use client boundaries. The answer is architectural, not tribal.</p>\n\n<p>I prototype the same hero in static HTML and React, measure LCP and INP, and let numbers pick. Egos lose to waterfalls every time.</p>\n\n<p>Edge SSR on Vercel or Cloudflare helps TTFB but still sends client JS. The win is HTML sooner, not zero React tax.</p>\n\n<h2>Audit offer</h2>\n\n<p>Send your homepage bundle graph and Lighthouse mobile trace. I will tell you whether React is earning its keep or acting as expensive templating. Sometimes the fix is smaller than a rewrite; sometimes it is MPA escape hatch.</p>\n\n<p>If the product roadmap is three screens and two forms, I prototype in HTML first and let stakeholders click before we npm install react-dom.</p>\n\n<p>When I audit react stacks, Audit offer is where hours disappear if nobody owns the outcome. I write before-and-after notes, attach PSI or waterfall screenshots, and leave the team a three-item follow-up list. Reproducible wins beat toolchain debates in sprint review. If this section matches your bottleneck, send the URL and whether LCP, INP, CLS, or crawl coverage is the pain point. For a related angle I keep coming back to, see <a href=\"/journal/static-prerender-shells-spa/\">Static Prerender Shells: SPAs That Paint Before JavaScript</a>.</p>","tags":["react","spa","performance"],"views":102}