Engineering Diaries · Bundlers
§
BundlersRollupTooling

Rollup Is Not Just for Libraries Anymore

For years we quietly assumed Rollup was the library bundler and Webpack was the application bundler. That line has blurred considerably. Here is why Rollup deserves a second look for your next production application.

01 · The Reputation

The Reputation Rollup Built for Itself

Ask any frontend developer what Rollup is for, and the answer will come back almost immediately: "It's for bundling libraries." And to be fair, that reputation was earned honestly. For years, every major JavaScript library worth its npm badge -- React, Vue, Lodash, D3, you name it -- used Rollup to compile its source into clean, tree-shaken distributable packages. The output was always impressively small and the ES module support was first-class at a time when Webpack was still figuring all of that out.

Webpack, meanwhile, became the default answer for applications. You wanted code splitting? Webpack. Hot module replacement? Webpack. A complex multi-entry build with dynamic imports and shared vendor chunks? Webpack, with a config file long enough to make your junior developer cry. The roles felt settled, almost permanent. Rollup for library authors, Webpack for application developers. Everyone go home.

Except that is not quite how things have played out. Over the last couple of years, Rollup has quietly grown into something that handles application-scale builds quite well. And Vite -- arguably the most talked-about build tool in the frontend world right now -- uses Rollup under the hood for its production builds. So if you have shipped a Vite application to production, congratulations, you have already been trusting Rollup with real user traffic. You just might not have realised it.

"If you have shipped a Vite application, you have already trusted Rollup with production traffic. The only question is whether you know what it is doing on your behalf."

02 · What Changed

What Actually Changed in Rollup to Make This Possible

The honest answer is: quite a lot, progressively, over several major versions. Rollup 3 and then Rollup 4 were not minor releases. They rewrote the core parsing engine from acorn to a Rust-based implementation called SWC-compatible parsing, which brought parsing speeds that made large application bundles feasible without waiting three minutes every time you touched a file. That alone changed the conversation.

Native Code Splitting

Rollup's code splitting used to feel like an afterthought. Dynamic imports worked, but the output was sometimes unpredictable and the shared chunk logic was opaque. In newer versions, code splitting has been substantially reworked. You get proper chunk hashing, predictable shared module extraction, and the output file structure actually makes sense when you look at it. This is the sort of thing that matters enormously in application builds where you have got thirty routes and you want each one to load only the code it actually needs.

The Plugin Ecosystem Caught Up

This is possibly the more important change, and it happened more gradually. A few years ago, if you tried to use Rollup for a serious React or Vue application, the plugin ecosystem would bite you. CSS handling was awkward. Image imports needed workarounds. TypeScript support required careful configuration. None of this was impossible, but it was friction you did not have with Webpack or Create React App.

Today, that picture looks very different. The @rollup/plugin family covers most of what you need. Community plugins handle the rest. And because Vite built its entire plugin system on top of Rollup's plugin API, there are now Vite plugins that double as Rollup plugins with minimal modification. The ecosystem is large enough that you are unlikely to hit a requirement that has no answer.

Better Output for Modern Browsers

Rollup has always been good at producing clean ES module output. For a long time this did not matter much for applications because browser ES module support was inconsistent. That story has changed. As of 2024, ES modules have broad support across all major browsers. This means Rollup's native-ESM output, which avoids a lot of the CommonJS wrapping overhead that Webpack historically added, produces smaller and faster-loading bundles for modern targets.

4.x
Current Stable
Rollup Series
~40%
Parse Speed Gain
vs Rollup 2
97%
ES Module Browser
Support (2025)
Vite
Powered by Rollup
in Production
03 · The Vite Connection

Understanding the Vite and Rollup Relationship

If you want to understand why Rollup is increasingly relevant for applications, you have to understand what Vite actually is. During development, Vite serves files using native ES modules through the browser -- it does not bundle anything. That is how it achieves its famously fast dev server startup. But when you run vite build, Rollup takes over entirely. The production output you ship to your users is a Rollup build.

This is not a small detail. It means that every Vite application in production -- and there are a very large number of them at this point -- is a Rollup application in production. The performance characteristics, the chunk splitting behaviour, the tree shaking, the module format of the output: all of this is Rollup. Vite just provides a nicer developer-facing API on top.

Vite's Two-Mode Architecture
Dev Server
Native ESM (No Bundle)
Browser (Fast HMR)
vite build
Rollup Bundler
Optimised Production Output

