Skip to main content

tsup vs Rollup vs esbuild: JS Build Tools 2026

·PkgPulse Team

Three build tools dominate JavaScript library and application bundling in 2026: esbuild, Rollup, and tsup. They overlap in surface area but solve fundamentally different problems. Picking the wrong one adds config overhead and build-time pain. Picking the right one often means you never think about bundling again.

This article compares all three with real config examples, a feature matrix, and clear guidance on when to use each.

TL;DR

Use tsup for TypeScript libraries that need dual ESM/CJS output with zero configuration. Use Rollup for complex libraries requiring custom plugins, advanced tree-shaking, or non-standard file handling. Use esbuild directly for application bundling, scripts, or when you need the absolute fastest possible build with minimal output-format requirements.

Key Takeaways

  • esbuild is 10–100x faster than Rollup for raw bundling; tsup inherits esbuild's speed
  • Rollup produces smaller bundles through AST-level tree-shaking; esbuild's tree-shaking is less granular
  • tsup adds TypeScript declaration generation (--dts) and sensible defaults on top of esbuild
  • For publishing npm packages in 2026, tsup is the default choice unless you hit its limits
  • Rollup's plugin ecosystem remains unmatched for complex transformations
  • esbuild shines in CI pipelines and monorepos where build speed is the constraint

The Landscape in 2026

esbuild launched in 2020 and immediately disrupted the build tool ecosystem by being orders of magnitude faster than everything else. Rollup, which had existed since 2015, adapted rather than faded — it remains the gold standard for library tree-shaking. tsup arrived as a practical wrapper around esbuild that adds the TypeScript library workflow plumbing that esbuild intentionally leaves out.

By 2026, most npm package authors have converged on one of these three (often tsup), while application developers mostly rely on Vite (which uses esbuild for dev and Rollup for production builds internally) or direct esbuild for scripts and services.

Tool Profiles

esbuild

esbuild is a bundler and minifier written in Go. Its defining characteristic is speed: it uses all available CPU cores, parses and emits in a single pass, and skips the abstraction layers that make JavaScript-based bundlers slow.

It handles TypeScript, JSX, ESM, and CJS out of the box. What it does not do well: it does not generate TypeScript .d.ts declaration files (it strips types but does not produce them), its tree-shaking is module-level rather than AST-level, and its plugin API is intentionally minimal.

// esbuild.config.mjs
import * as esbuild from "esbuild";

await esbuild.build({
  entryPoints: ["src/index.ts"],
  bundle: true,
  minify: true,
  sourcemap: true,
  target: ["node20"],
  format: "esm",
  outfile: "dist/index.js",
});

For dual ESM/CJS output, you need two separate build calls — there is no built-in shorthand.

// Dual format with esbuild requires two passes
for (const format of ["esm", "cjs"]) {
  await esbuild.build({
    entryPoints: ["src/index.ts"],
    bundle: true,
    format,
    outfile: `dist/index.${format === "esm" ? "mjs" : "cjs"}`,
  });
}

Rollup

Rollup is a module bundler focused on producing optimal output for library distribution. Its design principle is to treat every file as an ES module, analyze the full dependency graph, and shake out every unused export at the AST node level.

The result is bundles that look hand-written and stay as small as possible. Framework authors — React, Vue, Svelte, Preact — all use Rollup. Its plugin ecosystem covers every transformation imaginable.

// rollup.config.mjs
import typescript from "@rollup/plugin-typescript";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";

export default {
  input: "src/index.ts",
  output: [
    { file: "dist/index.cjs", format: "cjs", sourcemap: true },
    { file: "dist/index.mjs", format: "esm", sourcemap: true },
  ],
  external: (id) => !id.startsWith(".") && !id.startsWith("/"),
  plugins: [
    nodeResolve(),
    commonjs(),
    typescript({ tsconfig: "./tsconfig.json" }),
  ],
};

The downside: Rollup is slower than esbuild, and the config grows complex quickly for non-trivial setups.

tsup

tsup is a zero-configuration bundler for TypeScript libraries. Under the hood it uses esbuild for transpilation and tsc (or oxc-transform in newer versions) for declaration file generation. It makes the common npm-package case — dual ESM/CJS with .d.ts files — a one-liner.

// tsup.config.ts
import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm", "cjs"],
  dts: true,
  sourcemap: true,
  clean: true,
  splitting: false,
  minify: false,
});

Or from the CLI:

tsup src/index.ts --format esm,cjs --dts --sourcemap

tsup handles externals automatically based on package.json dependencies, watches for changes in dev mode, and supports multiple entry points cleanly. It is the default recommendation in the TypeScript library ecosystem, and powers tsup vs tsdown comparisons that are already thorough on that narrower question.

