Setting Up rollup-plugin-visualizer in Vite

Scenario: a team that migrated from Webpack to Vite loses access to the webpack-bundle-analyzer treemap they relied on before every release. vite build still runs fine and the app still ships, but there is no equivalent report — nobody on the team can visually confirm which dependency is responsible for the vendor chunk’s size, and the migration retrospective flags “we can no longer see what’s in our bundle” as a regression.

$ npx vite build
vite v5.2.0 building for production...
✓ 214 modules transformed.
dist/assets/index-D8x2Kf.js     48.21 kB │ gzip: 17.05 kB
dist/assets/vendor-A1b9Zq.js   612.44 kB │ gzip: 189.30 kB

The build output prints a size summary, but no visual breakdown of what is inside vendor-A1b9Zq.js. rollup-plugin-visualizer closes that gap — it is the Rollup-native equivalent of webpack-bundle-analyzer, and because Vite’s production build already runs on Rollup, wiring it in requires no separate stats-emission step.

Root cause: Vite build output has no visual layer by default

This page is a focused companion to visualizing bundle composition with analyzers, which also covers webpack-bundle-analyzer and source-map-explorer; this page focuses on getting rollup-plugin-visualizer correctly wired into a Vite 5+ project.

Vite’s default production build prints per-file sizes to the terminal, which is enough to notice that a chunk is large but not enough to see why. Unlike Webpack, Vite does not ship an equivalent of stats.json by default, and there is no separate CLI tool that reads Vite’s internal module records after the fact — the visualization has to be produced by a plugin that participates in the same Rollup build pass that generates the bundle. That is exactly what rollup-plugin-visualizer does: it is a genuine Rollup plugin, registered through Rollup’s own plugin interface, that inspects the final module graph during the writeBundle phase and writes out an interactive HTML report before the build process exits.

Teams migrating from Webpack sometimes assume the two tools read the same input, since both ultimately render a treemap of the same general shape. They do not: webpack-bundle-analyzer consumes a stats.json artifact that Webpack writes out as a discrete, inspectable file, which is why it can be re-run against an old build without rebuilding. rollup-plugin-visualizer has no equivalent standalone artifact to point at — its only data source is the live module graph inside the currently running Rollup build, so the report and the build it describes are always generated together, in the same process, at the same moment.

Exact plugin configuration

Install the plugin as a dev dependency:

npm install --save-dev rollup-plugin-visualizer

Register it inside build.rollupOptions.plugins — not Vite’s top-level plugins array. This distinction is the single most common setup mistake:

// vite.config.ts — Vite 5+
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  build: {
    sourcemap: true,          // Needed if you plan to cross-check with source-map-explorer later
    rollupOptions: {
      plugins: [
        visualizer({
          filename: 'stats/report.html',
          template: 'treemap',      // 'treemap' | 'sunburst' | 'network' | 'list'
          title: 'Vendor Bundle Composition',
          gzipSize: true,
          brotliSize: true,
          emitFile: false,          // Write next to project root, skip Rollup's asset pipeline
          sourcemap: true           // Attribute size using existing source maps, not just module IDs
        })
      ]
    }
  }
});

Why build.rollupOptions.plugins, not the top-level plugins array: Vite’s own plugins array runs hooks during both the dev server and the build, using Vite’s plugin container, which wraps but does not perfectly mirror the Rollup plugin lifecycle. rollup-plugin-visualizer specifically needs the writeBundle hook to fire after Rollup has finalized chunk contents — a hook that is only guaranteed to run correctly when the plugin is registered as a genuine Rollup plugin via rollupOptions.plugins. Registering it at the top level frequently produces either an empty report or no report file at all, because the hook either never fires or fires before chunk contents are final.

The emitFile option controls whether the report is written through Rollup’s asset-emission API (which places it inside dist/ alongside your build output and gives it a content hash) or written directly to disk at the filename path you specify. Setting emitFile: false and pointing filename outside dist/ — for example into a dedicated stats/ directory — keeps the report out of your deployed static assets while still making it easy to find after every build.

Template options: treemap, sunburst, and network

The template option controls the visual layout and accepts four values:

// vite.config.ts — Vite 5+ (generating all three visual templates in one build)
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  build: {
    rollupOptions: {
      plugins: [
        visualizer({ filename: 'stats/treemap.html', template: 'treemap', gzipSize: true }),
        visualizer({ filename: 'stats/sunburst.html', template: 'sunburst', gzipSize: true }),
        visualizer({ filename: 'stats/network.html', template: 'network', gzipSize: true })
      ]
    }
  }
});

Registering the plugin three times with different filename and template values produces three separate report files from a single build — useful when a team wants both the area-accurate treemap and the dependency-overlap network view without re-running the build. template: 'list' is a fourth option that skips visualization entirely and renders a flat sortable table, which is occasionally more useful in a CI log summary than an interactive chart nobody will open.

The list template is worth calling out specifically for headless CI environments where nobody will click through a chart at all — it produces a plain HTML table of every module and its stat, gzip, and (optionally) brotli size, sortable by column. Piping that same data through the plugin’s json: true option instead of a template produces a raw JSON array, which is the right choice if the intent is to feed sizes into a custom size-diffing script rather than a human-reviewed report.

