RenderloriaRender · Measure · Refine Request a rendering review

Guide

Bundle analysis for framework apps

A bundle analysis shows which modules end up in the JavaScript your app ships, and how big each one is. It is one of the fastest ways to find start-up cost you did not mean to pay. This guide covers the tools, how to read the output and what to do with it.

Why bytes still matter, and why they are not the whole story

Every kilobyte of JavaScript must be downloaded, parsed, compiled and then executed before it does anything useful. On slower phones the parse and execution steps take far longer than the download, and they happen on the main thread, where they compete with rendering and input. Google's rendering guide notes that client-side JavaScript tends to grow with each feature and that keeping it small is hard to sustain, especially on mobile (source 1).

But size is a proxy. A small module that runs an expensive loop on every keystroke can hurt Interaction to Next Paint more than a large one that loads lazily and sits idle. Treat bundle analysis as the map that tells you where to look, and traces as the evidence of what each module costs when it runs.

The tools

Each build system has its own analyzer, and they all produce a similar picture: a treemap where each rectangle is a module, sized by its bytes.

  • Vite and Rollup: the rollup-plugin-visualizer plugin writes an interactive treemap after each build. Nuxt and SvelteKit use Vite, so the same plugin applies.
  • webpack: webpack-bundle-analyzer shows each chunk and its modules. Next.js projects that build with webpack can use the official @next/bundle-analyzer wrapper; for Turbopack builds, Next.js 16.4 extended its Turbopack Bundle Analyzer with a route summary, a sortable table, snapshots that can be diffed over time and a way to separate modules on the critical render path from async dependencies (source 4).
  • Angular: the CLI can output build statistics, which tools such as esbuild's bundle analyzer or source-map-explorer can read.
  • Any production build with source maps: source-map-explorer maps the bytes in a built file back to source modules, which works even when you cannot change the build configuration.
  • Chrome DevTools Coverage panel: shows how much of each script actually ran during a session, which reveals code that ships but is not used on that page.

Check your framework's documentation for its current recommended analyzer, since build tooling changes between major versions.

Reading the treemap

Analyzers typically report three sizes. Stat or parsed size is the code before compression, roughly what the browser must parse. Gzip or Brotli size is what travels over the network. Parsed size is the better guide to main-thread cost; compressed size is the better guide to download time.

Work through the output in this order:

  1. Entry and shared chunks. These load on every page. A large shared chunk means every route pays for code that only some routes use.
  2. The largest third-party packages. Date libraries with all locales, full icon sets, rich-text editors, charting libraries and utility libraries imported whole are typical candidates.
  3. Duplicates. Two versions of the same package, often pulled in by different dependencies, appear as separate rectangles with the same name.
  4. Polyfills and legacy builds. Code that supports browsers your users do not run.
  5. Route chunks. Each route's own code, which should hold the features specific to that route.

Questions to ask of each large module

  • Is it needed on this route? If not, move it to the route that uses it, or load it on demand with a dynamic import() when the user opens the feature.
  • Is it needed before the first interaction? If not, load it after the page becomes interactive.
  • Is it needed at all on the client? In React Server Components setups, code used only to produce markup can stay on the server if the modules that use it are not marked 'use client' (source 2).
  • Is there a smaller way? A native browser API (for example Intl.DateTimeFormat instead of a formatting library), a lighter package, or importing only the functions you use.
  • Is it duplicated? Align dependency versions so one copy ships.

Framework-specific checks

React and Next.js

Look for client components high in the tree. Because 'use client' makes a module and everything it imports client code (source 2), one directive in a layout can pull large parts of the app into every page's bundle. The analyzer shows this as unexpectedly large shared chunks containing components you thought rendered on the server.

Vue and Nuxt

Check whether components that could be lazy are bundled into the main chunk, and whether UI libraries are imported whole instead of per component. Lazy components in Nuxt (prefixed with Lazy) load their code separately (source 3).

Angular

Check that feature routes are lazy-loaded and that @defer blocks hold heavy components that are not needed at start-up.

Dynamic imports without hurting interactions

Moving a feature behind a dynamic import() removes it from start-up, but the code still has to load when the user asks for it. If that happens on click, the click waits for a network request and script evaluation, which shows up as processing time in Interaction to Next Paint. Two habits avoid this. Prefetch likely features when the browser is idle or when the user shows intent, such as hovering over or focusing the control. And give immediate visual feedback on click, then load the feature, so the next frame paints quickly even if the feature takes a moment.

let editorModule;
const loadEditor = () => (editorModule ||= import('./editor.js'));

editButton.addEventListener('pointerenter', loadEditor, { once: true });
editButton.addEventListener('click', async () => {
  showLoadingState();
  const { openEditor } = await loadEditor();
  openEditor();
});

Compression and caching

Serving JavaScript with Brotli or gzip and long cache lifetimes for hashed file names cuts download time on first and repeat visits. Neither reduces parse and execution time, though, so they complement removing code rather than replacing it.

Third-party code in the bundle

Packages from npm are only part of the picture. Tag managers, chat widgets and analytics often load as separate scripts that never appear in your bundle analysis, yet run on the same main thread. The Coverage panel and a performance trace show them; Long Animation Frames attribution names them by source URL in the field. Treat them with the same questions as bundled modules: is it needed on this route, before the first interaction, at all?

Setting a budget

Once the bundle is in shape, keep it there. Most build tools can warn or fail when a chunk exceeds a size you set, and Lighthouse can enforce budgets in continuous integration. Set budgets per route type rather than one number for the whole app, and review them when a feature genuinely needs more.

A worked example

This example is invented to show the process. A Vue dashboard's analyzer shows a 210 KB (parsed) charting library in the shared chunk. Only the reports route draws charts. Moving the import into the reports route's components and loading the chart component with defineAsyncComponent removes it from every other route's start-up cost. A trace on a throttled CPU then confirms that script evaluation on the home route is shorter, and field data over the following weeks shows whether INP input delay early in page life improved.

Checklist

  • Generate a treemap for a production build.
  • Review entry and shared chunks first.
  • List the five largest third-party modules and ask the questions above of each.
  • Remove duplicates and unused polyfills.
  • Move route-specific and on-demand features into their own chunks.
  • Add size budgets to the build.
  • Confirm the effect with traces and field data, not size alone.

Sources

  1. Rendering on the Web, web.dev Primary source (Google)
  2. 'use client' directive, React reference Primary source (React team)
  3. Components directory: delayed hydration, Nuxt docs Primary source (Nuxt team)
  4. Next.js 16.4, Next.js blog (October 6, 2026) Primary source (Vercel)

Ready when your team is

Plan your rendering review with us

Tell us about your app, its framework and where it feels slow. We reply with questions and a fixed written quote.

Start a conversation
  • Fixed written quote
  • Written findings
  • No obligation