Feature Comparison

FeatureesbuildRolluptsup
Build speedFastest (Go native)ModerateFast (uses esbuild)
TypeScript transpilationYesVia pluginYes (built-in)
.d.ts declaration emitNoVia pluginYes (built-in)
Tree-shaking qualityModule-levelAST-level (best)Module-level (esbuild)
Dual ESM/CJS outputManual (two passes)YesYes (one config)
IIFE/UMD outputYesYesYes
Plugin ecosystemMinimal, intentionalExtensiveesbuild plugins
Zero-config for TS libsNoNoYes
Watch modeYesYesYes
Code splittingYesYesYes
Config complexityLow–MediumMedium–HighLow
Bundle size outputModerateSmallestModerate

Speed Benchmarks

The performance gap between esbuild/tsup and Rollup is large. On a TypeScript library with ~10,000 lines of source:

ToolCold buildRebuild
esbuild~0.3s~0.05s
tsup~0.4s~0.08s
Rollup + @rollup/plugin-typescript~3–6s~1–2s

Note: tsup's declaration generation adds time on top of transpilation. With --dts on a large project, total build time is dominated by tsc, not esbuild. This is where declaration bundling tools and isolatedDeclarations mode in TypeScript 6.0 become relevant.

Tree-Shaking Quality

For library authors concerned about consumer bundle size, Rollup's tree-shaking is meaningfully better than esbuild's.

Rollup analyzes the AST at the statement level. It can eliminate dead branches inside functions, remove unused class methods, and trace side-effect-free code through complex module graphs. The output is often smaller than esbuild's even for the same input.

esbuild's tree-shaking operates at the module level: it removes entire modules that are not imported, and it handles /*#__PURE__*/ annotations for class instantiation, but it does not perform the same depth of intra-function dead code analysis.

For most utility libraries, the practical difference is small. For a large library like lodash-es where consumers import only a few functions, Rollup consistently produces smaller output.

TypeScript Declaration Files

This is the clearest win for tsup over raw esbuild. esbuild does not produce .d.ts files at all — it strips TypeScript types and emits JavaScript. If you need declarations (which you do for any npm package), you must run tsc --declaration --emitDeclarationOnly as a separate step.

tsup bundles this into a single command. With dts: true, it runs declaration generation alongside the esbuild transpilation step and handles the output directories correctly.

Rollup handles declarations via rollup-plugin-dts or by delegating to tsc. Neither is as ergonomic as tsup's built-in handling.

# Raw esbuild + tsc (manual)
esbuild src/index.ts --bundle --format=esm --outfile=dist/index.mjs
tsc --declaration --declarationDir dist/types --emitDeclarationOnly

# tsup (single command)
tsup src/index.ts --format esm,cjs --dts

Output Formats

All three tools support ESM, CJS, and IIFE. The differences are in ergonomics:

FormatesbuildRolluptsup
ESM (import/export)format: "esm"format: "esm"format: ["esm"]
CJS (require)format: "cjs"format: "cjs"format: ["cjs"]
IIFE (browser global)format: "iife"format: "iife"format: ["iife"]
Dual ESM+CJSTwo build callsSingle config arraySingle config array
.mjs / .cjs extensionsManualManualAutomatic

tsup's automatic .mjs/.cjs extension handling is one of its most underrated features. Getting Node.js ESM/CJS interop right is a common source of bugs in npm packages.

Plugin Ecosystem

Rollup's plugin ecosystem is the most mature. There are official plugins for every common need:

  • @rollup/plugin-node-resolve — resolve node_modules
  • @rollup/plugin-commonjs — convert CJS dependencies to ESM
  • @rollup/plugin-typescript — TypeScript integration
  • @rollup/plugin-json — import JSON files
  • @rollup/plugin-replace — string replacement at build time
  • rollup-plugin-visualizer — bundle analysis

esbuild's plugin API is simple and synchronous-by-design. It handles the common cases but intentionally keeps the plugin surface small to preserve speed guarantees. For custom file loaders, simple string replacements, or asset processing, esbuild plugins work well. For anything involving async transformations or complex inter-plugin state, you will hit limits.

tsup uses esbuild's plugin API, so it inherits both the capability and the limitations.

When to Use Each

Use tsup when:

  • You are publishing a TypeScript library to npm
  • You need dual ESM/CJS output with .d.ts files
  • You want zero configuration to start and sane defaults throughout
  • Build time is a concern (esbuild speed + tsup convenience)
  • You are building something similar to existing packages in the best TypeScript build tools comparison
