How to Analyze Vite Bundles with rollup-plugin-visualizer

This guide covers the main analysis tool for Vite & Rollup Build Optimization, part of JavaScript Bundle Optimization & Code Splitting. Before changing chunking or replacing a dependency, you need to see what is in each chunk, how large it is in the units that matter, and why it is there. rollup-plugin-visualizer produces that picture from the Rollup build that Vite runs in production.

Reading the output correctly matters as much as generating it. The plugin reports several sizes — rendered (after tree-shaking, before minification in some setups), gzip and brotli — and offers several views. Teams that only look at the biggest rectangle in the treemap often optimise the wrong thing: a library that is large in total but only on a lazy route, while a smaller library on the critical path does more damage to LCP and INP.

From build to decision Workflow from generating the visualiser report to deciding on a bundle change. From build to decision Build with plugin stats.html + raw data Pick the chunk what the route loads Find largest modules by gzip size Ask why importer chain Change lazy, replace, dedupe

Rapid Diagnosis

  • Generate the report on a production build. The plugin only reflects what Rollup bundles; the dev server shows nothing useful.
  • Start from the route, not the biggest box. Identify which chunks your landing route loads (Network panel or the Vite manifest), then inspect those.
  • Use gzip or brotli size for transfer, rendered size for parse cost. Both matter; they tell different stories.
  • Look for duplicates. The same package name appearing under several chunks or at several versions.
  • Look for surprises. Locale files, development builds of libraries, test utilities, or full icon sets in production chunks.

Root Cause Analysis: What Analysis Usually Reveals

1. Libraries imported for one function. A date or utility library included wholesale because of a namespace import or a non-tree-shakeable build.

2. Large dependencies on the critical path. A rich-text editor or chart library statically imported by a component rendered on the landing route.

3. Duplicate versions. Two major versions of a library pulled in by different dependencies, both shipped.

4. Assets inlined as JavaScript. SVG icon sets, JSON data or images converted to base64 inside JS chunks, inflating parse cost.

Top modules in a landing-route chunk (brotli) Bar chart of the largest modules in a landing route's JavaScript chunk as revealed by the visualiser. Top modules in a landing-route chunk (brotli) moment + all locales 72KB lodash (full build) 25KB react-dom 42KB icons (full set as JSX) 38KB App components 31KB

Step-by-Step Workflow

1. Configure the plugin for useful output

javascript
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default {
  plugins: [
    visualizer({
      filename: 'dist/stats.html',
      template: 'treemap',          // also: 'sunburst', 'network', 'raw-data', 'list'
      gzipSize: true,
      brotliSize: true,
      emitFile: false,
    }),
  ],
};
// trade-off: computing gzip and brotli sizes slows large builds noticeably.
// Enable the plugin only in an "analyze" build mode rather than on every build.
bash
ANALYZE=1 npx vite build && open dist/stats.html
# trade-off: opening the report locally is fine; publishing stats.html with your
# site exposes your dependency list and file paths. Keep it out of the deploy.

Expected outcome: an interactive report you can filter by chunk and module.

2. Use the right view for the question

  • Treemap — what is big in each chunk.
  • Sunburst — how a package's size is distributed across its internal files (useful for libraries with many submodules).
  • Network — which modules import which, to answer "why is this here?".
  • Raw data / list — machine-readable output for CI comparisons.

Which visualiser view answers which question Mapping of rollup-plugin-visualizer templates to the analysis question each is best for. Which visualiser view answers which question View Best question Watch out for treemap What is large in this chunk? area hides small duplicated pieces sunburst Which part of a package is large? hard to compare chunks network Why is this module included? busy on big graphs; filter first raw-data / list Did size change in this PR? diff-friendly for CI

3. Trace why a module is included

In the network view, select the surprising module and follow its importers back to your code. The first import from your own source is where to intervene — replace a namespace import, change a barrel import to a direct path, or turn a static import into a dynamic one.

