Why we swapped our JavaScript bundler

Every extra kilobyte of JavaScript shipped to the browser adds parse and execution time, which directly affects how responsive a page feels. In an application as feature-rich as ours, those costs compound quickly. Over the past year, we traced a significant portion of our front-end performance problems back to an unexpected place: the module bundler itself.

Most modern codebases are split into small modules for maintainability. A bundler takes those modules and amalgamates them into files the browser can download. Our first bundler was written in 2014, before performance-oriented tools like Webpack and Rollup had matured. It worked, but it shipped far too much code and was painful for engineers to maintain. Replacing it became a priority, and the timing was right: we were mid-migration to Edison, our new web serving stack, which simplified integrating a modern bundler into our static asset pipeline.

What the old bundler got wrong

The legacy bundler was efficient at build time but produced bloated output. Engineers had to manually define which scripts to include in each package, and we shipped almost everything involved in rendering a page with few optimizations. Three structural problems made this untenable.

Problem #1: Multiple versions of bundled code
Our previous architecture, Dropbox Web Server (DWS), served each page as multiple pagelets, each with its own backend controller and JavaScript entry point. Teams working on different pagelets could end up on different code versions, forcing DWS to deliver separate versions of the same packaged code on one page. That created consistency issues — for instance, multiple instances of a singleton could be loaded simultaneously. Edison's pagelet-free architecture eliminated this constraint and let us adopt a conventional bundling scheme.

Problem #2: Manual code-splitting
Code-splitting splits a bundle into smaller chunks so the browser loads only what the current page needs. Without it, navigating from dropbox.com/home to dropbox.com/recents requires downloading the entire application bundle every time — either on first load because everything is in one file, or repeatedly because nothing is cached intelligently.

With code-splitting, only the required chunks are downloaded. Critical scripts load first; non-critical scripts load, parse, and execute asynchronously. Browser caching catches shared code pieces, reducing JavaScript downloaded during navigation.

Since our old bundler had no built-in code-splitting, engineers manually maintained a packaging map — a dictionary exceeding 6,000 lines that defined which modules went into which package. A strict set of "packager tests" enforced correct packaging, but they became a bottleneck. Any change often required hand-reshuffling modules across packages. And because the granularity was coarse, pages frequently loaded unnecessary modules. A page depending on modules a, b, and c might fetch two packages that also contained module d — code the page never executed.

Problem #3: No tree shaking
Tree shaking statically analyzes code structure and removes anything not directly referenced, producing leaner bundles. Our old bundler had no such feature. Packages routinely contained large swaths of unused code, especially from third-party libraries. The cost was measurable: adding observability metrics that relied on Protobuf definitions could inject several megabytes of dead code into the output.

Why Rollup won

Our core requirements were automatic code-splitting, tree shaking, and opt-in plugins for further optimization. We evaluated the available tools and settled on Rollup, primarily because it was the most mature option at the time and the easiest to integrate into our existing pipeline.

There was also a practical advantage: we already used Rollup — though minimally — to bundle NPM modules. Expanding that usage meant less engineering overhead than bringing in a wholly foreign tool. More importantly, our engineers already knew Rollup's quirks, which reduced the risk of unknown unknowns during a large migration. Rebuilding Rollup's feature set inside our legacy bundler would have cost far more engineering time than integrating the real tool more deeply.

Rolling out without breaking anything

Replacing a bundler at our scale means running two systems in parallel for a while. Both generate separate sets of bundles, and both must remain stable. On top of that, we had to convince page owners to opt in, while managing extra load on our build systems and CI.

We structured the rollout in four stages:

  • Developer preview: Engineers could opt-in to Rollup bundles in their local dev environment. This effectively crowdsourced QA — developers hit unexpected behavior early, giving us time to fix bugs before wider exposure.
  • Dropboxer preview: Rollup bundles were served to all internal employees. This provided early performance data and broad behavioral feedback.
  • General availability: At this stage, we began gradual external rollout to all users.
  • Maintenance: Finally, we planned dedicated time to clear tech debt and refine our use of Rollup for further gains.

For control, we combined cookie-based gating with our in-house feature-gating system. Historically, Dropbox rollouts rely exclusively on feature gates, but cookie-based switching let us toggle quickly between Rollup and legacy packages — invaluable during debugging. Nested within each stage, we ramped traffic in steps of 1%, 10%, 25%, 50%, and 100%. This let us collect performance and stability data early, minimizing impact if a breaking change slipped through.

A migration spanning thousands of pages needs more than a technical rollout — it needs momentum. We got it by making Rollup an Edison-only feature. Teams migrating to Edison would automatically get Rollup bundles, giving page owners a concrete reason to complete the migration beyond abstract performance benefits. Edison itself promised its own performance and developer-velocity wins, so coupling the two created a compounding effect across the company.

Where the rollout got hard

No migration of this scale goes smoothly, and the Dropbox team ran into several problems it didn't anticipate. The most significant hurdle was chaining one module bundler (Rollup) onto the existing Bazel-based build system. Running two bundlers simultaneously proved far more resource-intensive than estimated. Rollup's tree-shaking algorithm must load every module into memory and build the abstract syntax trees required to analyze dependencies before it can shake unused code. Meanwhile, the Bazel integration prevented caching of intermediary build artifacts, forcing CI to rebuild and re-minify every Rollup chunk on each run. The result was CI builds timing out from memory exhaustion, which pushed the rollout schedule back considerably.

The team also uncovered bugs in Rollup's tree-shaking algorithm that made it overly aggressive in some edge cases. Those bugs were caught during the developer preview and only produced minor issues — never user-facing. A more serious problem surfaced around strict mode. The existing bundler had been serving code from third-party libraries that wasn't strict-mode compatible; once the new bundler enabled strict mode, that same code caused hard runtime failures in the browser. Fixing it required a one-time audit of the entire codebase to find and patch non-compliant code.

During the internal Dropboxer preview, telemetry comparisons between Rollup and the legacy bundler showed less TTVC improvement than expected. The gap was traced to chunk count: Rollup produced many more chunks than the old packager. The team had initially assumed HTTP/2 multiplexing would make a larger number of chunks harmless, but the browser spent significantly more time discovering all the modules needed for a page. More chunks also meant worse compression, because algorithms like Zlib use a sliding-window approach that compresses one large file more efficiently than many smaller ones.

Measured outcomes

Once Rollup was live for all users, the results were clear. JavaScript bundle sizes dropped by 33%, total script count fell by 15%, and the team saw modest TTVC gains. Developer velocity improved as well: automatic code-splitting removed the need for engineers to manually shuffle bundle definitions with every change. Perhaps the most valuable long-term win was pulling the bundling pipeline out of 2014 — the project eliminated years of accumulated tech debt and reduced ongoing maintenance overhead.

The migration also exposed weaknesses in the existing architecture: render-blocking RPCs, excessive third-party function calls, and inefficiencies in how the browser loaded the module dependency graph. Rollup's mature plugin ecosystem has since made it easier to address those bottlenecks than it ever was before. Full adoption of Rollup delivered immediate performance and productivity wins, and it gives the frontend team a foundation for further optimization work down the line.