A spectre is haunting web development — the spectre of JavaScript Fatigue. All the powers of the old Internet have entered into a holy alliance to exorcise this spectre: programmer and designer, browser vendor and framework author, thought leader and shitposter.

But while the situation has improved incrementally since the coining of JavaScript Fatigue in late 2015 — create-react-app has made it a lot easier to get started with the React salad — that only treats the symptoms, not the disease.

Old-school web developers like Peter-Paul Koch roll their eyes at the teetering towers of complexity we’ve built for ourselves. They long for simpler times, when web developers worked directly in the raw materials of html, css, and javascript. They see the modern reliance on tooling as a “fetish” that is harmful to users; if you can’t do without tools you’re not a web developer.

The nine words you never want to hear a programmer say

Allow me to play Devil’s Advocate for a moment.

Ignore PPK’s inflammatory comment, and acknowledge the underlying truth: our tools really are complex, our web apps really are big, and we really are detached from the raw materials of our medium. Web development is harder than it used to be and probably harder than it needs to be. We have surrounded ourselves with complexity, but we can’t distinguish between the essential and the accidental.

The typical (modern/overcomplicated) JavaScript app is a “Single Page Application” built with a framework (e.g. Angular/Ember/React) that renders the entire app at runtime. Every individual choice that took us from server-rendered static pages to the current day makes sense in isolation:

scripting allowed web pages to be interactive without requiring a refresh.

AJAX likewise made pages feel more interactive — the data on the page could now change without a full refresh.

When a significant amount of data on the page is changing, it’s helpful to have a client-side templating system, instead of manually adding DOM nodes.

If we’re building pages on the client side, then doing the same thing server side is redundant.

If the server is sending the same raw data to the client as it is to the mobile app, then maybe the client should be built like an app that runs in a browser?

We’re writing way more javascript than we were before; we need tools to help us bundle all of these files together and minify them.

Since we already have to use build tools, we should extend them to solve some of our other problems with JS app development.

A modern web app is undeniably more complex than a server rendering static html — there are simply more moving parts — but it can do so much more than static html. Every individual step added some complexity to the configuration, but also made new work possible. Each step was an investment — up-front configuration costs that paid off over time.

Each step makes sense in context. If one had been following the state of the art, and upgrading their tooling as new tools came along, the individual changes were manageable. But the net result is a system that is incomprehensible to outsiders: a technological bureaucracy that’s completely inscrutable to both beginners and people who haven’t been able to keep up with the changes.

This is compounded by how often we find ourselves solving problems of our own creation; we use tools to mitigate problems caused by the previous set of tools. This is something we see in a lot of webpack’s banner features:

Hot loading solves the problem of apps losing state & taking a long time to reload.

Incremental builds solve the problem of builds from scratch taking a full minute.

Loaders solve the problem of frameworks modularizing HTML & JavaScript but not CSS or assets.

Code splitting solves the problem of single-page apps compiling into one huge file.

In turn, these features add their own problems:

Hot loading and incremental builds require you to set up separate builds and servers for development and production.

Loaders and code splitting are incompatible with regular node module resolution and break compatibility with standard tools, requiring either workarounds or webpack-aware tooling.

Radical simplicity

We cannot make this system less complex by adding to it. We cannot patch our way out of this; we need breaking changes. But our tools aren’t just complicated because of incremental design and unintended consequences. There is still complexity inherent to the problems we are solving. Even if we were to start from scratch, knowing what we now know, we’d still have the same diversity of platforms to support, the same frustrating edge cases, the same expectations of what a web app should be. If rewriting from scratch doesn’t work for individual software projects, why would we expect it to work for a whole ecosystem?

What we need is much more radical than a “do-over” — we need to solve different problems.

Would a good Forth programmer do better than me? Yes, but not just at the level of writing the code differently. Rather, at the level of doing everything differently. Remember the freedom quote? “Forth is about the freedom to change the language, the compiler, the OS or even the hardware design”. …And the freedom to change the problem. Those computations I was doing? In Forth, they wouldn’t just write it differently. They wouldn’t implement them at all. In fact, we didn’t implement them after all, either. The algorithms which made it into production code were very different — in our case, more complicated. In the Forth case, they would have been less complicated. Much less.

Yossi Kreinin, “My history with Forth & stack machines”

Now, in our industry, we don’t have much control over the platform or the hardware. But sometimes a change comes along that resets our expectations. The first iPhone came out about ten years ago, and it gave software designers a new paradigm to work with, unencumbered by expectations. Mobile websites needed to be radically simpler than their desktop versions, but until this point the mobile web wasn’t even worth considering. The constraints of a tiny screen, on a weak computer, getting data over slow cellular networks, forced us to develop new ideas about how to design for the web.

Many of these new ideas are an improvement over the old ways. Responsive design is a better way of thinking about web layout. Designing for touch forces us to give up on mystery meat navigation. And using less data makes for a better experience on unmetered wired connections as well.

Radicals and reactionaries

While there’s no shortage of people in the JavaScript community complaining about the complexity of our tools, few appreciate how they became complex, and what it would take to make them simpler. PPK is correct in thinking that the existing systematic complexity is unsustainable, but he mistakes correlation for causation: the tools are a symptom of complex problems, not the cause. His suggestion that web developers need to be able to “do without tools” ignores the reasoning for creating the tools in the first place.

Contrast that with someone like Substack. He’s a harsh critic of the complexity in modern JS development — frequently to a degree I consider unreasonable. But he’s not anti-tooling by any stretch of the imagination. If anything, he’s probably responsible for more JS tooling than any other individual. But his tools are imbued with the unix philosophy of “doing one thing well” — they’re not always easy to use, but they are consistently simple and composable.

This is illustrated beautifully in his starting point for building webapps. It features the essential components of modern JS development — node modules, client-side rendering — while removing almost everything else. It does a lot less than create-react-app, but it handles the parts that matter most, and explains how to incrementally add complexity when the program requires it.

When we lump Substack and PPK together as “web minimalists”, we’re conflating the radical with the reactionary. The reactionary approach, though superficially similar to the radical one, has opposite goals: instead of creating a new system, the reactionary returns to the old one; instead of making tools accessible to a new generation of developers, it merely reinstates the old gatekeepers. This is the problem with PPK — his vision is fundamentally nostalgic, not progressive. He doesn’t want to make web development better; he wants to Make Web Development Great Again.

Raising radical voices

I am neither radical nor reactionary. If we were to extend the political metaphor, I would be the kind of liberal that people complain about on twitter. I’m trying to improve the system, but only incrementally; I am too comfortable in the status quo to overthrow it.

But while I lack the vision and the will to change the world, I know which way the wind is blowing. Something is going to come and knock the whole system over, just as the mobile web did ten years ago. Our existing tools will be too complex, too hidebound to adapt, and a fresh start will not only be possible but necessary.

In the meantime, I’m trying to chip away at the complexity of the projects that I work on. I recognize that this will do nothing about the fundamental issues causing JavaScript Fatigue, but I can at least make things easier for people who work with me. (A vegan understands that there is no such thing as ethical consumption under capitalism, but she doesn’t stuff her face with foie gras while waiting for fully automated luxury communism to arrive.)

For example: I use React in most of my projects, both personally and at work, and I’m usually the first person on the team to try out a new webpack configuration. But I’m finding more and more ways to work within the parameters of create-react-app, with a particular focus on avoiding some of the self-inflicted problems I mentioned before:

I keep non-transient state in the URL query and in localStorage, and I keep each app small enough that it loads in under a second. This mitigates the lack of hot loading in create-react-app.

I’ve used a couple of alternate approaches to webpack’s CSS loaders, including atomic css frameworks and CSS-in-JS.

Instead of building one big app and using code-splitting to chunk it up, each page is its own app; routing is done server-side. Because they’re on the same domain, they have access to the same persistent storage and cookies; that is sufficient to make them work together cohesively.

And in preparation for the mysterious, but certain future, I’m trying to listen to more radical voices and explore the constrained but dramatically simpler tools out there:

next.js aims to make modern JS app development as easy as PHP templates.

Elm is a humane functional language with friendly docs, good developer tools and a zero-config compiler.

Gomix reconciles the View Source-era of web development with modern apps in a Google Docs-style interface.

Conclusions

Here’s the thing: I know that most — maybe even all of these — are dead ends. Radical utopian visions are almost always wrong! But even a half-baked idea expands our ideas of what is possible; giving voice to radical ideas expands the range of discourse. We might not be able to use a proof-of-concept in production, but we can experiment with them and see where to develop further. But while we’re exploring these options, here’s a few things we can do in the meantime:

First, we need to ask: should we even be writing software to solve this problem? Could this app be replaced with a spreadsheet?

Next, we need to scale back the complexity of our existing projects. There are limits to incrementalism, but we can at least soothe the pain. How can we make our systems more approachable for people who haven’t been following the last four years of blog posts?

Finally, we need to elevate radical voices. Reactionaries have a major advantage over radicals — they only have one idea, so they all sound in unison, while every radical has their own unique, incompatible vision. We have nothing to gain from further amplifying reactionary voices, and nothing to lose by amplifying radical ones. We cannot follow them all, but we owe it to ourselves to at least listen.

Radical simplicity requires sacrifice. In order to free ourselves from the weight of our tools, we need to accept tighter constraints, looser specifications, and narrower scopes. We need to set new expectations of what our software should look like, and who our tools are designed for. But the hardest sacrifice to make is in ourselves — we need to make our knowledge obsolete. This is ultimately what separates the reactionaries from the radicals — are we prepared to give up our expertise, our status, and fumble blindly into the future?