<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/tsup-vs-rollup-vs-esbuild-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/tsup-vs-rollup-vs-esbuild-2026/raw.md -->
<!-- Source path: content/guides/tsup-vs-rollup-vs-esbuild-2026.mdx -->

---
og_image: "/images/guides/tsup-vs-rollup-vs-esbuild-2026.webp"
title: "tsup vs Rollup vs esbuild 2026: Speed & TypeScript"
description: "tsup vs esbuild vs Rollup in 2026: compare library defaults, declaration files, tree-shaking, plugins, and build-speed trade-offs with sourced guidance."
date: "2026-03-18"
author: "PkgPulse Team"
tags: ["developer-tools", "build-tools", "typescript", "javascript"]
---

If you are searching for **tsup vs esbuild**, the short answer is: use tsup when you are publishing a TypeScript library and want esbuild speed plus package-friendly defaults; use esbuild directly when you control the whole build script and do not need declaration-file plumbing. Rollup stays in the comparison because it is still the strongest choice when output shape, plugin depth, and tree-shaking quality matter more than setup speed.

This refreshed guide compares tsup, esbuild, and Rollup with current official docs and npm-registry checks, then narrows the decision to the library-publishing questions that usually matter in 2026: ESM/CJS output, `.d.ts` files, tree-shaking, plugins, and CI build time.

## TL;DR

Use **tsup** for TypeScript libraries that need dual ESM/CJS output, source maps, externals, and declaration generation from a small config. Use **esbuild** directly for fast scripts, Node services, application bundles, and custom build pipelines where you do not need tsup's package-publishing wrapper. Use **Rollup** when plugin compatibility, output control, and library tree-shaking are the deciding factors.

## Key Takeaways

- **tsup vs esbuild** is mostly a packaging question: tsup wraps esbuild with library defaults, while raw esbuild leaves declaration files and package output conventions to you.
- esbuild's official docs still emphasize speed and broad loader support; tsup's official package positioning is specifically "Bundle your TypeScript library with no config, powered by esbuild."
- Rollup remains the safest answer for complex libraries that need mature plugins, precise chunks, or the best tree-shaking behavior.
- For npm packages in 2026, start with tsup unless you already know you need Rollup's plugin/output control or raw esbuild's lower-level API.
- Do not make the decision from benchmark headlines alone; compare your actual project with declarations, externals, sourcemaps, and minification enabled.

## 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 intentionally simpler than Rollup's, and its plugin API is intentionally minimal.

```javascript
// 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.

```javascript
// 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 give library authors fine-grained control over unused exports, side effects, and output shape.

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.

```javascript
// 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 JavaScript bundling and integrates declaration-file generation behind the `dts` option, so the common npm-package case — dual ESM/CJS with `.d.ts` files — is a one-liner without hand-rolling a separate esbuild-plus-`tsc` pipeline.

```javascript
// 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:

