Saurabh Chase Saurabh Chase
  • Home
  • Explore
    • Business
    • Technology
    • Personal Care
    • Troubleshooting Tips
  • Deep Dives
  • Login
  • Create an account
  • Contact Us

Contact Us

  • Seattle WA
  •  
  •  
  •  
  •  
  •  
  •  

Hello, World!

  • Login
  • Create an account
Saurabh Chase Saurabh Chase
  • Home
  • Explore
    • Business
    • Technology
    • Personal Care
    • Troubleshooting Tips
  • Deep Dives
  • Login
  • Create an account
  • Contact Us
Contact Us
  1. You are here:  
  2. Home
  3. Explore
  4. Troubleshooting Tips
  5. Build & Bundling
  6. Advanced Troubleshooting: Rollup at Enterprise Scale
Details
Category: Build & Bundling
Mindful Chase By Mindful Chase
Mindful Chase
15.Aug
Hits: 5

Advanced Troubleshooting: Rollup at Enterprise Scale

Rollup is a powerful JavaScript module bundler prized for its tree-shaking and standards-first approach to ESM. In small projects it feels effortless, but at enterprise scale—monorepos, hybrid ESM/CJS packages, multiple targets (web, Node, edge), and complex plugin stacks—teams encounter subtle, high-impact failures. These include broken tree-shaking due to side effects, mis-bundled dynamic imports, cross-environment resolution errors, code-splitting regressions, source map drift, and memory spikes in watch mode. This article is a deep troubleshooting guide for senior engineers who need to diagnose root causes, understand architectural implications, and implement long-term fixes that keep large Rollup builds fast, correct, and maintainable.


Background and Context

Rollup centers on ECMAScript Modules (ESM) and leverages static analysis for dead-code elimination. Its plugin ecosystem handles TypeScript, CommonJS interop, Babel transforms, CSS extraction, asset inlining, and minification. In large systems, problems often arise from mismatched module formats, non-static patterns that defeat analysis, or composition of many plugins that alter module graphs in unexpected ways.

Typical enterprise scenarios include building multiple outputs (ESM for modern bundlers, CJS for legacy Node, IIFE/UMD for browsers), splitting vendor and app code, bundling web workers and WASM, and producing server-side bundles for SSR. Each increases graph complexity and introduces new failure modes.

Architecture Overview and Implications

Module Graph and Static Analysis

Rollup constructs a module graph from import statements. Static analysis enables tree-shaking, but only when code paths are statically discoverable. require() with dynamic expressions or side-effectful getters can block elimination or alter execution order. Understanding this graph is central to debugging.

ESM/CJS Interop Boundaries

Many enterprise packages still publish CommonJS. The @rollup/plugin-commonjs and @rollup/plugin-node-resolve bridge the gap, but interop costs and heuristics (e.g., detecting module.exports patterns) can impact performance and correctness. Incorrect default vs named import semantics frequently cause runtime undefineds.

Code Splitting and Chunks

For large apps, Rollup emits multiple chunks with shared dependencies in separate files. Chunking depends on the graph and manualChunks strategy. Poor chunk boundaries lead to duplication, waterfall network loads, or broken lazy routes. The preserveEntrySignatures and output.inlineDynamicImports options affect how entries link together.

Plugins as a Transformation Pipeline

Plugins may transform source, resolve modules, or inject virtual modules. Plugin order and hook phases (resolveId, load, transform, generateBundle) profoundly affect outcomes. Misordered plugins can create subtle bugs: e.g., TypeScript emitting CJS before Rollup sees ESM, or Babel re-transpiling helpers that break tree-shaking.

Symptoms, Root Causes, and First-Response Triage

Symptom 1: Bundle size unexpectedly large

Root causes: side-effectful modules marked as tree-shakeable, missing sideEffects metadata, commonjs plugin wrapping entire modules, or accidental re-introduction of helpers/transpiled constructs that block DCE.

First response: enable treeshake diagnostics and inspect bundle output; verify package.json sideEffects field and export map.

Symptom 2: Runtime undefined imports or default is not a function

Root causes: CJS default export interop mismatches, improper esModuleInterop/allowSyntheticDefaultImports assumptions, or mixing synthetic defaults and named imports across layers.

First response: check the resolved module format in build logs; add requireReturnsDefault options to commonjs plugin or refactor import style.

Symptom 3: Code splitting produces duplicate libraries

Root causes: different import paths to the same library (react vs ./node_modules/react/index.js), multiple package versions hoisted in a monorepo, or manualChunks splitting that prevents sharing.

