Skip to main content Accessibility Feedback

Web performance and parallel vs. waterfall downloads

In the past, I’ve written about whether or not we still need build tools.

With HTTP/2, you can download a bunch of smaller CSS and JS files simultaneously (you used to be limited to two at a time), removing some of the benefits of concatenating everything into one file.

Last week, Brian David and Brian LeRoux had an interesting conversation on Mastodon about build tools and bundling ES modules, and I thought it would be a good time to talk about this a bit more.

With HTTP/1, browsers will download up to two files per domain simultaneously. If they encounter a JavaScript file, they’ll stop downloading other files until the JS file has been been downloads, compiled, and parsed.

This creates bottlenecks where if you have lots of small files being downloaded, the HTTP “handshake process” adds a bunch of latency. Combining lots of small files into one big one was a way to avoid this issue.

With HTTP/2, browsers will download many files simultaneously. As a result, one-big file may actually be worse than a handful of smaller ones, since all of those small files can be downloaded at the same time.

So, does that mean bundling is pointless? This is precisely what the Brians were discussing.

Let’s say you have an ES Module file that looks like this…

import {add, subtract} from './calculator.js';
import {get, getAll} from './dom.js';
import {getData, setData} from './api.js';

// Run some code...

Would bundling be beneficial here? As always, it depends!

If calculator.js, dom.js, and api.js include functions other than the ones being imported, bundling would reduce the amount of JavaScript shipped over the wire through a process called tree-shaking.

Most modern bundlers will only include functions that are imported and used when creating the bundled JS file. With browser-native ES modules, the entire file you’re importing from is downloaded, even if you’re only using one or two functions from it.

If any of the imported files have their own nested import files, this is what Brian David referred to as a “waterfall download.”

// api.js
import {endpoint} from './urls.js';
import {parseData} from './helpers.js';

// Do code...

export {getData, setData};

The browser downloads the main JavaScript file, compiles it, parses it, and notices that it imports three other files. It downloads, compiles, and parses those. Then it notices that api.js has some imports of its own. It downloads those, compiles them, and parse them, and so on.

The compile/parse/repeat process can add a lot of latency. Bundling the files removes those extra steps, and can significantly speed up a larger application.

If the files your importing don’t have other functions you’re not using in them, and if they don’t have their own imports, you’re mostly just downloading a handful of files in parallel and then you’re done.

In that case, a bundler isn’t that useful. But in my experience, that’s also not usually how most code bases are setup.