Vivian Voss

The Build Step Is a Prosthesis

it philosophy javascript typescript node.js

The phone says twelve per cent, and the train has another forty minutes to go. Its owner opens a recipe. The page that arrives is mostly a cake and a list of ingredients, and behind it, according to the HTTP Archive's measurement of the web in 2024, sits the median mobile helping of JavaScript: 558 kilobytes, of which 206, or 44 per cent, are never used while the page loads. The phone downloads and parses all of it, and runs a good deal of it, and the cost of that, as the V8 team put it, is now dominated by "download and CPU execution time". On a phone, CPU time comes out of the battery. Eleven per cent.

JavaScript on the median mobile page, 2024 352 KB used 206 KB unused 558 KB in total 44 per cent never used while the page loads Source: HTTP Archive, Web Almanac 2024, JavaScript chapter

Nobody chose that on purpose. It is the sum of a few steps that every modern web project takes by default, each of them sold with a promise. The question worth asking of each is a very old one in medicine and a rather rare one in software: is this an organ or a prosthesis?

Organs and prostheses

An organ does work that would not happen otherwise. The Rust compiler is one: without it there is no machine code, and the guarantees it checks are still there when the program runs. A content hash in a file name is another, because it lets a browser keep a file for a year and still notice the day it changes.

A prosthesis is something else. It replaces a part that was missing, which is admirable while the part is missing, and it becomes a burden once the body has grown the part back and the strap is still on. The test is two questions long. What does this step promise? And does it keep the promise?

Put to the default web toolchain, the test finds four promises, and they belong on the table one at a time.

Four promises on the table

The first promise is correctness. TypeScript checks types while the code is written, and one study, Gao, Bird and Barr at ICSE 2017, found that it would have flagged 15 per cent of a sample of public JavaScript bugs before release. That much it does, mind. Then the compiler throws the check away. TypeScript's own handbook says so plainly: "Type annotations never change the runtime behavior of your program", and in its own example "our type annotations were completely erased." The data that actually hurts a program arrives at run time, over the network, from a form or an API, and by then no type is left to look at it. Node.js has drawn its own conclusion: since version 23.6 it runs TypeScript files directly by stripping the types, and since 24.12 that is stable. The alternative is to keep the check and lose the step. A type checker run as a check, tsc --noEmit over JavaScript annotated with JSDoc, gives the same editor support without producing a second program, and validation at the boundary, where data enters, is the only place a type can still protect anything.

The second promise is protection. Obfuscation and minification are sold, implicitly or otherwise, as a lock on the code. The best-known obfuscator describes its own goal with admirable precision as "making reverse engineering and automated deobfuscation dramatically harder", which concedes that it delays the reader and stops nobody. Its counterpart, webcrack, advertises in its first line that it will "deobfuscate obfuscator.io" and unpack the bundles as well, and about three thousand developers have starred it. Code that runs in a visitor's browser is code the visitor has. Whatever is worth protecting belongs on the server, behind an interface that answers questions without handing over the method; where the execution itself has to move elsewhere, the answer is confidential computing, and never a bundler flag.

The third promise is speed. Bundling was good manners under HTTP/1.1, when every file cost a connection. Vite, which bundles for production, gives the strongest version of the case: unbundled modules are "still inefficient due to additional network round trips from nested imports." For deep import trees that is partly true. The platform answered it some time ago. Every major browser has run ES modules natively since Firefox 60 in May 2018, and since Safari 16.4 on 27 March 2023 import maps work everywhere; WebKit's own announcement says they provide versioned mapping "without the need for a build step." For the rest there is a short list that needs no bundler at all: HTTP/2 carrying many files over one connection, modulepreload fetching the deep end of the tree in parallel, Brotli on the server doing what minification did, and a hash in each file name doing the caching. The bundle, meanwhile, ships the 44 per cent that nobody calls while the page loads.

From the source to the browser With the prosthesis TypeScript source compile types erased bundle, minify obfuscate browser one large file From the platform JavaScript JSDoc types served unchanged: ES modules, import map HTTP/2, Brotli, hashed file names, modulepreload browser same files tsc --noEmit a check beside the code no compile step on the way to the browser

The fourth promise is the most ambitious: one language for both sides. Node.js took a runtime built for the event loop of a browser tab and put it on the server. Its documentation is candid about the design: "Node.js scales well, sometimes better than more heavyweight approaches like Apache", because it "uses a small number of threads to handle many clients." For many idle connections that holds. The same page also explains the price. There is "one Event Loop", and "while a thread is blocked working on behalf of one client, it cannot handle requests from any other clients." A server with sixteen cores runs its JavaScript on one of them, and the others are reached by bolting on cluster, which starts further copies of the whole process, or worker_threads, which the application has to manage itself. Somebody entrusted a whole server to a program written for a browser tab, and nobody told it how many cores it had.

Why the wrong side won

None of this happened because the browser or the server asked for it. The platform caught up years ago. What did not change was the direction from which the system is thought about: from the tool the developer knows, outwards to the machine. Starting from the machine would have meant learning what the browser and the server can do by now, which is a faff, and no sprint has a ticket for it.

The tool most developers know is JavaScript. In Stack Overflow's 2025 survey 66 per cent of all respondents use it, more than any other language. A decade of courses taught it in weeks, and an industry then built its architecture on whatever the courses had taught. A company hires the language the market supplies, the language goes wherever the company needs code, and so the browser's scripting language ended up in the build pipeline first and on the server soon after. Readers of this column will have heard the npm part of the story before; it has not improved in the telling.

Being established is a measure of how many people learnt a thing. It says nothing yet about whether the thing keeps its promise, and a step that binds a fair share of every project's time and of every visitor's battery, whilst delivering its virtue in part, deserves the question all the more.

On the visitor's side the same direction has a price as well. Every script a page runs is code executing on someone else's device, and the most common weakness in the 2025 CWE Top 25, at rank one, is cross-site scripting. A page that needs little script of its own can forbid everything else outright, which is about the simplest defence there is. HTML and CSS carry a remarkable amount of a website these days, and JavaScript belongs where it adds something the browser cannot do on its own.

The limit

A large codebase needs its type checker. Where a project runs to hundreds of thousands of lines, the check pays for itself every day. The test asks only whether it has to sit as a compilation step on the way to the browser. With JSDoc and --noEmit it need not.

Deep module graphs still cost round trips. An application with thousands of small modules can measure a difference. That is where modulepreload comes in, together with a flatter tree, before anyone reaches for a bundler.

Obfuscation buys time. If time is all that is wanted, for a game client or a licence check, it delivers that, and the protection itself still lives on the server.

Node does what it promises for many idle connections. Glue code that waits on other services most of the time sits comfortably on one event loop; the test applies where the work is computation.

The browser grew the leg back years ago, and we are still strapping on the prosthesis every morning.