DevToolBoxGRATIS
Blog

Rollup.js: The Complete Guide to Modern JavaScript Module Bundling

18 min readby DevToolBox Team

Rollup is a JavaScript module bundler that compiles small pieces of code into larger, more complex bundles. It is the bundler of choice for JavaScript libraries and is known for producing clean, efficient output with excellent tree-shaking. Rollup uses the ES module standard natively, which means it can statically analyze your imports and exports to eliminate dead code more effectively than bundlers that rely on CommonJS. If you are building a library for npm, a framework component, or a lean application bundle, Rollup is an outstanding tool to master.

TL;DR

Rollup is a module bundler optimized for libraries and lean applications. It offers best-in-class tree-shaking, outputs ESM/CJS/UMD/IIFE, and has a rich plugin ecosystem. Use Rollup when you need clean, efficient bundles -- especially for npm packages. For complex applications with HMR needs, consider Vite (which uses Rollup under the hood for production builds).

Key Takeaways

  • Rollup produces the smallest bundles among major bundlers thanks to native ES module support and superior tree-shaking.
  • The plugin ecosystem (@rollup/plugin-*) covers every need: TypeScript, CommonJS conversion, Babel, minification, and more.
  • Multiple output formats (ESM, CJS, UMD, IIFE) from a single config make it ideal for library authors.
  • Code splitting with dynamic imports supports lazy-loading for application builds.
  • Vite uses Rollup for production builds, so Rollup plugins work in both ecosystems.

Installation and Getting Started

Rollup can be installed globally or as a project dependency. The recommended approach is to install it locally and use it via npx or package.json scripts. Rollup supports both CLI usage and configuration files for complex setups.

# Install Rollup globally
npm install -g rollup

# Or as a dev dependency (recommended)
npm install --save-dev rollup

# Verify installation
npx rollup --version

# Bundle a file (CLI)
npx rollup src/index.js --file dist/bundle.js --format esm

# Bundle with a config file
npx rollup --config rollup.config.mjs

# Watch mode
npx rollup --config --watch

Configuration File

A rollup.config.mjs file is the standard way to configure Rollup. It exports a configuration object or an array of configurations. Using .mjs extension ensures the file is treated as an ES module. You can define single or multiple outputs, which is extremely useful for library authors who need to ship ESM, CommonJS, and UMD builds simultaneously.

// rollup.config.mjs
export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'esm',
    sourcemap: true,
  },
};

// Multiple outputs (library authors)
export default {
  input: 'src/index.js',
  output: [
    {
      file: 'dist/my-lib.esm.js',
      format: 'esm',
      sourcemap: true,
    },
    {
      file: 'dist/my-lib.cjs.js',
      format: 'cjs',
      sourcemap: true,
    },
    {
      file: 'dist/my-lib.umd.js',
      format: 'umd',
      name: 'MyLib',
      sourcemap: true,
    },
  ],
};

Essential Plugins

Rollup has a rich ecosystem of official plugins under the @rollup/ scope. The most commonly used plugins include: node-resolve (resolve node_modules imports), commonjs (convert CJS to ESM), terser (minification), babel (transpilation), typescript (TS compilation), and json (import JSON files). These plugins cover 90% of typical bundling requirements.

// rollup.config.mjs with essential plugins
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';
import babel from '@rollup/plugin-babel';
import typescript from '@rollup/plugin-typescript';
import json from '@rollup/plugin-json';

export default {
  input: 'src/index.ts',
  output: {
    file: 'dist/bundle.js',
    format: 'esm',
    sourcemap: true,
  },
  plugins: [
    // Resolve node_modules imports
    resolve({
      browser: true,
      preferBuiltins: false,
    }),

    // Convert CommonJS modules to ESM
    commonjs(),

    // Compile TypeScript
    typescript({
      tsconfig: './tsconfig.json',
    }),

    // Transpile with Babel
    babel({
      babelHelpers: 'bundled',
      presets: [['@babel/preset-env', {
        targets: '>0.25%, not dead',
      }]],
      exclude: 'node_modules/**',
    }),

    // Import JSON files
    json(),

    // Minify for production
    terser({
      compress: { drop_console: true },
    }),
  ],
};

Tree-Shaking (Dead Code Elimination)

Tree-shaking is Rollup's signature feature. Because Rollup natively understands ES module import/export statements, it can statically determine which exports are used and remove unused code from the final bundle. This produces significantly smaller bundles compared to bundlers that wrap modules in function calls. Setting "sideEffects: false" in package.json helps Rollup eliminate entire unused modules.