First response: normalize import paths, use dedupe in node-resolve, and audit the lockfile for duplicate versions.

Symptom 4: Source maps do not align with TS/Babel sources

Root causes: multiple transforms without sourceMap: true propagation, wrong sourcesContent, or minifier stripping mappings. Chain breaks at any plugin produce drift.

First response: ensure every plugin passes through maps and verify output.sourcemap is enabled consistently across outputs.

Symptom 5: Watch mode memory spikes / incremental builds slow down over time

Root causes: plugin memory leaks (caches not cleared), ever-growing virtual modules, or TypeScript/Babel caches keyed too broadly across a monorepo.

First response: disable caches to reproduce, then reintroduce scoped caches; profile memory via Node flags.

Diagnostics and Identification

Enable verbose graph and treeshake insights

rollup -c --logLevel debug --treeshake.annotations --treeshake.correctVarValueBeforeDeclaration
# Or programmatic API with onLog to capture details

Use --logLevel debug to reveal resolution decisions and module formats. The treeshake options help catch side-effect assumptions that block dead code elimination.

Inspect the chunk graph

Emit a bundle analysis to understand splits and duplicates:

import visualizer from \"rollup-plugin-visualizer\";
export default {
  // ...
  plugins: [visualizer({ filename: \"stats.html\", gzipSize: true, brotliSize: true })]
};
# After build, open stats.html and search for duplicated libs

Trace module format decisions

Log which modules are treated as CJS vs ESM, and which imports are synthetic defaults:

import commonjs from \"@rollup/plugin-commonjs\";
export default {
  // ...
  plugins: [
    commonjs({
      requireReturnsDefault: \"preferred\",
      onwarn(warn, def) {
        if (warn.code === \"THIS_IS_UNDEFINED\") return;
        def(warn);
      }
    })
  ]
};

Verify source map integrity end-to-end

Ensure each transform propagates maps:

typescript({ sourceMap: true })
babel({ babelHelpers: \"bundled\", sourceMaps: true })
terser({ format: { comments: false } })
# And in output:
output: { sourcemap: true }

Profile plugin performance and memory

Use Node inspector to sample CPU and heap usage:

node --inspect-brk node_modules/.bin/rollup -c
# In Chrome DevTools, capture heap snapshots during watch rebuilds
# Compare allocations attributed to plugin transforms

Common Pitfalls and Anti-Patterns

  • Using babelHelpers: \"inline\" across many files, duplicating helpers and defeating DCE.
  • Leaving preserveModules on for browser bundles, causing excessive requests and poor caching.
  • Importing CJS with named imports directly without the commonjs interop shim.
  • Dynamic require() that Rollup cannot analyze, pulling entire libraries into chunks.
  • Mismatched exports map in package.json causing Node resolution to differ from bundler resolution.
  • Multiple versions of the same library in monorepos due to lax constraints or mixed semver ranges.
  • Incorrect context (this binding) in IIFE/UMD builds resulting in runtime failures.
  • Minifier plugin placed before code-splitting finalization, producing unstable chunk hashes.

Step-by-Step Fixes

1) Stabilize ESM/CJS Interop

Normalize interop by forcing predictable behavior from commonjs and node-resolve, then refactor imports to match.

import resolve from \"@rollup/plugin-node-resolve\";
import commonjs from \"@rollup/plugin-commonjs\";
export default {
  input: \"src/index.ts\",
  plugins: [
    resolve({ preferBuiltins: true, exportConditions: [\"node\", \"default\"] }),
    commonjs({ requireReturnsDefault: \"preferred\", defaultIsModuleExports: false })
  ],
  output: [{ format: \"esm\", dir: \"dist/esm\" }, { format: \"cjs\", dir: \"dist/cjs\", interop: \"compat\" }]
};

Where a CJS package lacks a default export, use namespace import or createRequire in Node targets rather than relying on heuristics.

2) Recover Tree-Shaking

Ensure side-effect metadata and pure annotations are present.

# package.json
{
  \"sideEffects\": [
    \"*.css\",
    \"src/polyfills/**/*.ts\"
  ]
}

// Mark pure factory calls
/* @__PURE__ */ makeWidget();

// rollup.config.js
export default {
  treeshake: {
    moduleSideEffects: \"no-external\",
    propertyReadSideEffects: false,
    tryCatchDeoptimization: false
  }
};

Audit third-party packages; if a dependency is known to be side-effect free, whitelist it via moduleSideEffects or sideEffects metadata in a local wrapper package.

3) Fix Duplicate Libraries in Chunks

Deduplicate via node-resolve and manualChunks to centralize common deps.

resolve({ dedupe: [\"react\", \"react-dom\"] })
output: {
  manualChunks(id) {
    if (id.includes(\"node_modules\")) {
      if (id.includes(\"react\")) return \"vendor-react\";
      return \"vendor\";
    }
  }
}
# In a monorepo, enforce a single version via pnpm/yarn resolutions

4) Repair Source Map Chains

Require source maps at every stage and test with --sourcemap in all outputs; ensure minifier preserves mappings.

babel({ sourceMaps: true, babelHelpers: \"runtime\" })
terser({ mangle: true, compress: { passes: 2 }, format: { comments: false } })
output: [{ file: \"dist/index.js\", format: \"esm\", sourcemap: true }]

Validate in the browser devtools; verify that original TS files appear in the Sources panel with accurate line mapping.

5) Tame Watch Mode Memory

Use persistent caches with bounded size and clear on invalidations.

import { createFilter } from \"@rollup/pluginutils\";
const cache = new Map();
export default function memoTransform() {
  const filter = createFilter([\"**/*.ts\"], [\"**/*.test.ts\"]);
  return {
    name: \"memo-transform\",
    transform(code, id) {
      if (!filter(id)) return null;
      let hit = cache.get(id);
      if (hit && hit.src === code) return hit.result;
      const result = expensiveTransform(code);
      cache.set(id, { src: code, result });
      if (cache.size > 2000) cache.clear();
      return result;
    }
  }
}
# Also run Node with --max-old-space-size and monitor snapshots

6) Deterministic Multi-Target Builds

Share a single input graph and emit multiple formats without duplicating transforms.

const base = { input: \"src/index.ts\", plugins: [ts(), resolve(), commonjs()] };
export default [
  { ...base, output: { dir: \"dist/esm\", format: \"esm\", sourcemap: true } },
  { ...base, output: { dir: \"dist/cjs\", format: \"cjs\", exports: \"named\", sourcemap: true } }
];

When library consumers are bundlers, prefer ESM with preserveModules to improve secondary entry-tree shaking.

7) Resolve Edge & Node Conditions Correctly

Many packages export condition-specific builds via exports conditions. Configure node-resolve accordingly.

resolve({ exportConditions: [\"browser\", \"module\", \"default\"] })
# For server builds: exportConditions: [\"node\", \"default\"]

Misconfigured conditions lead to mixing browser and Node code, causing process or Buffer not defined errors at runtime.

8) Dynamic Imports and Lazy Routes

Prefer static strings and avoid template expressions in dynamic imports so Rollup can pre-split chunks.

// Good
const view = () => import(\"./views/Settings.js\");
// Risky
const view = (name) => import(\"./views/\" + name + \".js\");
# If you must, enumerate patterns via plugin or manualChunks

9) Web Workers and WASM

Bundle workers using plugins that emit separate assets and correct URLs.

import worker from \"rollup-plugin-web-worker-loader\";
export default { plugins: [worker({ targetPlatform: \"browser\" })] };
// For WASM: use copy or wasm loader plugin and set output.assetFileNames

10) CSS and Assets

Centralize CSS extraction to avoid FOUC and duplicated styles across chunks.

import postcss from \"rollup-plugin-postcss\";
postcss({ extract: true, minimize: true, modules: { scopeBehaviour: \"local\" } })
output: { assetFileNames: \"assets/[name]-[hash][extname]\" }

11) Minification Strategy

Minify once at the end, after chunking, to maintain stable hashes across builds.

import { terser } from \"rollup-plugin-terser\";
export default {
  // ...
  output: [{ dir: \"dist\", format: \"esm\", sourcemap: true }],
  plugins: [terser({ compress: { passes: 2 }, mangle: { toplevel: true } })]
};

For speed, consider rollup-plugin-esbuild or rollup-plugin-swc3 for transpilation, keeping terser for final minification to preserve semantics.

12) Package Externals and Peer Dependencies

Library bundles should externalize peers to reduce size and avoid duplicate copies.

import pkg from \"./package.json\" assert { type: \"json\" };
const external = [
  ...Object.keys(pkg.peerDependencies || {}),
  ...Object.keys(pkg.dependencies || {}).filter(x => /^(react|react-dom)$/.test(x))
];
export default { external };

Verify that externals are resolved at consumer build time; document required peer versions in README and peerDependencies.

13) Monorepo: Shared Config and Hoisted Dependencies

Create a base config consumed by packages and lock versions of core plugins. Prevent accidental cross-package resolution by using preserveSymlinks: false and workspace tools (pnpm/yarn) to keep a single version of critical libs.

// rollup.base.js
export default {
  plugins: [resolve({ preferBuiltins: true }), commonjs(), ts()],
  onwarn(w, warn) { if (w.code !== \"THIS_IS_UNDEFINED\") warn(w); }
};
// package rollup.config.js
import base from \"../../build/rollup.base.js\";
export default { ...base, input: \"src/index.ts\", output: { dir: \"dist\", format: \"esm\" } };

14) Reproducible Builds and Hash Stability

Avoid non-determinism: pin plugin versions, set chunkFileNames templates, and disable timestamp injections in banners.

output: {
  entryFileNames: \"[name]-[hash].js\",
  chunkFileNames: \"[name]-[hash].js\",
  assetFileNames: \"assets/[name]-[hash][extname]\"
}

15) SSR/Edge vs Browser Targets

Split configs by environment to avoid leaking browser shims into server bundles.

// ssr.config.mjs
export default {
  input: \"src/entry-server.ts\",
  external: [/^node:.+/, \"react\"],
  output: { format: \"esm\", dir: \"dist-ssr\" },
  plugins: [resolve({ exportConditions: [\"node\"] }), commonjs()]
};
// client.config.mjs uses browser export conditions

Diagnostics: Deep Dives

Track Resolution Paths

Hook resolveId to log path decisions and detect duplicates or weird aliases.

const traceResolve = {
  name: \"trace-resolve\",
  resolveId(source, importer) {
    console.log(\"RESOLVE\", source, \"from\", importer);
    return null; // defer to other resolvers
  }
};
export default { plugins: [traceResolve, resolve(), commonjs()] };

Validate Export Maps

Misleading exports maps produce different entry points for Node vs bundlers. Use a script to verify that module and exports align.

node -e \"const p=require(\u0027./package.json\u0027);console.log(p.module,p.exports)\"

Find Dead Code That Won't Shake

Force a build with treeshake.pureExternalModules and audit resulting size change; large differences indicate side-effectful externals.

treeshake: { pureExternalModules: [\"lodash-es\", \"date-fns\"] }

Performance Playbook

Speed Up Transforms

Prefer a single transpiler (esbuild or swc) for TS/JS, and limit Babel to legacy syntax or React transforms if necessary.

import esbuild from \"rollup-plugin-esbuild\";
esbuild({ target: \"es2020\", tsconfig: \"tsconfig.json\" })

Parallelize and Cache

Use plugin-level caches and set cache in the programmatic API for incremental builds. Keep transforms idempotent with stable cache keys (file path + content hash).

import { rollup } from \"rollup\";
let prevCache;
async function build(){
  const bundle = await rollup({ input: \"src/index.ts\", cache: prevCache, plugins: [ts()] });
  await bundle.write({ dir: \"dist\", format: \"esm\" });
  prevCache = bundle.cache;
}
build();

Chunk Strategy for the Network

Balance initial load and long-term caching with a focused manualChunks strategy and inlineDynamicImports control.

output: {
  manualChunks: {
    \"react-vendor\": [\"react\", \"react-dom\"],
    \"charting\": [\"chart.js\"]
  },
  inlineDynamicImports: false
}

Tree-Shake Friendly Source

Export pure functions and avoid side effects at module scope. Do not mutate exports after declaration; avoid re-exporting from modules that perform side effects on import.

Governance and Long-Term Stability

Version Pinning and Release Trains

Pin Rollup and all plugins; upgrade on a schedule with automated bundle diff checks (size and runtime smoke tests). Maintain a breaking changes checklist for plugin upgrades (e.g., commonjs option changes, terser defaults).

Build Validation Gates

Automate checks: maximum bundle size budgets, duplicated dependency detector, sourcemap validation, and SSR smoke tests. Fail CI when regressions occur.

node scripts/check-size.js
node scripts/check-duplicates.js
node scripts/verify-sourcemaps.js

Documentation and Shared Recipes

Codify patterns: interop rules, manualChunks guidelines, export maps, and environment-specific configs. Surface them as templates for teams to avoid bespoke configurations.

Case Studies: Quick Wins

Case 1: 35% size reduction by fixing interop

A dashboard app imported a CJS-only charting library via named imports. Switching to default import with requireReturnsDefault: \"preferred\", plus marking vendor chunk explicitly, restored DCE and shrank the bundle by 35%.

Case 2: Source map drift resolved

Maps pointed to transpiled JS instead of TS. Enabling sourceMaps across TS, Babel, and terser, and ensuring sourcesContent was preserved fixed production stack traces.

Case 3: Watch mode memory leak

A plugin cached ASTs keyed only by file path. Adding a content hash to the cache key and a size cap stabilized memory over day-long dev sessions.

Best Practices Checklist

  • Pin Rollup and plugin versions; upgrade in a managed wave with diff tooling.
  • Prefer ESM sources and exports; minimize CJS boundaries.
  • Use sideEffects metadata and @__PURE__ annotations.
  • Externalize peer deps for libraries; verify consumer environments.
  • Design chunking intentionally via manualChunks and analyze with visualizer.
  • Propagate source maps end-to-end; validate in CI.
  • Adopt a single fast transpiler; limit Babel usage.
  • Normalize import paths and dedupe critical packages.
  • Separate server and client configs with correct export conditions.
  • Automate size budgets, duplicate detection, and map verification.

Conclusion

Rollup excels when the module graph is predictable and transformations are disciplined. Enterprise failures usually trace back to a handful of themes: fuzzy ESM/CJS interop, hidden side effects that block tree-shaking, ad-hoc chunking, and brittle plugin chains. By instrumenting the graph, stabilizing interop, declaring side effects explicitly, and codifying chunk strategies, you can restore correctness and performance. Treat builds as products: measure, budget, and continuously improve. With the practices in this guide, teams can ship lean, dependable bundles—across browsers, servers, and edges—without sacrificing developer velocity.

FAQs

1. How do I know if a dependency is preventing tree-shaking?

Temporarily mark it as external and compare bundle size; if size drops, it is likely side-effectful or poorly structured for DCE. Use the visualizer to locate retained submodules and ask vendors for ESM-friendly builds.

2. What's the safest approach to ESM/CJS interop in mixed codebases?

Prefer native ESM throughout. Where CJS is unavoidable, configure commonjs with predictable defaults, use default imports for CJS modules, and avoid mixing synthetic default and named imports across layers.

3. Why are my source maps huge, and can I slim them down?

Large maps come from inlined sourcesContent and many transforms. Enable maps only for production artifacts that require debugging, and consider excluding vendor sources while keeping your app code mapped.

4. How should I split chunks for a micro-frontend architecture?

Extract shared frameworks into stable vendor chunks, and keep each micro-frontend's domain code isolated. Align chunk names with deployment boundaries and use consistent manualChunks across repos to avoid cache fragmentation.

5. When should I choose esbuild/swc over Babel?

Choose esbuild or swc for fast TypeScript/JS transpilation when you don't need complex Babel plugins. Keep Babel only for niche transforms (e.g., legacy decorators or React runtime tweaks) to preserve speed and tree-shaking.

Mindful Chase
Mindful Chase
Writing Code, Writing Stories

tbd

Experience

tbd

tbd

tbd

More to Explore

  • Troubleshooting MySQL: Replication Lag, Deadlocks, and Query Optimization
    Troubleshooting MySQL: Replication Lag, Deadlocks, and Query Optimization
    Databases 07.Aug
  • Troubleshooting FaunaDB: Fixing FQL Syntax, GraphQL Schema Errors, Role Misconfigurations, Latency, and Quota Limits
    Troubleshooting FaunaDB: Fixing FQL Syntax, GraphQL Schema Errors, Role Misconfigurations, Latency, and Quota Limits
    Databases 18.Apr
  • Advanced Troubleshooting: Tableau Dashboard Performance Degradation in Enterprise Environments
    Advanced Troubleshooting: Tableau Dashboard Performance Degradation in Enterprise Environments
    Data and Analytics Tools 09.Aug
  • Troubleshooting HaxeFlixel: Solving Performance and Build Issues in Large-Scale Games
    Troubleshooting HaxeFlixel: Solving Performance and Build Issues in Large-Scale Games
    Game Development Tools 05.Aug
  • Troubleshooting Nagios Services Stuck in PENDING State
    Troubleshooting Nagios Services Stuck in PENDING State
    DevOps Tools 05.Aug
Previous article: Advanced Troubleshooting: Scaling GNU Make for Enterprise Builds Prev Next article: Troubleshooting Broccoli Build Tool: Incremental Rebuild Failures, Plugin Leaks, and Performance Fixes Next
Copyright © 2025 Mindful Chase. All Rights Reserved.
Joomla! is Free Software released under the GNU General Public License.