Does Rollup preserve re-exported symbols when tree-shaking a library build?
0 reputation · 23 Dec 2020, 15:19 UTC
0 reputation · 23 Dec 2020, 15:19 UTC
Goal is to configure a Rollup library build that maximizes downstream tree-shaking while keeping the declared public API intact.
Static tree-shaking relies on ES module syntax and accurate sideEffects metadata, and code splitting is driven by dynamic import() with manualChunks shaping. Output format choice between es and cjs changes interop helpers and export preservation. Plugin pipeline order of resolveId, load, transform can alter resolution and final structure.
The unresolved decision concerns which exports must be explicitly preserved when mixing native ES re-exports with CommonJS interop and dynamic splits, and how output format influences that preservation.
Does Rollup drop named re-exports that are unused in the entry but part of the public API? Which output format settings best preserve tree-shakable exports for downstream consumers? When does manual chunking interfere with export visibility?
29775 reputation · 23 Dec 2020, 20:43 UTC
It depends on which build you mean, and Rollup treats the two cases differently:
export { x } from './mod' is dropped, and if that was the only reason mod was included, the module itself can be omitted too.So the premise of the worry — that Rollup silently drops unused re-exports from a library entry — is generally unfounded. The public API declared by your entry is treated as live code.
A re-export creates a binding, not a copy. Rollup traces that binding back to the defining module and keeps the underlying implementation alive as long as the binding is reachable. In a library build, reachability starts at the entry's export list, so the whole chain entry → barrel → implementation survives. This is also why barrel files (index.js that only re-exports) are safe with Rollup: downstream consumers using ES modules can still shake out the parts of your API they never import, because the ESM output keeps each symbol as an independent named export.
For maximum downstream tree-shaking, ship output.format: 'es' (or dual-publish with es as the primary module entry). ES output preserves static named exports, which is exactly what consumer bundlers need. CommonJS output wraps exports in interop helpers and a single exports object, which most bundlers cannot statically analyze — downstream tree-shaking degrades substantially. If you must ship CJS, treat it as a compatibility artifact, not the tree-shakable one.
Set sideEffects: false in your package.json (or scope it to files that truly have side effects, e.g. CSS imports or polyfills). Without it, consumer bundlers must assume every module has top-level side effects and will retain modules even when their exports go unused. Inside your own build, the equivalent control is treeshake.moduleSideEffects (default true, meaning "assume modules have side effects").
output.manualChunks overrides Rollup's natural splitting. Two practical risks for a library:
For most libraries, prefer preserveModules: true (ES output) over manual chunking: it mirrors your source file structure, keeps each export independently reachable, and gives downstream bundlers the cleanest graph.
format: 'es' and confirm both symbols appear as named exports — they are public API and must survive.sideEffects: false toggled and compare bundle sizes to confirm module dropping works as expected.// rollup.config.js (library)
export default {
input: 'src/index.js',
output: {
dir: 'dist',
format: 'es',
preserveModules: true
},
treeshake: { moduleSideEffects: false }
};Side-effect detection is heuristic: modules with top-level side effects (logging, global registration) may be retained even when their exports are unused, which is usually correct. Exact behavior can vary across Rollup versions and with plugins such as @rollup/plugin-commonjs, whose interop can obscure re-export chains — verify against your installed version with the steps above rather than relying on defaults alone.
Use comments to ask for clarification. Post a solution as an answer.
29,775 reputation · 23 Dec 2020, 23:47 UTC
While Rollup treats entry point exports as live code, it is important to clarify how the sideEffects property in package.json interacts with this behavior during downstream consumption. Even if Rollup preserves a re-export in your library build, a consumer's bundler may still prune the entire module if it is marked as side-effect free.
To ensure your public API remains intact and tree-shakable for others, verify your package.json configuration:
"sideEffects": false: Tells consumers that any module not explicitly imported can be safely removed. This is ideal for libraries consisting purely of functions and classes."sideEffects": ["*.css", "./src/init.js"]: Use an array to protect specific files that perform global initialization, preventing them from being shaken out even when their exports aren't used.If you are using export * from '...', ensure that the source modules do not contain unintended top-level side effects, as this can force the consumer to include the entire module regardless of which specific symbols they import.