Vite’s lightning-fast dev server is famous, but when it comes to production builds, you might find yourself staring at a surprisingly large dist folder and wondering where all that time went.

Let’s see Vite in action optimizing a small React app.

Here’s our starting point:

package.json

{
  "name": "vite-optimization-example",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.0.0",
    "vite": "^4.4.9"
  }
}

src/App.jsx

import React, { useState } from 'react';
import './App.css';
import _ from 'lodash'; // Heavy utility library

function App() {
  const [count, setCount] = useState(0);
  const numbers = [1, 2, 3, 4, 5];
  const shuffledNumbers = _.shuffle(numbers); // Using lodash

  return (
    <div>
      <h1>Vite Optimization Demo</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>Shuffled numbers: {shuffledNumbers.join(', ')}</p>
    </div>
  );
}

export default App;

src/App.css

body {
  font-family: sans-serif;
  padding: 20px;
}
h1 {
  color: #333;
}
button {
  padding: 10px 15px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}
button:hover {
  background-color: #0056b3;
}

Run npm install and then npm run build. You’ll see a dist directory. Let’s inspect its contents. The main JS bundle might be surprisingly large, especially if we had more components or dependencies.

The core problem Vite aims to solve with its build process is efficiently packaging your application’s code and assets for production, minimizing download size and parsing time for end-users. It leverages Rollup under the hood for this, but with Vite’s own plugins and configurations. The goal is to get you from a collection of source files and dependencies to a set of static assets that load as fast as possible.

This involves several key areas:

  1. Code Splitting: Vite automatically splits your code into smaller chunks. This means the browser only needs to download the JavaScript necessary for the current page, rather than the entire application. It intelligently identifies import statements and splits them.
  2. Tree Shaking: Unused code is removed from your final bundles. If you import a library but only use a fraction of its functions, tree shaking ensures only the used parts are included.
  3. Asset Handling: Images, CSS, and other assets are processed, often optimized (e.g., images compressed) and fingerprinted for cache busting.
  4. Minification: JavaScript, CSS, and HTML are minified to reduce their file sizes.

Let’s enhance our example to demonstrate these. First, we’ll address the heavy lodash import. Instead of importing the whole library, we can import specific functions.

src/App.jsx (Optimized Lodash Import)

import React, { useState } from 'react';
import './App.css';
import shuffle from 'lodash/shuffle'; // Import only the shuffle function

function App() {
  const [count, setCount] = useState(0);
  const numbers = [1, 2, 3, 4, 5];
  const shuffledNumbers = shuffle(numbers); // Using the specific function

  return (
    <div>
      <h1>Vite Optimization Demo</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>Shuffled numbers: {shuffledNumbers.join(', ')}</p>
    </div>
  );
}

export default App;

After running npm run build again, you’ll notice the bundle size is smaller because only the shuffle function from lodash (and its dependencies) is included, not the entire library.

To gain more control and further optimize, you’ll often interact with vite.config.js. Let’s create one:

vite.config.js

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer'; // For bundle analysis

export default defineConfig({
  plugins: [
    react(),
    visualizer({
      open: true, // Opens the report in your browser after build
      filename: 'bundle-stats.html',
      gzipSize: true,
      brotliSize: true,
    }),
  ],
  build: {
    rollupOptions: {
      output: {
        // Example: Group vendor libraries into a separate chunk
        manualChunks(id) {
          if (id.includes('node_modules')) {
            // Split node_modules into vendor chunks
            return 'vendor';
          }
        },
      },
    },
    // Example: Disable CSS splitting if you want one CSS file
    // cssCodeSplit: false,
  },
});

With vite.config.js in place, run npm run build. The visualizer plugin will generate an HTML report showing exactly what’s in your bundles and their sizes. This is invaluable for identifying large dependencies. The manualChunks configuration is a powerful way to control code splitting. By default, Vite does a good job, but you might want to group all your node_modules dependencies into a single vendor chunk, or split them further based on library type. This can improve caching, as the vendor chunk changes less frequently than your application code.

The one thing many developers miss is that Vite’s default build.rollupOptions.output.manualChunks is quite sophisticated. If you don’t provide your own manualChunks, Vite attempts to automatically split your dependencies into reasonable chunks based on import paths, aiming for a balance between the number of chunks and their size. Overriding it without understanding its defaults can sometimes lead to worse splitting.

The next step in optimizing your Vite build is often exploring advanced techniques like dynamic imports for route-based code splitting and leveraging modern browser features for faster loading.

Want structured learning?

Take the full Vite course →