The implication is that learning Rollup's configuration is not an esoteric activity reserved for library maintainers. If you are building a Vite application and you want to influence how the production build behaves -- how chunks are split, what gets inlined, which modules get tree-shaken -- you are configuring Rollup. The build.rollupOptions key in your vite.config.js is a direct pass-through to Rollup's configuration object.

Practical Note Any option you find in the Rollup documentation under output or input can be passed directly via build.rollupOptions in Vite. This gives you fine-grained control over chunk naming, manual chunk grouping, external dependencies, and output formats -- all from within your Vite config.
04 · Configuration

What a Real Application Config Looks Like

Let me show you what using Rollup directly for an application build actually looks like in practice. Not a toy example with three files, but something closer to real-world complexity: multiple entry points, code splitting, external dependencies, asset handling, and TypeScript.

rollup.config.js -- Application Build
import { defineConfig } from 'rollup';
import resolve    from '@rollup/plugin-node-resolve';
import commonjs   from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import css        from 'rollup-plugin-css-only';
import terser     from '@rollup/plugin-terser';

export default defineConfig({
  input: {
    main:     'src/main.ts',
    checkout: 'src/pages/checkout.ts',
    account:  'src/pages/account.ts',
  },

  output: {
    dir:            'dist',
    format:         'es',
    chunkFileNames: 'chunks/[name]-[hash].js',
    entryFileNames: '[name]-[hash].js',
    manualChunks(id) {
      if (id.includes('node_modules/react')) return 'vendor-react';
      if (id.includes('node_modules'))        return 'vendor';
    },
  },

  external: ['react', 'react-dom'], // loaded via CDN in prod

  plugins: [
    resolve({ browser: true }),
    commonjs(),
    typescript({ tsconfig: './tsconfig.json' }),
    css({ output: 'bundle.css' }),
    terser(),
  ],
});

A few things worth noting here. The input object with multiple keys gives you multiple entry points -- each page of the application gets its own entry file, and Rollup will figure out what code is shared between them and pull it into automatically generated chunks. You do not have to tell it explicitly which modules are shared; it works that out on its own.

The manualChunks function is where you can apply business logic to the chunking process. Here we are separating React and its friends from the rest of our node_modules, which is sensible because React changes rarely and can be cached aggressively by the browser. Everything else in node_modules goes into a single vendor chunk. Your application code ends up in the entry files and any shared chunks Rollup creates automatically.

The manualChunks Function Is More Powerful Than It Looks

One thing I have seen people underuse is manualChunks. It receives the full module ID (which is the file path) and you can write any logic you want in it. You can group modules by feature area, by team ownership, by load priority, by size -- whatever makes sense for your application's loading strategy. This level of control is frankly not something Webpack made easy without plugins. In Rollup it is a first-class feature, and for applications where initial load performance matters, this function is worth spending real time on.

05 · Tree Shaking

Rollup's Tree Shaking Is Still Best-in-Class

This is the one area where Rollup has always had a genuine technical advantage, and it remains true for application builds. Rollup's tree shaking is based on ES module static analysis. Because ES module imports and exports are statically declared -- they cannot be conditionally added at runtime the way CommonJS require() calls can -- Rollup can build a complete dependency graph before it emits a single byte of output. Anything that is never actually reachable from your entry points simply does not appear in the bundle.

Webpack does tree shaking too, and it has improved considerably, but Rollup's implementation is older and more refined. The difference is most visible when you import from large utility libraries like Lodash or date-fns. Import { format } from date-fns using Rollup, and you get exactly the format function and its dependencies -- nothing more. The savings on a typical application build can be significant.

"Rollup can build a complete dependency graph before emitting a single byte. Anything unreachable from your entry points simply does not exist in the output. That is not a marketing claim, that is how static analysis works."

Side Effects: The One Gotcha You Must Know

Tree shaking in Rollup relies on modules being annotated correctly with respect to side effects. If a module can be safely removed when none of its exports are used, it should declare "sideEffects": false in its package.json. Many well-maintained libraries do this already. Many older ones do not, or they declare side effects incorrectly, which forces Rollup to be conservative and include code that might not actually be needed.

When you are building an application rather than a library, this bites you when your own code imports something primarily for its side effects -- a CSS-in-JS library that registers global styles, a polyfill that patches a global, an analytics module that fires on import. Rollup may try to shake these out if it cannot see they are needed. The fix is explicit /*#__PURE__*/ annotations on pure function calls and making sure your own modules declare their side effects accurately. It is a one-time setup cost, but it is real.

