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
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:
- In the treemap, note each top-level chunk’s filename — e.g.
search.[hash].js. - 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. - 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.
Verification
Confirm the analysis is trustworthy before acting on it:
- 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).
- 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.
- 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.
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';
},
},
},
}
Related
- Reducing JavaScript Payload in Islands Apps — the parent guide whose tree-shaking and budgeting levers act on the weights this treemap reveals.
- Code-Splitting Islands by Route and Interaction — turn an island that never got its own chunk into a real split boundary.