Building with Friction


I recently wrote about how important it is to make the right thing easy. The opposite is also true: it’s important to make the wrong things difficult. I did allude to it in that post a little bit, but I thought it was worth calling out explicitly. It’s important to introduce some friction in our workflow to help prevent the wrong actions.

Continuing on the health-related analogies, friction is a big part of how I manage my sweet tooth. I work by myself in a small office. Nothing is preventing me from constantly snacking on a bunch of sweets, and wow would I ever love to. I’m a sucker for just about anything with sugar.

But I discovered something else about myself: I’m also kinda lazy. So I take a two-part approach. The first is to make the right thing easy. I have apples, oranges, almonds, dried cranberries, and all sorts of healthier snacking options right next to my desk. If I’m hungry, I don’t even have to move. I reach out my arm, and there they are.

But the second part of that process is just as important. I make the wrong thing harder. I do have some sweets, but they’re tucked away in an adjacent room. It’s not difficult to get to them, but it does require more effort than the healthy alternatives right next to me. It doesn’t stop me from having sweets, but it means that reaching for that chocolate involves a conscious decision to put in more work than if I decide to have an apple. It’s just enough friction to change the way I snack.

A lot of modern workflow improvements have been around removing friction. We want to make it easier to deploy rapidly. Tools like npm make it very easy to gain access to any and all modules we could think of. Tag management enables folks to very quickly add another third-party service.

All of these things, on the surface, provide some value, but the consequences are tremendous. Because these processes remove friction, they don’t ever really give us a chance to pause and consider what we’re doing.

Re-introducing some healthy friction, some moments of pause, in our processes is critical to ensuring a higher level of quality overall.

For example, let’s tackle the trouble with npm.

npm transformed the way we build, but I don’t think anyone can argue that it hasn’t wreaked some serious havoc in the process. The ready availability of a JavaScript module for pretty much anything you can imagine has lead to security issues, accessibility concerns and overall bloat. It’s made it too easy to add more code to our sites without ever considering the trade-offs.

I’m with Alex on this one. Adding more code should be a very intentional decision:

JavaScript should be a *deeply” intentional choice on the client. Tools that remove intentionality, whatever else they may have done for your team, probably sunk your perf battleship.

Here’s an example of how we could introduce some friction into the process to help with the performance challenges by focusing on two critical points in our workflow: install and build/deploy.

During install

The first thing we can do is introduce some friction when we first install a script. After all, the easiest issues to fix are the ones that haven’t happened yet.

I like bundle-phobia-install for this. bundle-phobia-install is a wrapper around npm install that uses information from Bundlephobia to conditionally install npm modules. It does this by comparing the size of the package against some predetermined limits. It defaults to a size limit of 100kB overall (as in, the total of all dependencies), but you can configure that however you would like.

You can also set up limits on individual packages.

For example, the following settings (configured in a package.json file) would ensure that no individual package with a size of over 20kB could be installed, and that the total size of all dependencies can be no more than 100kB.

  "bundle-phobia": {
    "max-size": "20kB",
    "max-overall-size": "100kB"

Now, if we were to try to install, say, lodash, the install would fail because lodash exceeds our individual package size limit.

You could still install lodash, but that now requires you to run bundle-phobia-install with the interactive flag (-i) and manually approve the install despite the fact that it exceeds your limits. It turns an unconscious decision into a conscious one.

During build/deploy

By having some friction on the install process, we help to provide a better base for size of our JavaScript. It’s still critical to put some friction on the build and deploy process, though. For one, our install approach is only limiting npm modules, not really our own code. We also don’t really know the exact shape of our bundles at install—that comes later.

For webpack-driven projects, you can take advantage of webpack’s performance hints. There are two hints available to us: performance.maxEntrypointSize and performance.maxAssetSize. performance.maxEntrypointSize lets us set a limit for all webpack produced assets for a given route. performance.maxAssetSize lets us set a limit for any individual webpack produced assets.

By default, the hints are just that—hints. They show up as warnings but don’t do anything concrete. You can change that by setting the peformance.hints property to error.

So, given the following configuration, webpack would throw errors whenever an individual asset exceeds 100kB or all total assets for a given route exceed 150kB.

module.exports = {
  performance: {
    hints: 'error',
    maxEntrypointSize: 100000,
    maxAssetSize: 150000

If you’re not using webpack, or if you are and still want to augment these hints, we can also introduce some bundle size checking at the pull request or deploy levels. Bundlesize is a common choice here.

With Bundlesize, we setup maximum sizes for each bundle we want to track. Then we can run Bundlesize against those limits on every pull request or during our continuous integration process to stop us from deploying if any of those bundle sizes have been exceeded.

Building with friction

Healthy friction in our processes, paired with automation and reporting where appropriate, can have a substantial impact on what we ship. When we force ourselves to take these moments to consider the implications of what we’re about to add to our codebase, when we make it hard to add more bloat to our applications by default, we not only change the way we build, but we change the way we think about building. It’s the observer effect applied to the way we code.

When we have to consider the weight of every module we add to our project (or which vulnerabilities are included or what accessibility concerns they bring along), we start to inherently pay a little more attention to at least a part of performance every single day. It won’t magically fix all our performance woes by itself, but it certainly gets us pointed in the right direction.