I figured that the first step in fiddling with the JavaScript code I use on my navigation widget will be to convert it in to a more fashionable build system, such as Webpack. Can I reduce the bandwidth usage of my pages thereby?
Why is Webpack?
In primitive times, JavaScript files were individually linked to from your HTML page. As you use more and more
third-party JavaScript libraries, this ends up with dozens of script
tags in a row just before the closing body
tag.
The malleability of JavaScript and the fact it can manipulate the DOM to include additional script
tags means that you
can create dynamic loaders; several packages arrived that work by supplying a require
function that works like the
equivalent in Node: each module can use it to load the modules it in turn depends on. There are alas! several module
conventions (Node’s CommonJS, import
… from
syntax that transpilers transpile in to
one of these formats according to command-line options chosen by baffled programmers.
The trick with all of these is that they involve extra web requests to download the modules as they are required. This overhead is lessened with SPDY or HTTP/2 but it is still an overhead that delays the start of your JavaScript code.
If the require
variants are like dynamic linking, Webpack is like a static linker: it scans the dependency tree of
your application, optionally runs transformations (like Babel), and bundles the code together in to one large file.
This can then be minified and compressed as a unit, which should get the best bang for your bandwidth buck.
A feature of Webpack I am not using on this site yet is that it can also pack your CSS, using
require
statements in the JavaScript to tie them together.
Webpack arranges for them to be appear to as another
module, one that to add style
elements to the page.
Packing Entry Nav with Webpack
The main change needed to exploit Webpack is to pull the main entry point in to its own file; Webpack trances dependencies from there. It ended up looking like this:
import React from 'react';
import {render} from 'react-dom';
import {EntryStore} from './entry-store';
import {EntryNav} from './entry-nav';
window.entryPage = function (options) {
var entryStore = new EntryStore(options.store);
render(<EntryNav entryStore={entryStore} initialDate={options.date} />, options.element);
}
In my case I am invoking it from my make
files, and the webpack.config.js
lives inside the alleged.blog
Python package within my Django file structure.
The Webpack config file looks like this:
var webpack = require("webpack");
module.exports = {
entry: './components/entry-page',
resolve: {
extensions: ['', '.webpack.js', '.web.js', '.js', '.jsx'],
}
module: {
loaders: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
query: {
presets: ['es2015', 'react'],
}
}
],
},
plugins: [
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin({}),
],
output: {
filename: 'entry.js',
path: './static/js',
},
}
This basically say start with the entry page, find referenced .js
and .jsx
files, convert to old-type JavaScript
with Babel, minimize with UglifyJs and write the bundle to entry.js
.
Because this module imports the React libraries, I can remove one of the script
tags from the HTML.
Replacing jQuery with the Promise of the future
Since I am using React to manage manipulation of the DOM, I am only using jQuery in two places:
- for its
$.ajax
function that wraps the unpleasantness ofXMLHttpRequest
calls, and - one occurrence of
$(elt).addClass
in the code for the animated transition.
The second of these is just a case of writing a simple addClass
function of my own.
We can replace my simple use of $.ajax
with the futuristic new HTML5 function fetch
. This
makes requests with a nicer API than XMLHttpRequest
and, even better, handles waiting for the response using the futuristic new JavaScript concept of
promises.
The code for maybe loading the data now looks like this:
loadYearData(year, onYearDataReady) {
if (!(year in this._promisesByYear)) {
this._promisesByYear[year] = fetch(this.yearDataApi + '?year=' + year)
.then(response => {
if (response.status >= 200 && response.status < 400) {
return response.json();
} else {
var error = new Error(response.statusText)
error.response = response
throw error
}
})
.then(obj => {
if (!('years' in this.data)) {
this.data.years = {};
}
this.data.years[year] = obj;
return {year: year, yearData: obj};
});
}
return this._promisesByYear[year];
}
The fetch
function starts the download and returns a promise; the
first then
call arranges to parse it as JSON when and if it
succeeds or raise an error otherwise; the second then
takes the
JSON once it is parsed, stores it, and returns it. By returning the same
promise to subsequent callers we ensure the HTTP request is
made once but all the callers get the return value once it is ready.
Promises are supported natively in most browsers. A polyfill is
I believe supplied by core.js
, which in turn is supplied by Babel’s
babel-preset-es2015
preset, so I don’t need to do anything special
to use it.
The fetch
function is available on some browsers but will need a polyfill for now.
The neatest way to do that using Webpack is to use its ProvidePlugIn
. The following
voodoo code added to the webpack.config.js
does the trick:
plugins: [
new webpack.ProvidePlugin({
'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch',
}),
…
],
This syntax is more than a little obscure, but I think it translates
roughly (reading right-to-left) as ‘load the module whatwg-fetch
,
import its global fetch
, then use this to create a global variable
named fetch
’. This removes any need for lines like import fetch
from 'whatwg-fetch'
in the calling code, which is nice because it means
I write my code as if we were in the future where
fetch
and Promise
are supported in all browsers.
Reduction in download size
Part of the point of this exercise—apart from moving my development practices towards the future—is to reduce the amount of JavaScript code downloaded to show my silly entry navigator. So the question is, is adding bundling and polyfills a win? Here is the table from before, with the new version added:
Resource | Before/KB | After/KB |
---|---|---|
react.min.js | 36.2 | — |
jquery.min.js | 29.4 | — |
entry-nav.js | 11.9 | — |
entry.js | — | 57.8 |
This takes the total for the navigation code from 77.5 KB to 57.8 KB. Not a bad reduction, and with cleaner code to boot!