// math.js - only used functions are included
export function add(a, b) {
  return a + b;
}

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

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

// index.js - only imports add
import { add } from './math.js';
console.log(add(2, 3));

// Rollup output: subtract and multiply are removed!
// This is tree-shaking in action.

// Mark side-effect-free modules in package.json:
// {
//   "sideEffects": false
// }
//
// Or specify files with side effects:
// {
//   "sideEffects": ["./src/polyfills.js", "*.css"]
// }

Code Splitting and Dynamic Imports

Rollup supports code splitting through multiple entry points and dynamic import() expressions. When using code splitting, output must use the "dir" option instead of "file". Rollup automatically creates shared chunks for code used by multiple entry points, and you can use manualChunks to control chunk grouping.

// rollup.config.mjs - code splitting with multiple entry points
export default {
  input: {
    main: 'src/main.js',
    admin: 'src/admin.js',
    vendor: 'src/vendor.js',
  },
  output: {
    dir: 'dist',
    format: 'esm',
    entryFileNames: '[name]-[hash].js',
    chunkFileNames: 'chunks/[name]-[hash].js',
    manualChunks: {
      // Force specific modules into named chunks
      lodash: ['lodash-es'],
      react: ['react', 'react-dom'],
    },
  },
};

// Dynamic import for lazy loading
// src/main.js
async function loadChart() {
  const { Chart } = await import('./chart.js');
  return new Chart('#canvas');
}

document.getElementById('btn').addEventListener('click', loadChart);

Output Formats: ESM, CJS, UMD, and IIFE

Rollup supports four main output formats. ESM (ES Modules) uses import/export syntax and is the modern standard for both browsers and Node.js. CJS (CommonJS) uses require() and module.exports for Node.js compatibility. UMD (Universal Module Definition) works in browsers, Node.js, and AMD loaders. IIFE (Immediately Invoked Function Expression) is for direct script tag inclusion without any module loader.

// ESM - Modern browsers and Node.js (recommended)
// Uses import/export syntax
{
  output: {
    file: 'dist/lib.esm.js',
    format: 'esm',     // or 'es'
  }
}

// CommonJS - Node.js require() style
{
  output: {
    file: 'dist/lib.cjs.js',
    format: 'cjs',
    exports: 'auto',   // 'default', 'named', 'none'
  }
}

// UMD - Universal (browsers + Node.js + AMD)
{
  output: {
    file: 'dist/lib.umd.js',
    format: 'umd',
    name: 'MyLibrary',  // global variable name
    globals: {
      react: 'React',
      'react-dom': 'ReactDOM',
    },
  },
  external: ['react', 'react-dom'],
}

// IIFE - Immediately Invoked Function Expression
// For <script> tags, no module loader needed
{
  output: {
    file: 'dist/lib.iife.js',
    format: 'iife',
    name: 'MyLibrary',
  }
}

Library Bundling Best Practices

Rollup excels at bundling libraries for npm. The key principles are: externalize all dependencies (do not bundle them), generate both ESM and CJS outputs, emit TypeScript declaration files, configure package.json exports map correctly, and keep the bundle as lean as possible.

// Complete library bundling config
// rollup.config.mjs
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import terser from '@rollup/plugin-terser';
import { readFileSync } from 'fs';

const pkg = JSON.parse(
  readFileSync('./package.json', 'utf8')
);

export default {
  input: 'src/index.ts',
  // Externalize peer dependencies
  external: [
    ...Object.keys(pkg.peerDependencies || {}),
    ...Object.keys(pkg.dependencies || {}),
  ],
  output: [
    {
      file: pkg.module,       // "dist/index.esm.js"
      format: 'esm',
      sourcemap: true,
    },
    {
      file: pkg.main,         // "dist/index.cjs.js"
      format: 'cjs',
      sourcemap: true,
      exports: 'named',
    },
  ],
  plugins: [
    resolve(),
    commonjs(),
    typescript({ declaration: true, declarationDir: 'dist/types' }),
    terser(),
  ],
};

// Matching package.json fields:
// {
//   "main": "dist/index.cjs.js",
//   "module": "dist/index.esm.js",
//   "types": "dist/types/index.d.ts",
//   "exports": {
//     ".": {
//       "import": "./dist/index.esm.js",
//       "require": "./dist/index.cjs.js",
//       "types": "./dist/types/index.d.ts"
//     }
//   },
//   "files": ["dist"]
// }

