Vite’s tree shaking is surprisingly aggressive, often eliminating code you might expect to stick around, even within your own modules.

Let’s see it in action. Imagine you have a utility file like this:

// src/utils.js
export function calculateSum(a, b) {
  return a + b;
}

export function calculateDifference(a, b) {
  return a - b;
}

export function calculateProduct(a, b) {
  return a * b;
}

And in your main application file, you only import and use one of these functions:

// src/main.js
import { calculateSum } from './utils.js';

const result = calculateSum(5, 3);
console.log(`The sum is: ${result}`);

When Vite builds this (using vite build), the output in dist/assets/main.js (or similar) will only contain the calculateSum function. The calculateDifference and calculateProduct functions will be completely gone from the final bundle. This is tree shaking at work, aggressively pruning unused exports.

This happens because Vite leverages Rollup’s powerful static analysis capabilities. During the build process, Rollup (which Vite uses under the hood for production builds) analyzes your entire dependency graph. It starts from your entry points and traces all import and export statements. If an exported module or function is never imported or used anywhere in the graph that’s being bundled, Rollup marks it as "dead code" and excludes it from the final output.

The key to effective tree shaking in Vite, and Rollup generally, is adherence to ES Module (ESM) standards for imports and exports. Dynamic import() calls and CommonJS require() statements are harder for static analysis tools to track, and often lead to entire modules being included even if only a small part is used.

Here are the main levers you control for effective tree shaking:

  • ESM Syntax: Always use import { namedExport } from 'module' and export const namedExport = .... Avoid require() and module.exports.
  • Side-effect-free Modules: Functions that don’t have side effects (like modifying global state, logging to console, or making network requests without being explicitly called) are prime candidates for tree shaking. If a module’s sole purpose is to export functions that are pure computations, it’s very likely to be shaken.
  • Configuration (Less Common): While Vite and Rollup are generally smart, sometimes you might need to explicitly tell Rollup to keep certain modules if it misinterprets them as having side effects. This is done via the build.rollupOptions.treeshake.moduleSideEffects option in vite.config.js, where you can provide a function to identify modules that do have side effects or false to tell Rollup to assume no side effects for all modules (use with extreme caution).

The magic behind this is that Rollup doesn’t just look at your direct imports. It recursively analyzes the node_modules as well. If a dependency you use only exports a few functions, and your project only imports a subset of those functions, Rollup will try to include only the necessary parts of that dependency. This is why keeping your dependencies lean and ensuring they themselves are written with ESM and minimal side effects is crucial for a truly small bundle.

One thing most people don’t realize is how deeply Rollup can analyze within a module. If you have a large utility file and only import one helper function, Rollup can often parse that file, identify which export corresponds to the imported function, and then only include the code necessary to define that specific function, potentially excluding other exported functions and even internal helper functions within that same file that are not called by the exported function.

The next hurdle you’ll likely encounter is dealing with libraries that aren’t written with tree shaking in mind, forcing you to include more code than you expected.

Want structured learning?

Take the full Vite course →