Build Tools — Rollup
Rollup is a module bundler for JavaScript that compiles small pieces of code into something larger and more complex, such as a library or application. Created by Rich Harris in 2015, Rollup pioneered tree-shaking and the ESM-first approach that has since influenced virtually every bundler in the ecosystem.
While Webpack dominates application bundling, Rollup is the preferred tool for publishing JavaScript libraries. Its focus on producing clean, efficient output makes it ideal for npm packages, component libraries, and any project where bundle size and ES module compatibility matter most.
Rollup excels in scenarios where Webpack and Vite (which uses Rollup under the hood in production) are overkill or produce suboptimal output. Its key advantages include:
| Feature | Rollup | Webpack | esbuild |
|---|---|---|---|
| Tree-shaking | Pioneer, most effective | Good | Good |
| ESM output | First-class support | Supported | Limited |
| Plugin ecosystem | Mature | Largest | Growing |
| Library bundling | Best in class | Overkill | Good |
| Code splitting | Supported | Mature | Limited |
info
Rollup is built around ES modules (ESM). Unlike CommonJS, ESM has a static module structure — imports and exports are known at parse time, not runtime. This static structure is what enables Rollup's powerful tree-shaking.
| 1 | // ESM — static imports enable tree-shaking |
| 2 | import { map, filter } from "lodash-es"; |
| 3 | |
| 4 | // Rollup can statically analyze this import |
| 5 | // and only include 'map' and 'filter' in the output, |
| 6 | // eliminating the rest of lodash-es |
| 7 | |
| 8 | // Tree-shaking removes "dead code" — exports that |
| 9 | // are imported but never used |
| 10 | import { useEffect, useState, useCallback } from "react"; |
| 11 | |
| 12 | // If useCallback is never referenced in the bundle, |
| 13 | // Rollup will omit it from the output entirely. |
| 14 | |
| 15 | // Side-effect detection matters: |
| 16 | import "./styles.css"; // side-effect — always included |
| 17 | import "some-polyfill"; // side-effect — Rollup may warn |
| 18 | import { knownExport } from "./utils"; // clean — tree-shakable |
warning
Rollup is configured via a rollup.config.mjs file that exports an array of configuration objects. Each object represents a build target with its own input, output, and plugins.
| 1 | // rollup.config.mjs |
| 2 | import resolve from "@rollup/plugin-node-resolve"; |
| 3 | import commonjs from "@rollup/plugin-commonjs"; |
| 4 | import typescript from "@rollup/plugin-typescript"; |
| 5 | import terser from "@rollup/plugin-terser"; |
| 6 | import dts from "rollup-plugin-dts"; |
| 7 | |
| 8 | const config = [ |
| 9 | // ESM build |
| 10 | { |
| 11 | input: "src/index.ts", |
| 12 | output: { |
| 13 | dir: "dist", |
| 14 | format: "esm", |
| 15 | sourcemap: true, |
| 16 | entryFileNames: "[name].mjs", |
| 17 | }, |
| 18 | plugins: [ |
| 19 | resolve({ extensions: [".ts", ".js"] }), |
| 20 | commonjs(), |
| 21 | typescript({ tsconfig: "./tsconfig.json" }), |
| 22 | ], |
| 23 | external: ["react", "react-dom"], // peer dependencies |
| 24 | }, |
| 25 | // CommonJS build |
| 26 | { |
| 27 | input: "src/index.ts", |
| 28 | output: { |
| 29 | dir: "dist", |
| 30 | format: "cjs", |
| 31 | sourcemap: true, |
| 32 | entryFileNames: "[name].cjs", |
| 33 | }, |
| 34 | plugins: [ |
| 35 | resolve({ extensions: [".ts", ".js"] }), |
| 36 | commonjs(), |
| 37 | typescript({ tsconfig: "./tsconfig.json" }), |
| 38 | ], |
| 39 | external: ["react", "react-dom"], |
| 40 | }, |
| 41 | // TypeScript declarations |
| 42 | { |
| 43 | input: "src/index.ts", |
| 44 | output: [{ dir: "dist", format: "esm" }], |
| 45 | plugins: [dts()], |
| 46 | }, |
| 47 | ]; |
| 48 | |
| 49 | export default config; |
info
Rollup's plugin ecosystem is mature and well-maintained. These are the essential plugins for most projects:
@rollup/plugin-node-resolve
Resolves third-party modules from node_modules using Node.js resolution algorithm. Rollup does not resolve external modules by default — this plugin is essential for any project with dependencies.
| 1 | import resolve from "@rollup/plugin-node-resolve"; |
| 2 | |
| 3 | // Basic usage — resolves all node_modules imports |
| 4 | resolve(); |
| 5 | |
| 6 | // With options — control what gets resolved |
| 7 | resolve({ |
| 8 | extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json"], |
| 9 | // Only resolve modules that match these conditions |
| 10 | resolveOnly: ["lodash-es", "date-fns"], |
| 11 | // Prevent bundling certain dependencies |
| 12 | modulesOnly: true, |
| 13 | // Prefer ESM over CJS when both are available |
| 14 | preferBuiltins: true, |
| 15 | }); |
@rollup/plugin-commonjs
Converts CommonJS modules to ESM so Rollup can process them. This is necessary because the majority of npm packages still use CommonJS, and Rollup only understands ESM natively.
| 1 | import commonjs from "@rollup/plugin-commonjs"; |
| 2 | |
| 3 | commonjs({ |
| 4 | // Include specific packages |
| 5 | include: "node_modules/**", |
| 6 | // Try to detect named exports from CJS modules |
| 7 | namedExports: { |
| 8 | "react": ["useEffect", "useState", "useCallback"], |
| 9 | "react-dom": ["createPortal", "flushSync"], |
| 10 | }, |
| 11 | // Require all requires to be in the module scope |
| 12 | strictRequires: true, |
| 13 | // Ignore dynamic requires (they break tree-shaking) |
| 14 | ignoreDynamicRequires: true, |
| 15 | }); |
@rollup/plugin-typescript
Integrates the TypeScript compiler with Rollup, handling type checking and transpilation. For faster builds, consider using @rollup/plugin-esbuild instead, which uses esbuild for transpilation.
| 1 | import typescript from "@rollup/plugin-typescript"; |
| 2 | |
| 3 | typescript({ |
| 4 | tsconfig: "./tsconfig.json", |
| 5 | // Output declaration files separately |
| 6 | declaration: true, |
| 7 | declarationDir: "./dist/types", |
| 8 | // Exclude test files from build |
| 9 | exclude: ["**/*.test.ts", "**/*.spec.ts"], |
| 10 | // Use ES2022 target for library output |
| 11 | target: "ES2022", |
| 12 | // Preserve JSX for library consumers |
| 13 | jsx: "react-jsx", |
| 14 | }); |
Rollup supports multiple output formats through the format option. Choosing the right format depends on your target audience and consumption method.
| Format | Use Case | Example |
|---|---|---|
| esm | Modern bundlers, import/export | import { foo } from "./lib" |
| cjs | Node.js require(), legacy bundlers | const foo = require("./lib") |
| umd | Browser script tags, AMD, CJS | window.MyLib = {...} |
| iife | Self-executing browser bundle | (function() {...})() |
| system | SystemJS loader compatibility | System.register(...) |
| 1 | // Multi-format build — library authors should provide ESM + CJS |
| 2 | const config = [ |
| 3 | // ESM — for modern bundlers (Vite, Webpack, Rollup itself) |
| 4 | { |
| 5 | input: "src/index.js", |
| 6 | output: { |
| 7 | file: "dist/my-lib.mjs", |
| 8 | format: "esm", |
| 9 | }, |
| 10 | }, |
| 11 | // CJS — for Node.js require() |
| 12 | { |
| 13 | input: "src/index.js", |
| 14 | output: { |
| 15 | file: "dist/my-lib.cjs", |
| 16 | format: "cjs", |
| 17 | }, |
| 18 | }, |
| 19 | // UMD — for direct browser usage |
| 20 | { |
| 21 | input: "src/index.js", |
| 22 | output: { |
| 23 | file: "dist/my-lib.umd.js", |
| 24 | format: "umd", |
| 25 | name: "MyLib", // global variable name |
| 26 | }, |
| 27 | }, |
| 28 | ]; |
info
Rollup supports code splitting via dynamic import(). When it encounters a dynamic import, Rollup creates a separate chunk that is loaded on demand. This is controlled by the output.dir option (required for code splitting) and output.entryFileNames.
| 1 | // rollup.config.mjs — code splitting config |
| 2 | export default { |
| 3 | input: { |
| 4 | main: "src/main.js", |
| 5 | admin: "src/admin.js", |
| 6 | }, |
| 7 | output: { |
| 8 | dir: "dist", |
| 9 | format: "esm", |
| 10 | // Chunk naming patterns |
| 11 | entryFileNames: "[name]-[hash].mjs", |
| 12 | chunkFileNames: "chunks/[name]-[hash].mjs", |
| 13 | // Preserve modules for tree-shaking at module level |
| 14 | preserveModules: false, |
| 15 | }, |
| 16 | plugins: [resolve(), commonjs()], |
| 17 | }; |
| 18 | |
| 19 | // Dynamic import creates automatic split points |
| 20 | // src/main.js |
| 21 | const module = await import("./heavy-feature.js"); |
| 22 | // Rollup creates: dist/chunks/heavy-feature-abc123.mjs |
warning
Publishing a library with Rollup requires careful consideration of what to bundle, what to externalize, and how to structure your output. These best practices ensure your library works across bundlers and runtimes.
| 1 | // rollup.config.mjs — library-focused configuration |
| 2 | import resolve from "@rollup/plugin-node-resolve"; |
| 3 | import commonjs from "@rollup/plugin-commonjs"; |
| 4 | import typescript from "@rollup/plugin-typescript"; |
| 5 | import terser from "@rollup/plugin-terser"; |
| 6 | |
| 7 | export default { |
| 8 | input: "src/index.ts", |
| 9 | output: [ |
| 10 | { dir: "dist", format: "esm", entryFileNames: "[name].mjs", sourcemap: true }, |
| 11 | { dir: "dist", format: "cjs", entryFileNames: "[name].cjs", sourcemap: true }, |
| 12 | ], |
| 13 | // Externalize peer dependencies — don't bundle React! |
| 14 | external: [ |
| 15 | "react", |
| 16 | "react-dom", |
| 17 | /^@namespace\//, |
| 18 | ], |
| 19 | plugins: [ |
| 20 | resolve({ extensions: [".ts", ".tsx", ".js"] }), |
| 21 | commonjs(), |
| 22 | typescript({ tsconfig: "./tsconfig.build.json" }), |
| 23 | // Minify only the UMD/IIFE builds, not ESM |
| 24 | terser({ compress: { drop_console: true } }), |
| 25 | ], |
| 26 | }; |
| 27 | |
| 28 | // package.json |
| 29 | { |
| 30 | "type": "module", |
| 31 | "main": "./dist/index.cjs", |
| 32 | "module": "./dist/index.mjs", |
| 33 | "types": "./dist/index.d.ts", |
| 34 | "exports": { |
| 35 | ".": { |
| 36 | "import": "./dist/index.mjs", |
| 37 | "require": "./dist/index.cjs", |
| 38 | "types": "./dist/index.d.ts" |
| 39 | } |
| 40 | } |
| 41 | } |
info
Choosing the right bundler depends on your specific use case. Here is a comparison to help you decide.
| Criterion | Rollup | Webpack | Vite |
|---|---|---|---|
| Primary use | Libraries | Applications | Applications |
| Dev server | No (use Vite) | Built-in | Built-in (esbuild) |
| Production build | Rollup | Webpack | Rollup |
| Tree-shaking | Excellent | Good | Excellent |
| HMR speed | N/A | Moderate | Instant |
| Plugin count | ~1,000 | ~8,000 | ~500 |
info
- Externalize peer dependencies — React, Vue, and other framework dependencies should never be bundled into your library. List them in external and mark them as peerDependencies in package.json.
- Provide ESM + CJS — Ship both formats using separate output configs. ESM for modern bundlers, CJS for Node.js and legacy tooling.
- Use source maps — Always enable sourcemap: true in development and library builds to aid debugging.
- Optimize for tree-shaking — Write modules with explicit named exports to maximize Rollup's tree-shaking effectiveness. Avoid side effects in module scope.
- Declare side effects — Add ""sideEffects": false'} to your package.json if your library has no side effects, enabling deeper tree-shaking.
- Version your plugin dependencies — Pin major versions of Rollup plugins to avoid breaking changes. Rollup's plugin API evolves slowly but breaking changes do occur.
- Use the .mjs extension for ESM output to make the module format unambiguous regardless of the project's ""type"'} field.
| 1 | // package.json — optimal library configuration |
| 2 | { |
| 3 | "name": "my-awesome-lib", |
| 4 | "type": "module", |
| 5 | "sideEffects": false, |
| 6 | "main": "./dist/index.cjs", |
| 7 | "module": "./dist/index.mjs", |
| 8 | "types": "./dist/index.d.ts", |
| 9 | "exports": { |
| 10 | ".": { |
| 11 | "import": "./dist/index.mjs", |
| 12 | "require": "./dist/index.cjs", |
| 13 | "types": "./dist/index.d.ts" |
| 14 | }, |
| 15 | "./styles.css": "./dist/styles.css" |
| 16 | }, |
| 17 | "files": ["dist"] |
| 18 | } |
Rollup provides a comprehensive CLI for building without a config file, though the configuration file approach is recommended for complex projects.
| 1 | # Install Rollup globally or as a dev dependency |
| 2 | npm install --save-dev rollup |
| 3 | |
| 4 | # Basic build with config file |
| 5 | rollup -c rollup.config.mjs |
| 6 | |
| 7 | # Build for production (minified) |
| 8 | rollup -c --environment BUILD:production |
| 9 | |
| 10 | # Watch mode — rebuild on file changes |
| 11 | rollup -c --watch |
| 12 | |
| 13 | # Build without config file |
| 14 | rollup src/index.js --file dist/bundle.mjs --format esm |
| 15 | |
| 16 | # Build with multiple formats |
| 17 | rollup src/index.js --file dist/bundle.cjs --format cjs |
| 18 | |
| 19 | # Generate source maps |
| 20 | rollup -c --sourcemap |
| 21 | |
| 22 | # Tree-shake with verbose logging |
| 23 | rollup -c --bundleConfigAsCjs --timing |
warning