Watch Mode and Development Workflow

Rollup includes a built-in watch mode that rebuilds your bundle when source files change. You can configure which files to watch, set a rebuild delay, and use the programmatic API for custom build pipelines. For application development, consider pairing Rollup with a dev server plugin or using Vite which adds HMR on top of Rollup.

// rollup.config.mjs with watch options
export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'esm',
  },
  watch: {
    include: 'src/**',
    exclude: 'node_modules/**',
    clearScreen: false,
    // Rebuild delay (ms)
    buildDelay: 100,
  },
};

// package.json scripts
// {
//   "scripts": {
//     "build": "rollup -c",
//     "dev": "rollup -c --watch",
//     "build:prod": "NODE_ENV=production rollup -c"
//   }
// }

// Programmatic API
import { watch } from 'rollup';

const watcher = watch({
  input: 'src/index.js',
  output: { file: 'dist/bundle.js', format: 'esm' },
});

watcher.on('event', (event) => {
  if (event.code === 'BUNDLE_END') {
    console.log('Built in ' + event.duration + 'ms');
  }
  if (event.code === 'ERROR') {
    console.error(event.error);
  }
});

// Close watcher when done
// watcher.close();

Advanced Configuration

Advanced Rollup configurations often include path aliases, environment variable replacement, conditional plugins, bundle visualization, custom warning handlers, and banner/footer injection. The configuration file is plain JavaScript, so you can use any logic to build your config dynamically.

// Advanced rollup.config.mjs
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';
import replace from '@rollup/plugin-replace';
import alias from '@rollup/plugin-alias';
import { visualizer } from 'rollup-plugin-visualizer';

const isProduction = process.env.NODE_ENV === 'production';

export default {
  input: 'src/index.js',
  output: {
    dir: 'dist',
    format: 'esm',
    sourcemap: !isProduction,
    banner: '/* My Library v1.0.0 */',
  },
  plugins: [
    // Path aliases (like Webpack resolve.alias)
    alias({
      entries: [
        { find: '@', replacement: './src' },
        { find: '@utils', replacement: './src/utils' },
      ],
    }),

    // Environment variable replacement
    replace({
      preventAssignment: true,
      'process.env.NODE_ENV': JSON.stringify(
        process.env.NODE_ENV || 'development'
      ),
    }),

    resolve(),
    commonjs(),

    // Only minify in production
    isProduction && terser(),

    // Bundle visualization
    isProduction && visualizer({
      filename: 'stats.html',
      gzipSize: true,
    }),
  ].filter(Boolean),

  // Suppress specific warnings
  onwarn(warning, warn) {
    if (warning.code === 'CIRCULAR_DEPENDENCY') return;
    warn(warning);
  },
};

Writing Custom Plugins

Rollup plugins follow a simple hook-based API. The most important hooks are: buildStart (initialization), resolveId (custom module resolution), load (provide module content), transform (modify module code), and generateBundle (post-processing). Understanding this lifecycle lets you extend Rollup for any use case.

// Writing a custom Rollup plugin
function myPlugin(options = {}) {
  return {
    name: 'my-plugin',

    // Called on each build start
    buildStart() {
      console.log('Build started...');
    },

    // Resolve custom import paths
    resolveId(source) {
      if (source === 'virtual:config') {
        return source; // handle this id
      }
      return null; // let other plugins handle it
    },

    // Provide content for resolved ids
    load(id) {
      if (id === 'virtual:config') {
        return 'export default { version: "1.0" };';
      }
      return null;
    },

    // Transform individual modules
    transform(code, id) {
      if (id.endsWith('.css')) {
        return {
          code: 'export default ' + JSON.stringify(code),
          map: null,
        };
      }
    },

    // Post-processing the final bundle
    generateBundle(options, bundle) {
      for (const [name, chunk] of Object.entries(bundle)) {
        console.log(name + ': ' + chunk.code.length + ' bytes');
      }
    },
  };
}

Rollup vs Webpack vs Vite

Each bundler has distinct strengths. Rollup produces the cleanest output and is best for libraries. Webpack has the richest ecosystem and is best for complex applications. Vite uses Rollup for production and esbuild for development, offering the best developer experience with instant HMR.

