Deployment @ Spacebib — Migrating from JSPM to Webpack 2

At Spacebib, we use React for the front-end as a hybrid between a single-page application(SPA) and legacy app. This means our root component is not the HTML root, but rather at some lower point in the DOM after having rendered a PHP template page. Working in this manner has led to some interesting technical challenges, one of those being our tooling.

Starting out as a single page application in React is very straightforward. You serve a single bundle that contains all the application code, and once the bundle is loaded all pieces of your app fall into place, barring any asynchronous data loading.

On the other end of the spectrum, when working with a legacy app, you first load the legacy app, then later instruct it to load either a specific bundle for that specific page, or the entire bundle.

Our original approach

When we started out with React in 2016, it was still in a state of great flux, and there were many competing choices for state management, bundling, routing, and so on. One of the libraries that we adopted was JSPM, and can be seen in a talk given here: https://engineers.sg/video/alternative-libraries-for-react-reactjs-singapore--900

To summarise the talk briefly; we picked JSPM because of the ease of use and documentation for simple workflows, as well as deduping installations to a manageable time/size (this was before yarn came out, but even now it still beats npm and yarn in installation size).

We also had a simple workflow that loaded the entire bundle, with conditional code to render parts of the bundle based on HTML divs. Below is a quick and dirty example:

components/pages/App.js
class App extends Component { ... }
if (document.getElementById('App')) {
render(App, document.getElementById('App'));
}

This is pretty straightforward, and having everything in a single bundle leaves out the need for conditional loading logic outside of the bundle.

Growing pains

However, while JSPM was pretty good at what it did, it didn’t do very much when it came to customizability. Source maps were partially broken, hot-reloading in JSPM was still experimental and did not have much support, setting up a test suite was a roadblock when the test libraries expected to read from node_modules but ended up with jspm_packages instead.

We had been eyeing Webpack for a while due to its extensive ecosystem, but had been putting it off since it was stuck in 1.x and 2.x promised a lot more.

In the last month, Webpack finally came out with their new version 2: which promises features that JSPM previously championed, like native ES6 import/exports and ES6 module tree-shaking.

We implemented a process to migrate over to Webpack 2, and it was a surprisingly straightforward process with a few hiccups.

Migration pains and gains

  1. Loss of an easy library wrapper

One plus about JSPM is that it is very easy to include external libraries — there is support for direct github project links, which read the package.json and serve the corresponding distributed source accordingly. The JSPM installer generates appropriate configuration and serves the library via SystemJS, giving you an ES6 API to work with. Being able to abstract away the differences between module formats like commonJS and AMD, has been a huge boon for those fatigued with the current Javascript landscape.

With Webpack 2, we did not get that safety net.

To take one example, we had an issue with Summernote. With JSPM, the wrapper about that library was a simple import 'summernote'; , whereupon it would evaluate the bundle at run-time. With Webpack however, pre-bundling meant it would try to evaluate the code at bundle time, throwing errors in a non-browser context.

2. Code splitting

With Webpack 2, we could finally utilize code chunking in its full glory. The downside of a large single bundle was its huge size, sitting at an unfriendly 1.6MB even after minifying and compression. To split up the bundle with JSPM would require complex bundle arithmetic that wasn’t well documented, let alone going the extra step of moving common code into a separate bundle and ensuring they all worked together.

Using a combination of CommonChunksPlugin and ManifestPlugin allowed us to mark large regions of code for browser caching instead of reloading a huge new bundle with every release. This stackoverflow answer also helped us to do smart chunking, resulting in the following snippet:

const isExternal = module => {
const userRequest = module.userRequest;
if (typeof userRequest !== 'string') {
return false;
}
return userRequest.indexOf('node_modules') >= 0;
};
const reactChunks = ['components/pages/App.js', ...]; // all react root components
const plugins = [
new webpack.optimize.CommonsChunkPlugin({ // common react userland code
name: 'react',
chunks: reactChunks,
minChunks: function(module, count) {
return !isExternal(module) && count >= 2;
}
}),
new webpack.optimize.CommonsChunkPlugin({ // common react vendor code
name: 'react.vendor',
chunks: reactChunks,
minChunks: function(module) {
return isExternal(module);
}
}),
new ManifestPlugin(),
];

which produces at least 3 chunks, react.js which is the common code amongst our components, react.vendor.js which is the common vendor code amongst our components, and finally the components themselves, App.js. That’s a lot of caching when only App.js has to change!

3. Hot reloading

With the seamless integration of webpack-dev-server, we managed to get this much vaunted feature to work. It was still an involved process that required much trial and error, but that journey is out of the scope of this article.

Looking forward

The javascript bundler landscape is still not yet out of the aftershock phase, but things are settling down where most of our needs are covered by Webpack 2 now — although competitors like Rollup still remain on the horizon.

The field of development tooling can get very involved!