How to Analyze Island Bundle Size with rollup-plugin-visualizer

You cannot shrink a payload you have not attributed. When an islands app ships more JavaScript than expected, the first job is to find which island owns the bytes and which dependency inside it dominates — guessing wastes hours. This guide sets up rollup-plugin-visualizer to produce a gzip-accurate treemap of your production build and walks through reading it so every chunk maps back to a specific island. It is the measurement half of reducing JavaScript payload in islands apps; once you know where the weight is, the code-splitting and tree-shaking levers have a target.

Prerequisites


Reading an Island Bundle Treemap A stylised treemap. Three rectangles represent island chunks sized by compressed bytes: a large search chunk, a medium cart chunk, and a small carousel chunk. Inside the search chunk, a nested rectangle labelled date library occupies most of the area, showing that one dependency dominates the island's payload and is the attribution target. TREEMAP — AREA ∝ COMPRESSED BYTES search.[hash].js — 61 KB gz date library 48 KB — dominates the island island logic — 13 KB shared helpers cart — 9 KB island logic carousel 4 KB

Implementation Steps

Step 1 — Install and register the plugin

Goal: Attach the visualizer so it observes the final client bundle, not an intermediate one.

Install it as a dev dependency, then register it as the last entry in the plugins array. Order matters: the visualizer must run after every transforming plugin so it measures what actually ships.

// vite.config.js  (or the vite: {} block inside astro.config.mjs)
import { visualizer } from 'rollup-plugin-visualizer';

export default {
  plugins: [
    // ...all framework and transform plugins first...
    visualizer({
      filename: 'dist/stats.html', // written via emitFile at build end
      template: 'treemap',         // treemap is best for area-based attribution
      gzipSize: true,              // show transferred size, not raw source
      brotliSize: true,            // and brotli, which most CDNs actually serve
      emitFile: false,             // write to disk so a later build pass can't clobber it
    }),
  ],
};

For Astro specifically, this block goes inside the vite key of astro.config.mjs, and it attaches to the client build that emits the island chunks under dist/_astro/.

Expected output: The build log ends with a line noting the stats file was written, e.g. dist/stats.html.


Step 2 — Produce the treemap from a production build

Goal: Generate the treemap against optimised, minified output — a dev build is meaningless for size.

# Production build only. A dev server bundle is unminified and not code-split
# the way production is, so its sizes do not reflect what users download.
npm run build

# Open the emitted treemap.
open dist/stats.html    # macOS; use xdg-open on Linux, start on Windows

Expected output: A browser page showing nested rectangles. Top-level rectangles are output chunks; nested rectangles are the modules inside each chunk, sized by their contribution. Hovering a rectangle shows its raw, gzip, and brotli sizes.


Step 3 — Attribute chunks to islands

Goal: Map each top-level rectangle to the island that owns its entry point.

Every island is a bundler entry, so each island produces (at least) one chunk. Match them:

  1. In the treemap, note each top-level chunk’s filename — e.g. search.[hash].js.
  2. Hover into the chunk and read the module paths of the largest nested rectangles. The island’s own source file (e.g. src/components/SearchBox) confirms ownership; the dominant dependency rectangle is your optimisation target.
  3. If a heavy dependency appears nested inside two island chunks, you have duplication — it should be hoisted into a shared vendor chunk. That is the “duplicated shared dependencies” failure mode from the parent guide.
