Skip to content

Preact Size Optimization — Minimizing 3kB Bundle Further

DodaTech Updated 2026-06-28 5 min read

Learn how to optimize Preact bundle size: tree shaking, code splitting, Lazy Loading, and measuring bundle impact to keep your application under 10kB.

In this lesson, you'll understand how to analyze bundle size, eliminate unused code, and apply techniques that keep Preact applications minimal.

What You'll Learn

How to analyze bundle size with Vite, apply tree shaking, use code splitting with lazy loading, avoid common bloat patterns, and measure optimization impact.

Why It Matters

Preact's main advantage is its tiny size. A poorly optimized bundle can balloon to 100kB+ with unused libraries, defeating the purpose of choosing Preact over React.

Real-World Use

Doda Browser's extension loads under 2 seconds on 3G networks because the team optimized every kilobyte: tree-shook lodash, lazy-loaded the settings panel, and avoided React-compat libraries for simple components.

flowchart LR
    A[Raw Bundle] --> B[Tree Shaking]
    B --> C[Code Splitting]
    C --> D[Lazy Loading]
    D --> E[Optimized Bundle]
    E --> F[<10kB Total]
    style A fill:#673ab8,color:#fff
    style F fill:#4a148c,color:#fff

Analyzing Bundle Size

Use Vite's bundle analyzer to see what's in your output:

npm install rollup-plugin-visualizer --save-dev
// vite.config.js
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    preact(),
    visualizer({ open: true }) // Opens analysis in browser
  ],
  build: {
    rollupOptions: {
      output: {
        manualChunks: undefined
      }
    }
  }
});

Output: After running npm run build, a treemap opens in your browser showing each file's size. Large blocks indicate optimization opportunities.

Tree Shaking

Eliminate unused exports from your code and dependencies:

// BAD — imports entire lodash library
import _ from 'lodash';
_.debounce(fn, 300);

// GOOD — imports only the debounce function
import debounce from 'lodash/debounce';
debounce(fn, 300);

// BEST — no library needed for simple debounce
function debounce(fn, ms) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
}

Output: The lodash approach adds 70kB. The per-function import adds 2kB. The custom implementation adds 0.1kB.

Code Splitting with Lazy Loading

Split your application into chunks loaded on demand:

import { lazy, Suspense } from 'preact';

// These are loaded only when needed
const Dashboard = lazy(() => import('./routes/Dashboard'));
const Settings = lazy(() => import('./routes/Settings'));
const AdminPanel = lazy(() => import('./routes/AdminPanel'));

function App() {
  return (
    <div>
      <nav>
        <Link href="/">Home</Link>
        <Link href="/dashboard">Dashboard</Link>
        <Link href="/settings">Settings</Link>
      </nav>
      <Suspense fallback={<div>Loading...</div>}>
        <Router>
          <Home path="/" />
          <Dashboard path="/dashboard" />
          <Settings path="/settings" />
        </Router>
      </Suspense>
    </div>
  );
}

Output: The initial bundle contains only Home and the shell. Dashboard and Settings are separate chunks loaded when the user navigates to those routes.

Avoiding Common Bloat

// BAD — importing preact/compat for everything
import React from 'react'; // 5kB compat layer
import { useState } from 'react'; // Could use preact/hooks directly

// GOOD — using Preact directly for core functionality
import { render } from 'preact';
import { useState } from 'preact/hooks';
import { signal } from '@preact/signals'; // 1.2kB

// BAD — bundling an entire icon library
import { Camera, Heart, Star } from 'react-feather'; // 30kB+

// GOOD — inline SVGs or a minimal icon component
function IconCamera() {
  return (
    <svg viewBox="0 0 24 24" width={16} height={16}>
      <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
      <circle cx="12" cy="13" r="4"/>
    </svg>
  );
}

Output: Avoiding preact/compat for core functionality saves 2kB. Inline SVG icons add ~0.5kB each vs 30kB for an icon library with three icons.

Size Budgets in CI

Set a size budget to prevent bundle bloat:

// vite.config.js — enforce size budget
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // Warn if any chunk exceeds 20kB
        chunkFileNames: 'assets/[name]-[hash].js',
        entryFileNames: 'assets/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash][extname]'
      }
    },
    // Report gzipped sizes
    reportCompressedSize: true
  }
});

In your CI pipeline, add a script that fails if total bundle exceeds a threshold:

npm run build
du -sh dist/assets/*.js
# Fail if any chunk > 20kB or total > 50kB

Common Mistakes

  1. Not running production builds for measurement: Development bundles include source maps and debug code. Always measure npm run build output.
  2. Importing entire libraries for one function: Instead of import { debounce } from 'lodash', use per-function packages or write a 10-line custom version.
  3. Using preact/compat when native Preact works: preact/compat adds 2kB. Only use it when you need specific React libraries.
  4. Bundling large JSON data files: Large JSON files add to the bundle. Fetch them at runtime with fetch() instead.
  5. Not code-splitting routes: A single bundle with all routes loads code the user may never visit. Split by route for faster initial load.

Practice Questions

  1. What tool visualizes bundle composition? Answer: rollup-plugin-visualizer. It generates a treemap showing the size contribution of each module.

  2. How do you lazy-load a component in Preact? Answer: Use lazy(() => import('./Component')) from preact and wrap it in <Suspense> with a fallback.

  3. What is tree shaking? Answer: A build step that removes unused exports from JavaScript bundles. It relies on ES module static structure.

  4. How much does preact/compat add to bundle size? Answer: Approximately 2kB (gzipped). Preact core is 3kB, so compat brings the total to ~5kB.

Challenge

Build a Preact app with three routes, each importing different dependencies. Analyze the bundle, then optimize by: splitting routes with lazy loading, removing unused dependencies, and replacing large libraries with custom code. Measure before and after.

Mini Project

Take an existing Preact project, run the bundle analyzer, identify the top 3 largest dependencies, and replace each with a lighter alternative or custom implementation. Document the size savings for each change.

FAQ

What is the minimum possible Preact bundle size?

: Preact core is 3kB gzipped. With preact/hooks and a few components, expect 5-8kB. A full app with routing typically runs 10-15kB.

Does code splitting improve initial load time?

: Yes. Code splitting defers non-critical code to separate chunks loaded on demand, reducing the initial bundle size and parse time.

How do I measure gzipped bundle size?

: Vite's reportCompressedSize: true in config shows gzipped sizes. Or run gzip -k dist/assets/*.js and check the .gz files.

Can I use Preact without a bundler?

: Yes. Preact can be loaded from CDN via ESM or UMD builds. For production, a bundler is recommended for optimal size.

What's Next

Learn about Preact Testing to write unit and integration tests for Preact components.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro