You might not need Rollup for Bundling Libraries
For publishing one of my latest libraries I wanted to verify whether the old saying of using Rollup for bundling libraries and webpack for bundling apps still holds true.
So I did a little experiment and discovered that you might be much better off not bundling your libraries at all.
The assumptions
-
We’re using create-react-app with react-scripts 2.0.5 to scaffold an app that includes our bundled toy library. This means we will use webpack 4.19.1 for bundling this app.
-
The toy library looks like this:
index.js
export {default as Small} from "./Small.js"; export {default as Big} from "./Big.js";
Small.js
export default function Small() { return null; }
Big.js
import * as lodash from "lodash"; export default function Big() { return Object.keys(lodash).join(","); }
-
For this article we’re focussing on ES Modules as the output for the toy library, since commonjs modules do not benefit from webpack’s tree shaking by default. (There exists a plugin though)
-
The toy library is imported like this:
import {Small} from "toy-library";
So we’re trying to test whether certain ways of publishing this library lead to omitting the Big
Component from our final build.
Baseline
If the toy-library
is not included, the create-react-app
main js bundle takes up:
34.65 KB gzipped
Rollup #1
Since the toy library isn’t using anything fancy we can keep our config very simple:
rollup.config.js
export default {
input: "src/index.js",
output: {
file: "dist/bundle.js",
format: "esm",
},
external: ["lodash"], // dont include lodash's sources in the bundle
};
This results in a neatly bundled up file that looks like this:
import * as lodash from "lodash";
function Small() {
return null;
}
function Big() {
return Object.keys(lodash).join(",");
}
export {Small, Big};
While nice and concise this doesn’t help webpack from recognising that import {Small} from "toy-library"
doesn’t actually require to load the whole lodash
library.
So in our app we’ll end up with:
58.32 KB gzipped
Babel Cli
Let’s try something else then. Let’s use the @babel/cli
to prepare the toy-library for publishing. This build script is used in the package.json
{
"scripts": {
"build": "babel --out-dir dist src"
},
"devDependencies": {
"@babel/cli": "^7.1.2",
"@babel/core": "^7.1.2"
},
...
}
Since the toy library isn’t using any fancy javascript features, babel isn’t doing much. It ends up with putting three files into the dist
folder that pretty much look like the original three files.
So let’s look what the result looks like when importing the Small
function via
import {Small} from "toy-library";
58.32 KB gzipped
Okay. Apparently lodash
still get’s included by webpack into the main bundle.
After some digging around I found the reason why. The entry point of the toy-library
imports both the Big
and Small
functions. And theoretically, importing Big
could cause side effects.
By default webpack plays it safe and doesn’t exclude any import
.
So we need to tell webpack that the modules of the toy-library
don’t have any side effects.
Fortunately there’s a very straight forward of doing so. We can add this line into the toy-library
’s package.json
:
"sideEffects": false
This option tells webpack that all our modules are side-effect-free and can be removed whilst treeshaking the final bundle.
And indeed it worked. After building the app, it’s now just
34.67 KB gzipped
Rollup #2
These days Rollup allows code splitting. It’s still marked as experimental, but so far I haven’t come across any issues. This allows me to specify all entry points and Rollup will create the corresponding amount of bundles.
Here’s the adapted rollup.config.js:
export default {
input: ["src/index.js", "src/Small.js", "src/Big.js"],
experimentalCodeSplitting: true,
output: {
dir: "dist",
format: "esm",
},
external: ["lodash"],
};
And indeed, this results in an output that looks very similar to the one by the babel-cli
above. But there is a fundamental difference behind the scenes.
Babel takes a directory or files as input and transpiles each input file into a separate output file.
Rollup on the other hand takes the passed entry points (it doesn’t accept dictionaries) and creates a bundle for each of them. So it’s doing a lot more behind the scenes including making sure that these bundles are as small as possible via tree shaking.
So after making sure that "sideEffects": false
is set in the package.json
, the result is indeed what we were hoping for:
34.67 KB gzipped
Conclusion
When offering a library to users that can rely on webpack’s tree shaking capabilities, there’s no need to use Rollup to decrease your library’s impact on the final bundle size.
Quite the opposite: if users only want to use parts of your library, you are much more likely that babel’s naïve one-to-one mapping of input to output files will yield better results. While Rollup offers emitting several output files via experimentalCodeSplitting
, it’s still up to the library author to define all entry points manually.
There’s another benefit of not bundling your library before publishing it: users will be able to access small parts of your library. For example, they could import some sub-package.js
via import subPackage from "library/dist/sub-package"
So I’d like to conclude by suggesting to use babel for creating ES Modules and commonjs outputs. Rollup can be used for creating treeshaken umd output.
You can check out my bundle package to get an idea of what a full configuration could look like.