Skip to content
← Back to articles
5 min read

Replacing React with Preact: Shaving 160 KB Off Every Page

Swapping React for Preact via @astrojs/preact with compat mode. Zero code changes, zero regressions, 30% less JavaScript.

Replacing React with Preact: Shaving 160 KB Off Every Page
In this post

TL;DR: Replaced React with Preact via @astrojs/preact + @preact/compat. Zero component rewrites. Zero regressions. The page bundle dropped from 565 KB to 396 KB — a 30% reduction. Every page with interactive islands now loads 160 KB less JavaScript.

The Setup

This site uses Astro 6 with React islands for a handful of interactive components:

  • EngagementDock — like/view tracking on blog posts
  • CommandPalette — Cmd+K search overlay
  • CopyEmail — clipboard copy button on the contact page
  • ToastProvider — ephemeral notification toasts
  • FilterControls — category filtering on blog and project listings
  • Giscus — GitHub-backed comments on blog posts
  • ReadingProgress — scroll progress bar on blog posts

Every one of these is a thin interactive wrapper. None of them use React’s reconciler, virtual DOM diffing, or complex tree reconciliation. They were paying the React tax — ~130 KB for React + ReactDOM — for features they don’t use.

The Migration

The swap took three changes:

1. Swap packages:

- @astrojs/react, react, react-dom, @nanostores/react
+ @astrojs/preact, preact, @preact/compat, @nanostores/preact

2. Swap integrations:

- react({ include: '**/*.{tsx,jsx}' })
+ preact({ compat: true })

3. Swap one import path:

- import { useStore } from '@nanostores/react'
+ import { useStore } from '@nanostores/preact'

That was it. @preact/compat aliases react to preact/compat at the Vite level, so every existing import { useState } from 'react' resolves to Preact’s hooks implementation. Zero component files were touched beyond that one import path change.

The Results

MetricBefore (React)After (Preact)Change
Total JS payload565 KB396 KB-169 KB (-30%)
Framework runtime~160 KB~19 KB-141 KB (-88%)
Per-page JS (with islands)321 KB201 KB-120 KB (-37%)
E2E tests passing25/2525/25No regressions

The breakdown of the new page bundle:

ChunkSizeWhat
page.*.js201 KBSentry SDK + shared vendor runtime
preact.module.*.js10.4 KBPreact core
signals.module.*.js7.7 KBPreact signals (nanostores integration)
EngagementDock.*.js7.2 KBLike/view tracking
CommandPalette.*.js6.4 KBSearch overlay
jsxRuntime.module.*.js5.7 KBPreact JSX transform
hooks.module.*.js2.7 KBPreact hooks (useState, useEffect, etc.)
client.js2.6 KBPreact client runtime
Other components~7 KBToastProvider, CopyEmail, Giscus, Filters

The biggest win is React + ReactDOM going from ~160 KB to Preact’s ~19 KB. The second win is the Astro client runtime dropping from 182 KB (React-specific) to 2.6 KB (Preact-specific).

Tradeoffs

Preact is not a drop-in replacement for every React app. Some edge cases:

  • Suspense — Not in Preact. We weren’t using it.
  • createPortal — Available in compat. We weren’t using it.
  • Third-party React libraries — Most work through compat, but exotic ones may not.
  • TypeScript types — Preact has its own types. Compat provides React-compatible types.

For this site — a content blog with thin interactive islands — Preact is a strict upgrade. The same JSX, the same hooks, the same mental model. Just less of it.

Standards reference

This article relates to the Web Development standards.

View Standards

Sponsor • Namecheap

Namecheap — Domains and hosting

Learn More

Written by Jordan Thirkle

Stay-at-home dad building AI-accelerated products. I write code during naps and after bedtime — every post comes from real work, not theory.

X GITHUB LINKEDIN NEWSLETTER
0