Opening the report

rollup-plugin-visualizer writes a fully self-contained static HTML file — no dev server, no build step, and no network request required to view it:

npx vite build
# then open the report directly:
open stats/report.html        # macOS
xdg-open stats/report.html    # Linux
start stats\report.html       # Windows

If the file does not exist after the build, re-check the plugin registration location and confirm the build actually completed the writeBundle phase — a build that errors out partway through will not run late-lifecycle plugin hooks.

Step-by-step verification checklist

  1. Confirm the plugin is inside rollupOptions.plugins, not the top-level plugins array. This is the fix for the single most common “report never generates” report from teams migrating off Webpack.

  2. Run a real production build, not the dev server. npx vite buildvite dev will never trigger the plugin, since there is no bundled Rollup output during development.

  3. Verify the file exists at the configured path. ls -la stats/report.html — if emitFile: false was used, the file appears directly at the filename path relative to the project root, not inside dist/.

  4. Open the report and confirm gzip and brotli sizes render. If gzipSize and brotliSize were both set to true but the report only shows raw byte counts, confirm the installed rollup-plugin-visualizer version supports both flags — versions before 5.x compute gzip size only.

  5. Cross-check the largest chunk against the vite build terminal summary. The chunk your terminal already flagged as largest (vendor-A1b9Zq.js in the scenario above) should correspond to the largest top-level rectangle in the treemap. If it does not, confirm the build that generated the report is the same build whose terminal output you are comparing against — a stale report from an earlier build is a common source of confusion.

  6. Add the report path to .gitignore. Treat it as build output, not source:

    # .gitignore
    dist/
    stats/
  7. Wire the artifact into CI. Upload the generated file so reviewers can open it from the pull request without running the build locally:

    # .github/workflows/build.yml — Vite 5+ bundle report artifact
    - name: Build and generate bundle report
      run: npx vite build
    
    - name: Upload bundle report
      uses: actions/upload-artifact@v4
      with:
        name: bundle-report
        path: stats/report.html

Edge cases and gotchas

Gotcha 1 — multi-page or library builds produce one report per Rollup invocation

If vite.config.ts defines multiple entry points through build.rollupOptions.input, or the project runs Vite in library mode with several output formats, Rollup may invoke the build pipeline more than once. Each invocation triggers the plugin separately, and by default every run overwrites the same filename. Give each build target its own filename using a conditional based on an environment variable passed from the build script:

// vite.config.ts — Vite 5+ (distinct report per build target)
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

const target = process.env.BUILD_TARGET ?? 'default';

export default defineConfig({
  build: {
    rollupOptions: {
      plugins: [
        visualizer({ filename: `stats/report-${target}.html`, template: 'treemap' })
      ]
    }
  }
});

Gotcha 2 — SSR builds silently produce a near-empty report

Frameworks like SvelteKit and SolidStart run a separate server-side Rollup build alongside the client build. If rollup-plugin-visualizer is only registered in the shared vite.config.ts without checking isSsrBuild (or the equivalent ssrBuild flag exposed by the framework), it runs against the server bundle too — which externalizes most node_modules and produces a mostly-empty treemap that looks like a broken report rather than an accurate one. Guard the plugin so it only runs for the client build:

// vite.config.ts — Vite 5+ (SSR-aware registration)
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig(({ isSsrBuild }) => ({
  build: {
    rollupOptions: {
      plugins: isSsrBuild ? [] : [visualizer({ filename: 'stats/report.html', template: 'treemap' })]
    }
  }
}));

Gotcha 3 — the network template needs a minimum dependency count to be legible

The network template renders every module as a node and every import relationship as an edge. On a project with fewer than roughly a dozen third-party dependencies, the resulting graph is too sparse to reveal anything useful, and on a project with several hundred dependencies, it becomes too dense to read without heavy zooming. network earns its keep specifically for mid-size dependency trees where the question is “which chunks share this specific package” — for anything else, the treemap remains the more legible default, matching the general guidance in the choosing between treemap, sunburst, and network templates section of the parent cluster.

Once the report reliably identifies a large chunk, the natural next step is deciding whether that chunk should be isolated into its own vendor chunk via manualChunks or reduced at the source through tighter tree-shaking configuration — the visualizer only shows the problem, it does not fix it.

FAQ

Why does rollup-plugin-visualizer produce an empty or missing report?

The most common cause is registering the plugin in Vite’s top-level plugins array instead of build.rollupOptions.plugins. Vite’s own plugins array runs plugin hooks during dev-server transforms and the initial bundling pass, but rollup-plugin-visualizer needs to run as a genuine Rollup output plugin during the production build’s bundle-generation phase, which only happens through rollupOptions.plugins.

Does rollup-plugin-visualizer work with vite build --watch or the dev server?

No. The plugin hooks into Rollup’s writeBundle phase, which only runs during a full production build (vite build). It does not run during vite dev, since Vite’s dev server serves modules individually over native ESM rather than producing a bundled output for Rollup to analyze.

Should the report file be committed to the repository?

No. Treat the report the same as any other build output — generate it fresh on every build and either open it locally or upload it as a CI artifact. Add the output path to .gitignore alongside the dist directory, since committing it produces a large binary-adjacent HTML diff on every dependency change.