Background

Recently, I decided enough was enough and that I should be able to properly code split for my pet project. As a quick introduction, code splitting is a term that applies to a compilation step – when compiling some source code to a target, we can decide that the source should be split up between multiple targets.

One could imagine developers back in the day concerned with how to compile code into their Windows libraries or Java JARs, but in web development/JS land, the name of the game is Webpack and javascript libraries. This is of extra importance because the size of your payload (bandwidth), and how many requests your app sends (latency), can both affect your users experience when they load a page.

Having one giant payload of a huge application sent to the client when your user wants to just use 5% of it to check one post is bad. Having an extra round trip for every little mouse click the user makes is also bad. Finding a middleground is good!

Code splitting is the method of finding that middleground. While real world analytics will probably be the best for getting accurate measures of how users use your application, you can probably make an educated guess of where logical splitpoints would go based on use cases. For example, for a blog, loading a infinite list of posts may be the first use case and one where the user doesn’t move past (/posts). Then when a specific post is viewed, the next use case of post manipulation is reached – viewing, editing, saving, etcetera (/posts/:id).

Implementation

Working with a webpack based project, my first step was to upgrade to version 4. While it’s not the first version to support code splitting, most of the documentation applied to v4, and it’s always good to have an excuse to update to new versions! An important note, I always treat an upgrade as its’ own step. That is, I would never have a commit Upgrade webpack to v4 and add code splitting. For your own sanity, I suggest splitting those two steps up.

For the most part, the upgrade was smooth, required little changes, appeared to improve performance, and simplified my config files. Kudos to the Webpack team!

The Gotcha

The gotcha arose when I started to implement code splitting. I followed the official documentation (https://webpack.js.org/guides/code-splitting/), specifically for dynamic imports. I wanted to be able to fully control at the code level where my splits would happen. This basically consists of using a proposed syntax that webpack already supports. A normal import X from 'x', which would include the x module in your payload, will transform to import('x').then(({ default }) => ...). Webpack will transpile this code to a routine that will send a new request for the code, wait for it, and then execute the promise properly.

I updated to this new syntax, and everything compiled and worked. The only problem was, none of my code was actually splitting!

The problem that Typescript introduces is that, depending on your configuration, Typescript will compiled the dynamic import import('xyz') itself to a require wrapped with a Promise, which would happen before the webpack process. This will happen if your target is commonjs, because Typescript knows dynamic imports is not supported by commonjs – what a good friend it’s being trying to nip that in the bud for us!

Really though, you can’t be too mad – this literally wouldn’t work unless it was being compiled by webpack straight after. Because you are probably only using webpack on the client, you can see the issue.

Instead, we can work around the issue by changing the target to esnext in our Typescript config. However! This then bit me the other way, because I in fact was relying on the commonjs target in other subtle ways that caused TS issues:

// Import assignment cannot be used when targeting ECMAScript modules.
import find = require("lodash/find");
// Not actually an ES module, complains and doesn't want to convert an object
// export to a module
import * as Bluebird from "bluebird";

, and so I also ended up doing some refactors there, and also including:

{
  "esModuleInterop": true,
  "allowSyntheticDefaultImports": true,
  "moduleResolution": "node"
}

in my config. Your mileage may vary, but hopefully this will solve your issues!