# Cross-check the file-to-island mapping against the emitted files.
ls -lh dist/_astro/*.js
# Each island's chunk name derives from its component; the visualizer's
# module list inside the chunk shows the exact source path that owns it.

Expected output: A short table you write down — island → chunk file → dominant dependency → compressed size — which becomes the input to your payload budget.


Step 4 — Confirm sizes against the network

Goal: Ensure the treemap’s numbers match what the browser actually transfers.

The treemap’s gzip figure is computed by the plugin; the authoritative number is what the server sends. Load the built page and compare.

# Serve the production build and inspect transfer sizes in DevTools → Network.
npx serve dist
# In the Network panel, the "Transferred" column for each island chunk should
# match the treemap's gzip/brotli size within a few percent (CDN brotli level
# can differ from the plugin's default).

Expected output: For each island chunk, the DevTools Transferred size is within ~5% of the treemap’s compressed size. A large discrepancy means the plugin measured a different compression level than your server uses — align them or trust the network number.


Reading a Treemap of One Island Chunk A treemap of a fifty-eight kilobyte island chunk. A date library occupies nearly half. An icon set adds a fifth. A validation library and a utility barrel take most of the rest. The component's own code is a small rectangle in the corner. Each block is annotated with the replacement that removes it. 58 KB island chunk — the component is the small square date library · 26 KB used for one relative-time string → format on the server, send the string icon set · 12 KB whole pack imported → inline the four SVGs used validation · 9 KB → server already validates utility barrel · 7 KB → import directly component 4 KB — the part you wrote Fixing the four blocks above takes this chunk from 58 KB to about 9 KB.

Verification

Confirm the analysis is trustworthy before acting on it:

  1. Total reconciles. Sum the treemap’s per-chunk gzip sizes and compare to the total script transfer in the Network panel. They should agree within a few percent. A big gap means some chunks are excluded from the treemap (see troubleshooting).
  2. Attribution is stable. Re-run the build. Chunk contents should be stable even though hashes change; the dominant dependency in each island chunk must not move. Instability points to non-deterministic chunking.
  3. Budget input is complete. Every island in your app appears as at least one chunk in the treemap. A missing island means it was statically inlined into another chunk — a signal it is not a real code-split boundary, which the code-splitting islands by route and interaction guide addresses.

Make Sure You Are Analysing the Real Build Three verifications. The analysed output must come from a production build with minification and tree shaking enabled. Sizes should be compared after compression as well as raw, because a large but highly compressible chunk behaves differently on the wire. And the chunk you inspect must be one a real page actually requests, confirmed against a network log. Three things to confirm before acting on a treemap 1 · production flags on a development build shows modules that tree shaking removes — you would optimise a phantom 2 · read raw and compressed sizes compression changes the ranking; execution cost tracks raw bytes, transfer cost tracks compressed 3 · confirm the chunk is actually requested a large chunk nobody loads is not a problem; check the network log before spending a day on it

Troubleshooting

The treemap is empty, missing, or only shows one giant chunk

Root cause: Either the visualizer is not the last plugin (so it observed an intermediate bundle), or the framework runs multiple Rollup passes and a later pass overwrote stats.html, or nothing is actually code-split so there is only one entry.

Fix: Move visualizer(...) to the end of the plugins array. Give it a unique filename so no later build pass clobbers it. If there is genuinely one chunk, your islands are not distinct entry points — verify each island is reached via a client directive or dynamic import(), not a static import from a shared bootstrap.

// Ensure a unique output name so Astro's server pass can't overwrite it.
visualizer({ filename: 'dist/client-stats.html', gzipSize: true });
Treemap sizes are far larger than the Network panel's transferred sizes

Root cause: You are reading the raw (uncompressed, parsed) size in the treemap while the Network panel shows compressed transfer. Raw size is roughly 3–4× the gzip size for typical JavaScript.

Fix: Switch the treemap to display the gzip or brotli figure (enable gzipSize: true and brotliSize: true, then select that metric in the treemap’s control). Express budgets in the compressed number, since that is what crosses the wire.

A dependency appears inside several island chunks

Root cause: A shared dependency is being duplicated into every island that imports it because no common parent chunk owns it. Total shipped bytes are inflated even though each island looks reasonable in isolation.

Fix: Hoist the dependency into a single vendor chunk with manualChunks, so it is downloaded and cached once and referenced by every island.

// vite.config.js
build: {
  rollupOptions: {
    output: {
      manualChunks(id) {
        if (id.includes('node_modules/date-fns')) return 'vendor-date';
      },
    },
  },
}

Frequently Asked Questions

Why is the treemap empty or missing after my build?

The visualizer must be the last plugin in the array so it observes the final bundle after other plugins transform it, and emitFile must be able to write to disk. If you build in a framework that runs multiple Rollup passes, such as Astro's client and server builds, ensure the plugin is attached to the client build and that the output filename is unique so a later pass does not overwrite it.

Should I read the gzip size or the raw size in the treemap?

Read the gzip or brotli size, because that is what crosses the network and what your budget should be expressed in. Raw parsed size matters for parse and compile cost, so it is worth glancing at, but the number that governs transfer and most budgets is the compressed one. Enable both gzipSize and brotliSize in the plugin options so the treemap can show them.

How do I map a hashed chunk filename back to an island?

Configure the bundler to name chunks after their entry point rather than a bare hash, or read the build manifest that maps entry modules to output files. In Astro the client build emits files under _astro with a name derived from the component, and the visualizer's module list inside each chunk shows the source path of the island that owns it.

← Back to Reducing JavaScript Payload in Islands Apps