javascript
// Before: pulls the whole icon set into the landing chunk.
import * as Icons from '@acme/icons';
// After: import only what the component renders.
import { SearchIcon } from '@acme/icons/search';
// trade-off: deep imports couple you to a package's internal file layout,
// which can change in minor versions. Prefer packages that publish
// per-icon entry points or sideEffects-free ESM that tree-shakes reliably.

Expected outcome: the module disappears from the chunk, or moves to the lazy chunk where it belongs.

4. Compare reports between builds in CI

Emit raw-data JSON on the main branch and on each pull request, and post a size diff per chunk as a PR comment. Large unexplained growth is caught before merge.

javascript
// scripts/diff-stats.mjs — compare per-chunk brotli totals from two raw-data reports.
import base from './base-stats.json' with { type: 'json' };
import head from '../dist/stats.json' with { type: 'json' };
function perChunk({ nodeMetas, nodeParts }) {
  const out = {};
  for (const meta of Object.values(nodeMetas)) {
    for (const [chunk, partUid] of Object.entries(meta.moduleParts)) {
      out[chunk] = (out[chunk] ?? 0) + (nodeParts[partUid]?.brotliLength ?? 0);
    }
  }
  return out;
}
const before = perChunk(base), after = perChunk(head);
for (const chunk of new Set([...Object.keys(before), ...Object.keys(after)])) {
  const delta = (after[chunk] ?? 0) - (before[chunk] ?? 0);
  if (Math.abs(delta) > 2048) console.log(`${chunk}: ${delta > 0 ? '+' : ''}${(delta / 1024).toFixed(1)} KB brotli`);
}
// trade-off: the raw-data schema is internal to the plugin and changes between
// major versions. Pin the plugin version, or use size-limit for stable CI output.

Expected outcome: reviewers see "landing chunk +38KB brotli" next to the code that caused it.

Verification

After each change, rebuild and confirm the module has left the critical-path chunk or shrunk, then measure the route in a throttled lab run: JavaScript transfer size, scripting time and TBT should all fall. Keep the before and after reports; they make the change easy to explain in code review and in a performance changelog.

Reading Sizes Correctly

The three sizes answer different questions. Rendered size is the module's code as it appears in the output chunk before compression; it is the best proxy for parse and compile time. Gzip and brotli sizes estimate bytes on the wire; brotli is usually what modern CDNs serve. A module that compresses exceptionally well — repetitive JSON, generated code — can look small in brotli and still cost significant parse time. Conversely, already-minified, high-entropy code compresses less. When the goal is LCP on slow networks, prioritise brotli size on the critical path; when the goal is INP and TBT on slow CPUs, prioritise rendered size.

Common Findings and Their Fixes

  • Full locale sets (moment, date libraries, i18n bundles): switch to modular date libraries or load locales dynamically per user language.
  • Development builds in production: a library resolved through a browser or development condition — check resolve.conditions and NODE_ENV defines.
  • Polyfills for features every target supports: tighten Browserslist and the build target.
  • JSON or SVG inlined into JS: load large data with fetch and import SVG as URLs rather than components, unless they are tiny.
  • Two copies of the framework: usually a linked local package with its own node_modules; use resolve.dedupe.

FAQ

Why does the visualiser total differ from the files in dist?

The plugin measures module contributions within chunks, sometimes before final minification or without Rollup's runtime helpers, and its gzip estimates compress each module separately rather than the whole file. Treat its numbers as relative — which modules are large — and use the actual emitted files for absolute budgets.

Is vite-bundle-visualizer different?

It is a thin CLI wrapper that runs your Vite build with rollup-plugin-visualizer configured, so you do not need to edit your config. The output and views are the same.

Can it analyse the SSR bundle?

Yes, if the plugin runs during the SSR build too. The server bundle's size rarely matters for Core Web Vitals, but analysing it can reveal code accidentally shared with the client or dependencies that should be externalised.