```bash
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](/guides/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026) that are already thorough on that narrower question.

## Feature Comparison

| Feature | esbuild | Rollup | tsup |
|---|---|---|---|
| Build speed | Fastest (Go native) | Moderate | Fast (uses esbuild) |
| TypeScript transpilation | Yes | Via plugin | Yes (built-in) |
| `.d.ts` declaration emit | No | Via plugin | Yes (built-in) |
| Tree-shaking quality | Declaration-level dead-code removal; simpler than Rollup | Deepest/library-focused control | esbuild-powered by default; less configurable than Rollup |
| Dual ESM/CJS output | Manual (two passes) | Yes | Yes (one config) |
| IIFE/UMD output | Yes | Yes | Yes |
| Plugin ecosystem | Minimal, intentional | Extensive | esbuild plugins |
| Zero-config for TS libs | No | No | Yes |
| Watch mode | Yes | Yes | Yes |
| Code splitting | Yes | Yes | Yes |
| Config complexity | Low–Medium | Medium–High | Low |
| Bundle size output | Moderate | Smallest | Moderate |

## Speed Benchmarks and How to Read Them

esbuild's official positioning is still speed-first: it is written in Go, parallelizes heavily, and avoids many JavaScript bundler abstraction layers. tsup inherits that fast transpilation path because it is powered by esbuild. Rollup is usually slower in raw bundling tests, but it buys you deeper output control and a larger plugin ecosystem.

For a fair local comparison, benchmark the exact workflow you will ship:

| Question to test | Why it matters |
|---|---|
| `tsup --dts --sourcemap --format esm,cjs` | Declaration generation can dominate wall-clock time even if esbuild transpilation is fast. |
| Rollup with your real plugins | Plugin transforms and chunk rules are often the actual bottleneck. |
| Raw esbuild plus `tsc --emitDeclarationOnly` | This is the honest baseline for a typed npm package, not esbuild alone. |
| A warm CI rebuild | Cache behavior and dependency installation can outweigh bundler differences. |

The practical conclusion for **tsup vs esbuild**: esbuild is the faster primitive; tsup is the better default interface for TypeScript package publishing. If a benchmark excludes declarations, externals, sourcemaps, and package metadata, treat it as a lower-bound speed check rather than a publishing decision.

## Tree-Shaking Quality

For library authors concerned about consumer bundle size, Rollup's tree-shaking is usually the safer choice when output shape and side-effect control matter more than raw build speed.

Rollup exposes mature controls for preserving modules, marking side effects, shaping chunks, and letting plugins participate in the full bundle graph. That gives library authors more room to tune consumer-facing output for complex packages.

esbuild's official docs describe tree shaking as declaration-level dead-code removal. It removes unused top-level declarations and honors side-effect signals such as `sideEffects` metadata and `/* @__PURE__ */` annotations, but it intentionally keeps the model simpler than Rollup's library-bundling workflow.

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 a declaration-generation path as part of the tsup build and handles the output directories correctly, while leaving raw esbuild users to wire declaration emit themselves.

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

```bash
# 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:

| Format | esbuild | Rollup | tsup |
|---|---|---|---|
| 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+CJS | Two build calls | Single config array | Single config array |
| `.mjs` / `.cjs` extensions | Manual | Manual | Automatic |

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](/guides/best-typescript-build-tools-2026)

```bash
# 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 fine-grained tree-shaking and side-effect controls 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 abstracts most bundler choices behind its dev/build workflow, and the current Vite/Rolldown transition means you should follow the Vite docs rather than hard-coding an old esbuild-plus-Rollup mental model. Standalone esbuild, Rollup, or tsup is usually the right comparison for packages, services, CLIs, and custom build pipelines — not a default Vite app.

## Real-World Config Examples

### tsup for a utility library

```typescript
// 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

```javascript
// 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

```javascript
// 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](/guides/best-typescript-first-build-tools-2026) for how tsup fits into the full ecosystem.

## Sources checked

Current source checks during this refresh (accessed 2026-05-16):

- [tsup official documentation](https://tsup.egoist.dev/) and [tsup GitHub repository](https://github.com/egoist/tsup) — package-publishing positioning, config shape, and esbuild-powered implementation.
- [tsup npm package](https://www.npmjs.com/package/tsup) and npm registry metadata — latest version and package description.
- [esbuild official documentation](https://esbuild.github.io/) — API shape, supported formats, TypeScript behavior, and performance-oriented design.
- [esbuild npm package](https://www.npmjs.com/package/esbuild) and npm registry metadata — latest version and package positioning.
- [Rollup official documentation](https://rollupjs.org/) — output formats, plugin model, and library-bundling configuration.
- [Rollup npm package](https://www.npmjs.com/package/rollup) and npm registry metadata — latest version and package positioning.
- [PkgPulse tsup vs tsdown guide](/guides/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026) — adjacent library-bundler context used to avoid duplicating the narrower tsup/tsdown/unbuild decision.