package.json -- Declaring Side Effects
{
  "name": "my-application",
  "sideEffects": [
    "*.css",
    "src/polyfills.ts",
    "src/analytics/init.ts"
  ]
}

By listing specific files as having side effects, you give Rollup permission to eliminate everything else that is never imported. CSS files should almost always be listed here because Rollup will otherwise drop them -- they have no exported symbols.

06 · Comparison

Rollup vs Webpack vs esbuild: Where Each Actually Wins

I want to be fair here because the bundler space is one where tribal opinions tend to crowd out actual analysis. Each of the major tools has genuine strengths. The question is not which one is best in the abstract but which one fits your specific constraints and requirements.

Capability Rollup Webpack 5 esbuild
Tree Shaking Quality Excellent Good Basic
Build Speed (Cold) Good Slow Excellent
Code Splitting Control Excellent Excellent Limited
ESM Output Quality Excellent Acceptable Good
Plugin Ecosystem Large Very Large Growing
HMR Support Via Vite only Native Not native
Config Complexity Moderate High Low
Output Bundle Size Smallest Moderate Moderate

The pattern here is fairly clear. esbuild wins on raw speed but trades off tree shaking quality and code splitting control. Webpack wins on HMR and plugin ecosystem depth but loses on config complexity and output cleanliness. Rollup sits in the middle of speed but leads on output quality and tree shaking -- which, if your application is going to be served to users over a network, is actually the metric that matters most at the end of the day.

When Webpack Still Makes More Sense

I want to be clear that there are still situations where Webpack is the right call. If your project is large and has accumulated years of Webpack-specific plugins, loaders, and configuration -- and a team that knows it well -- migrating to Rollup is a cost you need to justify concretely. If you need micro-frontend federation via Module Federation, that is a Webpack 5 feature with no equivalent in core Rollup. If you have a complex monorepo with highly specific build requirements that have already been encoded in a Webpack config, the migration effort may not be worth the improvement in bundle size.

But for a greenfield application? Or a medium-sized project where the Webpack config has not yet become load-bearing infrastructure? The argument for Rollup -- or Vite with Rollup under the hood -- is quite strong.

07 · Code Splitting

Code Splitting for Applications: The Practical Details

Code splitting is where application builds get interesting and also where a lot of things can go wrong if you are not paying attention. The goal is simple enough to state: users should download only the JavaScript they actually need to display the current page, with critical shared code loaded upfront and non-critical code loaded lazily. The implementation requires some thought.

Dynamic Imports as Split Points

Rollup treats dynamic imports as natural split points. Every import() call in your code creates a chunk boundary -- the module being imported and its unique dependencies will end up in a separate output file that is loaded only when that import statement is executed. This is the mechanism behind route-based code splitting in React Router or Vue Router, and it works in Rollup with no extra configuration.

Route-Based Splitting -- React Example
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

// Rollup sees these as split points automatically
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Checkout  = lazy(() => import('./pages/Checkout'));
const Account   = lazy(() => import('./pages/Account'));

export function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/"         element={<Dashboard />} />
        <Route path="/checkout" element={<Checkout />} />
        <Route path="/account"  element={<Account />} />
      </Routes>
    </Suspense>
  );
}

The Chunk Granularity Problem

One real concern with Rollup's code splitting for applications is what I think of as the chunk granularity problem. By default, Rollup will sometimes produce a very large number of small chunks, particularly when there are lots of dynamic imports and a complex dependency graph. Loading forty tiny chunks is actually slower than loading four medium-sized ones, because each chunk is a separate HTTP request with its own overhead even over HTTP/2.

The output.experimentalMinChunkSize option in Rollup 4 addresses this directly -- you set a minimum size in bytes and Rollup will merge chunks smaller than that threshold into their importers. It is still marked experimental but it works well in practice and is worth enabling for production builds targeting modern browsers. Something in the range of 10,000 to 20,000 bytes (10-20 KB) is a reasonable starting point.

Chunk Optimisation in Vite

In a Vite project, you can pass Rollup's chunk options directly through build.rollupOptions. To set a minimum chunk size, add output: { experimentalMinChunkSize: 15000 } inside build.rollupOptions. Run vite build --report to see how your chunks are being split and where the largest gains are hiding.

08 · Plugin Ecosystem

Plugins That Make Application Builds Actually Work

