You can control how Vite names your output chunks, which is super useful for cache busting or organizing your build artifacts.

Let’s say you have a React app and you want to ensure that whenever your App.jsx component changes, the corresponding JavaScript chunk gets a unique name for cache busting. Here’s how you’d set that up in your vite.config.js:

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    rollupOptions: {
      output: {
        // This is where the magic happens
        chunkFileNames: 'assets/chunks/[name]-[hash].js',
        entryFileNames: 'assets/entries/[name]-[hash].js',
        assetFileNames: 'assets/static/[name]-[hash].[ext]',
      },
    },
  },
});

In this configuration:

  • chunkFileNames: This pattern dictates the naming convention for dynamically imported chunks or code-split modules.
  • entryFileNames: This pattern is for your main entry points (e.g., index.js or main.jsx).
  • assetFileNames: This pattern applies to static assets like CSS, images, and fonts.

When you run vite build, Vite uses Rollup under the hood, and these output.output options are passed directly to Rollup’s generateBundle hook. The [name] placeholder refers to the entry point or chunk name as determined by Rollup’s code-splitting logic, and [hash] is a content-based hash of the file’s contents. This hash is the key to cache busting; any change in the file’s content will result in a new hash, and thus a new filename, forcing browsers to re-download the updated asset.

Let’s see this in action. Imagine your src/main.jsx is your entry point and it imports src/App.jsx, which is then code-split by Vite.

src/main.jsx:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

src/App.jsx:

import React, { useState } from 'react';

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Vite Chunk Naming Example</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      {/* Imagine some other complex component or library is imported here */}
      {/* which Vite might code-split into a separate chunk */}
    </div>
  );
}

export default App;

After running vite build with the vite.config.js shown earlier, your output directory (typically dist/) might look something like this:

dist/
├── index.html
├── assets/
│   ├── entries/
│   │   └── main-aBcDeFgH.js
│   │   └── index-iJkLmNoP.js  // If index.html points to index.js
│   ├── chunks/
│   │   └── App-qRsTuVwX.js
│   │   └── vendor-yZaBcDeF.js // For dependencies like React
│   └── static/
│       ├── index-12345678.css
│       └── logo-98765432.png
└── ...

Notice how main.jsx (the entry point) becomes assets/entries/main-aBcDeFgH.js and App.jsx (which Vite might have decided to split) becomes assets/chunks/App-qRsTuVwX.js. The [hash] part will be different for each file based on its content. If you change App.jsx, its hash will change, leading to a new filename like App-zYxWvUtS.js, and index.html will be updated by Vite to point to this new file.

The [name] placeholder isn’t always straightforward. Rollup’s chunk naming strategy can be influenced by the order of imports, the size of the chunks, and whether you’re using dynamic import() statements. For instance, a large dependency like React might get its own chunk named vendor-[hash].js if it’s not already bundled into the entry point. Vite tries to be smart about this, but sometimes you might need to dive into Rollup’s documentation on its chunk naming heuristics if you need very fine-grained control beyond simple pattern matching.

A common misconception is that [name] will always be the original filename. This is not true; [name] is determined by Rollup’s internal naming for the chunk, which is often derived from the entry point or the module that initiated the chunk creation, but it can be transformed. For example, a module path like src/components/MyComponent.jsx might become components/MyComponent for the [name] placeholder.

One aspect of Rollup’s output configuration that often surprises people is how it handles manual chunks. If you explicitly define manual chunks in your build.rollupOptions.output object, the [name] in chunkFileNames will correspond to the key you used in that manual chunk definition. For example:

// vite.config.js
export default defineConfig({
  // ...
  build: {
    rollupOptions: {
      output: {
        chunkFileNames: 'assets/chunks/[name]-[hash].js',
        manualChunks(id) {
          if (id.includes('node_modules/lodash')) {
            return 'lodash'; // This 'lodash' will be the [name]
          }
        },
      },
    },
  },
});

In this scenario, any code from lodash would end up in a file named assets/chunks/lodash-[hash].js.

The next thing you’ll likely want to explore is how to manage different build outputs for different environments, like staging versus production, potentially using different chunk naming strategies or output directories.

Want structured learning?

Take the full Vite course →