# Getting started with tsup takes 30 seconds
npm install tsup --save-dev
npx tsup src/index.ts --format esm,cjs --dts

Use Rollup when:

  • You are building a framework or library where output bundle size matters critically
  • You need custom file transformations (Markdown processing, custom loaders, asset inlining)
  • You need AST-level tree-shaking for a large library with many optional exports
  • Your build requires multiple entry points with complex chunking strategies
  • You need an established plugin that only exists in the Rollup ecosystem

Use esbuild directly when:

  • You are bundling a Node.js application or service (not a library for distribution)
  • You need the absolute fastest build times in a CI/CD pipeline or monorepo
  • You are writing build scripts or CLI tools and do not need .d.ts files
  • You need IIFE bundles for browser scripts where declaration files are irrelevant
  • You are integrating into a larger build system where you control the full pipeline

The Vite case

If you are building a web application rather than a library, consider Vite before reaching for any of these directly. Vite uses esbuild for dev server transformation (fast) and Rollup for production builds (optimal output), giving you the best of both in a single tool. Standalone esbuild or Rollup for web apps is rarely the right choice in 2026.

Real-World Config Examples

tsup for a utility library

// tsup.config.ts
import { defineConfig } from "tsup";

export default defineConfig({
  entry: {
    index: "src/index.ts",
    utils: "src/utils/index.ts",
  },
  format: ["esm", "cjs"],
  dts: true,
  sourcemap: true,
  clean: true,
  treeshake: true,
  external: ["react", "react-dom"],
  esbuildOptions(options) {
    options.banner = {
      js: '"use client";',
    };
  },
});

Rollup for a component library

// rollup.config.mjs
import typescript from "@rollup/plugin-typescript";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import { visualizer } from "rollup-plugin-visualizer";

export default {
  input: {
    index: "src/index.ts",
    "components/Button": "src/components/Button/index.ts",
    "components/Modal": "src/components/Modal/index.ts",
  },
  output: [
    {
      dir: "dist/esm",
      format: "esm",
      preserveModules: true,
      sourcemap: true,
    },
    {
      dir: "dist/cjs",
      format: "cjs",
      preserveModules: true,
      exports: "named",
      sourcemap: true,
    },
  ],
  external: ["react", "react-dom", /^react\//],
  plugins: [
    nodeResolve(),
    commonjs(),
    typescript(),
    visualizer({ filename: "stats.html" }),
  ],
};

esbuild for a Node.js service

// build.mjs
import * as esbuild from "esbuild";

await esbuild.build({
  entryPoints: ["src/server.ts"],
  bundle: true,
  platform: "node",
  target: "node20",
  format: "esm",
  outfile: "dist/server.js",
  external: [
    // Do not bundle native addons or large deps
    "sharp",
    "better-sqlite3",
  ],
  define: {
    "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "production"),
  },
  minify: process.env.NODE_ENV === "production",
  sourcemap: true,
});

Migration and Upgrade Notes

If you are currently using Webpack for library bundling, migrating to tsup is the lowest-friction path. The mental model shift is: stop thinking about loaders and plugins for every file type, and lean on esbuild's built-in TypeScript, JSX, and JSON handling. Most Webpack library configs can be replaced by a five-line tsup.config.ts.

If you are on Rollup and considering moving to tsup for speed, the trade-off to evaluate carefully is tree-shaking quality. Run your current Rollup bundle through a size analyzer (rollup-plugin-visualizer or bundlephobia) and compare the output with tsup. For many utility libraries the difference is under 5%. For larger libraries with many optional exports, Rollup may produce meaningfully smaller consumer bundles.

If you are starting a new TypeScript library from scratch in 2026, tsup is the default. It is the bundler referenced in the official TypeScript library starter templates, it is what most open-source TypeScript projects use today, and its defaults (ESM + CJS output, source maps, declaration files, external resolution from package.json) are correct for npm publishing without any customization. See the broader best TypeScript first build tools comparison for how tsup fits into the full ecosystem.

Methodology

Comparisons in this article are based on the npm download statistics from npm-compare.com, the 2024 JavaScript bundlers comparison benchmark by Tony Cabaye, official documentation for esbuild (0.24.x), Rollup (4.x), and tsup (8.x), and community benchmarks from the Rollup GitHub discussions on tree-shaking approaches. Build time numbers are representative of mid-2025 hardware running on an M2 MacBook Pro; your numbers will vary.

Comments

Get the 2026 npm Stack Cheatsheet

Our top package picks for every category — ORMs, auth, testing, bundlers, and more. Plus weekly npm trend reports.