Using and bundling individual D3 modules
Version 4.0.0 of D3 came out a little over a month ago. v4 is exciting because the library is now a collection of small modules.
I'm always interested in making sites as small as possible so I spent a some time migrating a D3-based map from v3 to v4. Here's the original map based on v3. There's not much to it but it's loading all ~150 KB of D3 plus another request for a single KB for queue. The goal is to save resources so let's say our baseline is ~150 KB of JS and two requests.
Here's the plan I came up with to explore this new modular structure:
- one script tag per module referencing d3js.org
- one script tag per module referencing a local
.js
file - a single script tag referencing some kind of ugly rolled pack bundle thing
Clearly that last one is ideal: one script tag to load all the JavaScript required by the page.
I tweaked the original to use individual modules via script
tags referencing d3js.org. Here's a bl.ock showing that. With this approach, the page is loading ~50 KB of JS via seven requests. That's a nice reduction in the amount of JS but too many requests.
The second stop on this tour of modular D3 was an intermediate one. There wasn't much use in having all the modules locally. Maybe it'd be nice if I was trying to make something that could run on an intranet without internet access, or something to embed in an app. The real reason to have everything locally is to get closer to number three. After a few npm install
's I was all set up with everything in a local node_modules
directory. I tweaked the paths in the script tags but there's no appreciable difference vs. the CDN so let's move on.
The JavaScript bundler du jour is webpack so that's what I set out to try. After haphazardly tweaking various parameters in webpack.config.js
, I gave up. I couldn't figure out how to get all modules into a single file exposing a d3
global with all the methods expected by my code.
In researching webpack workings, I stumbled on Mike Bostock's custom bundle tutorial. He's using rollup so I figured why not give that a shot. Following his example, rollup.config.js
is almost identical:
import npm from "rollup-plugin-node-resolve";
export default {
entry: "index.js",
format: "iife",
moduleName: "d3",
plugins: [npm({jsnext: true})],
dest: "d3.rollup.js"
};
The more important part is my index.js
which exports all the necessary D3 pieces:
export { select, selectAll } from 'd3-selection';
export { json } from 'd3-request';
export { geoAlbersUsa, geoPath, geoGraticule } from 'd3-geo';
export { queue } from 'd3-queue';
The lines above started in the form of:
export * from 'd3-selection';
But the whole point of this exercise is to get only the code that's required so instead exporting everything from each module, I exported only methods that were used in my code. It's a bit more work to figure out which specific methods to export but it's worth it: the final JS size will be noticeably smaller. That being said, if you're moving from monolithic D3 to modular D3, even if you export *
you'll see a reduction in the amount of JS you're including in your pages.
After ripping off the tutorial config file, getting index.js
just right and combining with UglifyJS, I had a single script file that had everything my page expected so here's yet another California reservoirs bl.ock.
With renewed confidence, I revisited the mess I thought was a webpack configuration. Before the rollup breakthrough, I was struggling with how to export everything as a single global D3 to mimic what you get when you load monolithic D3. After realizing I could use es2015-style exports, I figured I'd try feeding the index.js
I'd used with rollup to webpack. The last piece was discovering webpack's output.library
property. I added that to my webpack config to specify the name for the resulting global and it worked!
Here's the webpack.config.js
:
var webpack = require('webpack')
module.exports = {
entry: "./index.js",
output: {
path: __dirname,
filename: "d3.webpack.min.js",
library: "d3"
},
module: {
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
}]
},
plugins: [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
]
}
And of course, the same map as a bl.ock with output from webpack.
What about file size and requests and all that? The rollup output, after running through Uglify, comes in lightest at ~40 KB of JavaScript via a single request. The webpack output comes in at ~60 KB. I have no idea why. If every KB counts, use rollup. When gzipped, things get even better. Rollup's output is ~15 KB and webpack's is about 20 KB.
I've put all the various flavors of this experiment up on github if you want to play around with anything.
It's worth noting that this finer-grained D3 structure wasn't a straight drop-in replacement for v3. Code changes are expected when moving to a major new version of a library and this was no exception. I had to make a few tweaks to my code but that's a topic for another post.