FeatureRollupWebpackVite
Primary use caseLibrariesApplicationsApplications + DX
Tree-shakingExcellent (native ESM)Good (since v5)Excellent (Rollup)
Dev server / HMRPlugin neededBuilt-in (WDS)Built-in (instant)
Output cleanlinessVery cleanWrapped in runtimeClean (Rollup)
Config complexitySimpleComplexMinimal
Build speedFastModerateVery fast (esbuild)
Code splittingYesYes (advanced)Yes (Rollup)
Plugin ecosystemModerateVery largeGrowing (Rollup compat)
Bundle sizeSmallestLargerSmall (Rollup)

Best Practices

  • Always externalize peer dependencies when building libraries. Bundling React or other peer deps into your library causes duplicate copies in consumer apps.
  • Use the package.json "exports" field with "import" and "require" conditions to support both ESM and CJS consumers.
  • Enable sourcemaps during development but consider disabling them in production builds for smaller output.
  • Set "sideEffects: false" in package.json when your library is side-effect-free to enable maximum tree-shaking.
  • Use rollup-plugin-visualizer to analyze bundle size and identify opportunities for optimization.
  • Prefer @rollup/plugin-typescript over rollup-plugin-ts for TypeScript -- the official plugin is better maintained and more reliable.

Frequently Asked Questions

When should I use Rollup instead of Webpack?

Use Rollup when building JavaScript libraries, framework components, or any package published to npm. Rollup produces cleaner, smaller bundles with better tree-shaking. Use Webpack for complex applications that need advanced code splitting, HMR, and a large plugin ecosystem. For new application projects, Vite (which uses Rollup under the hood) is often the best choice.

Does Rollup support TypeScript?

Yes. Use @rollup/plugin-typescript to compile TypeScript files. It integrates with your tsconfig.json and can generate declaration files (.d.ts). For faster builds, you can also use esbuild-based alternatives like rollup-plugin-esbuild which handles TypeScript transpilation much faster (though without type checking).

How does Rollup tree-shaking work?

Rollup statically analyzes ES module import/export statements to determine which exports are used. Unused exports and their associated code are removed from the final bundle. This works because ES modules have a static structure -- imports and exports are determined at parse time, not runtime. CommonJS modules cannot be tree-shaken as effectively because require() calls are dynamic.

Can Rollup handle CSS and images?

Yes, through plugins. Use rollup-plugin-postcss for CSS (supports CSS Modules, Sass, Less), @rollup/plugin-image for image files, and rollup-plugin-copy for static assets. Rollup does not handle non-JavaScript assets natively, but its plugin system covers all common asset types.

What is the difference between ESM and CJS output?

ESM (ES Modules) uses import/export syntax and supports tree-shaking, top-level await, and static analysis. CJS (CommonJS) uses require()/module.exports and is the traditional Node.js format. ESM is the modern standard and should be the primary format. Include CJS output for backward compatibility with older Node.js versions or tools that do not support ESM.

How does Vite relate to Rollup?

Vite uses Rollup as its production bundler. During development, Vite uses esbuild for fast module transformation and serves files via native ESM. For production builds, Vite delegates to Rollup with pre-configured optimizations. This means Rollup plugins are generally compatible with Vite, and knowledge of Rollup configuration transfers directly.

Can I use Rollup for application bundling?

Yes, but with caveats. Rollup handles code splitting and dynamic imports well, but it lacks built-in HMR (Hot Module Replacement) and dev server features. For applications, Vite provides a better developer experience by wrapping Rollup with a fast dev server. Use Rollup directly for applications only if you have simple needs or a custom build pipeline.

How do I handle circular dependencies in Rollup?

Rollup supports circular dependencies in ES modules, but it emits warnings. You can suppress specific warnings using the onwarn handler in your config. The best practice is to refactor your code to eliminate circular dependencies, as they can cause subtle initialization bugs. If unavoidable, ensure the circular references do not cause issues at runtime.

Related Tools

𝕏 Twitterin LinkedIn
Was dit nuttig?

Blijf op de hoogte

Ontvang wekelijkse dev-tips en nieuwe tools.

Geen spam. Altijd opzegbaar.

Try These Related Tools

{ }JSON Formatterβœ“JSON Validator

Related Articles

esbuild: The Complete Guide to the Fastest JavaScript Bundler

Master esbuild for ultra-fast bundling with CLI, JavaScript API, plugins, loaders, minification, source maps, and production optimization.

Prettier: The Complete Guide to Opinionated Code Formatting

Master Prettier for consistent code formatting with configuration, ESLint integration, editor setup, pre-commit hooks, and CI/CD pipelines.

TypeScript Type Guards: Complete Gids voor Runtime Type Checking

Beheers TypeScript type guards: typeof, instanceof, in en aangepaste guards.