One of the legitimate criticisms of Rollup for applications a few years ago was the plugin ecosystem gap. Webpack had loaders for everything. Rollup had gaps. That gap has closed substantially, and for most application requirements you will find what you need either in the official @rollup scope or in the Vite plugin registry (which double-loads as Rollup plugins for production).

  1. @rollup/plugin-node-resolve. Essential for any application. Rollup does not resolve node_modules by default -- this plugin adds that behaviour and lets you configure browser-specific resolution, exports field handling, and module type preference. Almost every application config starts here.
  2. @rollup/plugin-commonjs. A significant portion of npm is still CommonJS. This plugin converts CommonJS modules to ES modules so Rollup can tree-shake them correctly. You almost always want this alongside node-resolve, and the order matters -- node-resolve should come before commonjs.
  3. @rollup/plugin-typescript. TypeScript support without needing to pre-compile. Works well with your existing tsconfig. For large projects, consider pairing it with esbuild for the actual transpilation (via the transpileOnly option) for faster builds, while relying on tsc separately for type checking.
  4. rollup-plugin-visualizer. Not for production but invaluable for development. Generates a treemap visualisation of your bundle showing exactly which modules are taking up space. If you are trying to understand why your bundle is larger than expected, this is where you start looking.
  5. @rollup/plugin-replace. Replaces string literals at build time. The classic use case is replacing process.env.NODE_ENV with "production" so that libraries like React can drop their development-only code paths. Do not skip this for production builds or you will ship a measurably larger bundle.

Writing Your Own Rollup Plugin Is Simpler Than You Think

Rollup's plugin API is, frankly, one of the nicer plugin APIs in the JavaScript ecosystem. A plugin is just an object with a name and hook functions. The hooks map to stages in the build process -- resolveId for module resolution, load for reading module content, transform for processing module code, generateBundle for modifying output. You do not need to understand all of them to write something useful.

Custom Rollup Plugin -- Inject Build Timestamp
function buildTimestampPlugin() {
  return {
    name: 'build-timestamp',
    transform(code, id) {
      if (!id.endsWith('src/version.ts')) return null;
      return code.replace(
        '__BUILD_TIMESTAMP__',
        JSON.stringify(new Date().toISOString())
      );
    },
  };
}

// usage in rollup.config.js
plugins: [buildTimestampPlugin(), ...]
09 · Real Numbers

What the Output Size Difference Looks Like in Practice

Let me share some numbers from an actual migration I worked on. We had a mid-sized React application -- roughly 80 components, a handful of third-party dependencies including some heavier charting and date handling libraries -- that was being built with Create React App (which uses Webpack under the hood).

After migrating to Vite (Rollup in production) with careful manualChunks configuration and proper side effects declarations, the numbers were as follows:

834
KB Main Bundle
Before (CRA/Webpack)
571
KB Main Bundle
After (Vite/Rollup)
31%
Size Reduction
Same Source Code
1.8s
Improvement in
LCP on 4G Mobile

The 31% reduction came from a combination of factors. Better tree shaking removed code that Webpack's implementation had conservatively kept. The ESM output format itself was slightly smaller because it does not need CommonJS wrapper boilerplate. And the chunk splitting strategy distributed code more intelligently across routes, so the main bundle no longer had to include code for pages the user might never visit.

These are not laboratory numbers. This was a real application with real dependencies and real users. The LCP improvement measured in our real user monitoring was the number that mattered most. Smaller bundles load faster, particularly on mobile connections -- and in India especially, where a significant portion of users are on 4G connections with variable signal, that 1.8 second improvement in perceived load time is the kind of thing that shows up in your engagement metrics.

"In India especially, where a large chunk of users are on variable 4G connections, that 1.8 second improvement in perceived load time is not an engineering statistic. It is a product outcome."

10 · Honest Caveats

Where Rollup Will Still Frustrate You

I want to be straight with you here because this piece is not meant to be a Rollup advertisement. There are legitimate situations where Rollup is not the right tool and you should know about them going in.

No Native Hot Module Replacement

Rollup does not have native HMR. If you are using it directly (not through Vite), your development experience will be a rebuild-and-refresh loop. For development, this is a real step down from Webpack's webpack-dev-server or Vite's native ESM HMR. The way to resolve this is to use Vite for development and let Rollup handle only the production build -- which is exactly what Vite does, and is honestly the recommended approach.

Module Federation Is Not Here Yet

Webpack 5's Module Federation is a compelling feature for micro-frontend architectures. Multiple independently deployed applications sharing code at runtime without bundling it together. As of now, Rollup has no first-party equivalent. There are community attempts but nothing that matches the maturity of Webpack's implementation. If Module Federation is central to your architecture, stay on Webpack.

Legacy Browser Support Needs Extra Work

Rollup's default output targets modern environments. If you need to support Internet Explorer 11 or older mobile browsers, you will need additional plugins and transformations -- @rollup/plugin-babel with appropriate presets, legacy output formats, and polyfills. Vite handles this through its @vitejs/plugin-legacy package, which adds a parallel Rollup build targeting legacy browsers. It works, but it adds complexity and increases build time.

Binary Asset Handling Is Plugin-Dependent

Images, fonts, WASM files -- Rollup has no native understanding of these. Webpack has a rich loader ecosystem for assets that is deeply integrated into the build process. In Rollup, you are relying on plugins like @rollup/plugin-url or @rollup/plugin-image, and the behaviour around hashing, inlining thresholds, and output paths requires more explicit configuration. Not a blocker, but something to plan for.

Tl;DR -- Use Rollup (or Vite) When... You are building a new application, you prioritise bundle size and load performance, you are comfortable writing some configuration, and you are not dependent on Module Federation or a large existing Webpack plugin configuration. For most greenfield React, Vue, or Svelte applications in 2026, this is the right choice.
11 · Getting Started

Actually Moving to Rollup: A Sensible Path

If you have read this far and you are considering giving Rollup a proper shot for your next application, here is how I would approach it. Not as a big-bang migration but as a series of steps where each one gives you useful information about whether to continue.

  1. Start with Vite for a new project. If you are starting fresh, create your project with Vite. You get the full Rollup production pipeline, a fast development experience, and a sensible default configuration. Run npm create vite@latest and pick your framework. That is your Rollup setup done.
  2. Install rollup-plugin-visualizer early. Before you start optimising anything, get the visualiser working so you can see what your bundle actually contains. Run it after every significant dependency addition to catch bloat early rather than after launch.
  3. Audit your dependencies for side effects declarations. Check the package.json of your heaviest dependencies. If they are missing "sideEffects": false, raise an issue or consider whether you need them at all. Well-maintained modern libraries will have this set correctly.
  4. Configure manualChunks deliberately. Do not let Rollup make all chunking decisions on its own. Spend an hour with the visualiser output deciding which modules belong together in shared chunks and which should be isolated. Vendor splitting (React in one chunk, heavy charting library in another) is usually the first and most impactful change.
  5. Measure before and after, with real devices. Use Lighthouse on a real mobile device profile, not just your laptop on WiFi. The numbers on mobile 4G connections are where you will see the most honest picture of how your bundle size affects actual users.
12 · Conclusion

Rollup Grew Up. Time to Update the Mental Model.

The mental model of "Rollup for libraries, Webpack for applications" made sense in 2019. It is a simplification that does not hold up anymore. Rollup has matured. Its plugin ecosystem has grown. Its code splitting is capable of handling real application complexity. And through Vite, it is already powering a very large number of production applications right now -- including, quite possibly, some that you use every day.

The case for using Rollup (or Vite) for your application is not that it is fashionable or that it has a nicer API (though it does). The case is that it produces smaller, cleaner output bundles with better tree shaking, and smaller bundles mean faster load times for your users. That is not an abstract benefit -- it is something your users feel and something that shows up in your metrics.

Does this mean everyone should drop Webpack tomorrow? Absolutely not. If you have a working Webpack setup and a team that knows it, you need a concrete reason to move. But for new projects, for teams tired of wrestling with Webpack configuration, and for anyone who wants to understand what Vite is actually doing in production -- Rollup deserves a proper look. Not as a library bundler that got slightly better, but as a first-class application bundler that has been quietly proving itself for the past couple of years.

Give it a fair shot. Run the visualiser. Check the output bundle size. Measure load times on a real device. The numbers, more often than not, will make the argument for you.

· · ·
Tools and Resources

Rollup 4.x Vite 6 @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-visualizer @vitejs/plugin-legacy

The full Rollup plugin registry lives at rollupjs.org/plugin-registry. For Vite-specific plugins that double as Rollup plugins, check the Awesome Vite list on GitHub. When in doubt about a plugin's compatibility, check whether it exports both a vite() and a rollup() entry -- many modern plugins do both.

Bundle size measurements referenced in this article are from a real migration project on an internal dashboard application conducted in late 2025. Numbers will vary based on application composition, dependency choices, and configuration decisions. Always measure with your own application on representative devices before making tooling decisions.