Skip to content

Chapter 16: The JavaScript Runtimes Era: Node, Deno, Bun

Most of this book is about JavaScript in the browser. This chapter is about JavaScript outside it.

That’s worth a sentence of justification. The argument the rest of the book makes — that the browser is a serious application platform we can build on directly — doesn’t depend on the server-side JavaScript story. Most readers’ applications will still talk to a server, and the server might run any language. What does depend on the server-side story is the frontend toolchain. Every build tool, bundler, dev server, type checker, linter, formatter, test runner, and deployment system that touches modern frontend code runs on a JavaScript server runtime. The runtime under your tooling shapes what your tooling can do.

The runtime under your tooling has changed three times in fifteen years. This chapter is about those changes — Node, Deno, Bun — and what each of them was arguing.

On November 8, 2009, at JSConf EU in Berlin, Ryan Dahl gave a talk titled Node.js. The talk introduced a new JavaScript runtime built on Chrome’s V8 engine, designed around a single-threaded event loop with non-blocking I/O, and aimed at a problem most JavaScript developers hadn’t been thinking about — building high-concurrency network servers.

The thesis was specific. Servers spend most of their time waiting for I/O — reading from disk, talking to databases, waiting for upstream HTTP responses. Most server languages of 2009 handled concurrency with threads, where each incoming request got its own thread of execution that blocked while waiting for I/O. Threads are expensive (each one needs its own stack, its own scheduler overhead, its own memory), and a thread-per-request server hits scaling limits in the low thousands of concurrent connections. Node’s argument was that JavaScript’s existing event-loop model — already used in the browser to handle asynchronous events — could be applied to server work, and that a single-threaded event loop running thousands of asynchronous I/O operations in parallel would outperform a thread-per-request server on the same hardware.

The argument turned out to be right, and the implementation was good. Node spread quickly through 2010 and 2011 as the obvious choice for I/O-bound network services — real-time applications, API gateways, chat servers, anything that needed to keep many connections open simultaneously. The startups of the early 2010s adopted Node aggressively, and it became, within a few years, one of the major server runtimes in production use.

The more lasting consequence wasn’t the server. It was npm.

Node’s package manager, npm, was created by Isaac Schlueter in 2009 and shipped publicly in 2010. The npm story has enough weight to deserve its own chapter, which is the next one. This chapter covers only what npm did to the frontend.

What npm did to the frontend was move the toolchain.

Before npm, frontend tooling was a fragmented set of scripts, makefiles, Ruby gems (for Compass and SASS), Java JARs (for the Closure Compiler), Python utilities (for the original Sphinx documentation generator), and ad-hoc downloads. There was no shared substrate for distributing JavaScript build tools because there was no shared substrate for distributing JavaScript. npm, by providing a registry and a CLI for installing JavaScript packages, gave the frontend community a place to publish and consume tooling.

The migration happened over roughly five years. Grunt (2012) was an early Node-based build tool. Gulp (2013) followed. Webpack (Tobias Koppers, 2012) became the dominant bundler by 2015. Babel (originally 6to5, by Sebastian McKenzie, 2014) became the standard JavaScript-to-JavaScript transformer for handling ES6 features in older browsers. ESLint (Nicholas Zakas, 2013) replaced the older JSHint and JSLint. Prettier (James Long, 2016) introduced opinionated automatic formatting. Each of these tools ran on Node, was distributed via npm, and was developed by people whose primary background was frontend.

By 2017, the standard frontend codebase had a node_modules directory with hundreds or thousands of packages, a package.json declaring dependencies and scripts, and a build process that ran entirely on Node. The runtime that Ryan Dahl had introduced in 2009 as a server platform had become the substrate of the frontend itself.

This is the durable contribution of Node. The server-side use case is still significant, but the frontend toolchain consolidation is the change every working frontend developer interacts with daily.

In June 2018, Ryan Dahl gave another JSConf talk. This one was titled 10 Things I Regret About Node.js.

The talk was a public accounting of what Dahl now considered design mistakes in his own work. The list included the choice not to use Promises in Node’s core APIs (the standard library had been built around callbacks before Promises landed in ES2015, and the result was a sprawling ecosystem of util.promisify-based wrappers and inconsistent API styles). It included the lack of any security model — Node code can read any file, make any network request, and execute any binary, and sandboxing is the developer’s problem. It included the centralized package registry as a single point of failure (npm Inc. controlled the registry and could, in principle, remove packages or freeze the ecosystem). It included the node_modules directory as a poor distribution mechanism. It included the split between CommonJS and ES modules. It included the lack of TypeScript as a first-class language. And several smaller points.

A few weeks later, Dahl announced Deno — a new JavaScript runtime, also built on V8, that addressed each of his regrets. Deno was secure by default (a Deno script needed explicit permissions to read files, access the network, or read environment variables). It supported TypeScript natively without a separate compiler. It used URL-based imports (import { something } from "https://deno.land/x/library/mod.ts") instead of node_modules. It used Promises throughout the standard library. It bundled a formatter, linter, and test runner.

Deno was technically credible and conceptually clean. It also struggled.

The reason was ecosystem inertia. npm, by 2018, was a registry with over a million packages and a decade of accumulated tooling, documentation, tutorials, and Stack Overflow answers. Deno’s URL-based imports asked the ecosystem to migrate to a new distribution model and rebuild much of what npm provided. Some libraries shipped Deno-compatible versions; most didn’t. Deno added partial npm compatibility in 2022 as a concession to ecosystem reality, which helped, but the framework’s mindshare remained much smaller than Node’s.

Deno’s design choices were largely vindicated. The Node project itself adopted many of them — Node 20 added a test runner, Node 18 added native fetch, ES modules became first-class in Node, TypeScript-native execution arrived in Node 22 (with --experimental-strip-types). The argument Deno made publicly was, in many cases, the argument the Node maintainers eventually agreed with internally. The runtime that won, though, was still Node.

Bun’s story is shorter and stranger.

Jarred Sumner started Bun in 2021, working alone. The project’s defining characteristic was speed. Sumner had written Bun in Zig — a relatively new systems language with manual memory management and aggressive performance characteristics — and the result was a JavaScript runtime that benchmarked significantly faster than Node or Deno across a wide range of common workloads. Bun installed npm packages faster, ran HTTP servers faster, executed JavaScript faster (using JavaScriptCore, Safari’s engine, rather than V8), and bundled JavaScript faster (Bun’s bundler outperformed Webpack, Rollup, and even esbuild on common workloads).

Bun’s speed was its initial pitch, and the benchmark numbers spread quickly through the developer community. The deeper architectural argument was consolidation. Bun shipped as a single binary that provided a runtime, a package manager (compatible with npm), a bundler, a transpiler, a test runner, and a process manager. The fragmented toolchain that had accumulated around Node — npm or pnpm or Yarn for installs, Webpack or Rollup or esbuild or Vite for bundling, Babel or SWC for transpilation, Jest or Vitest for testing — could, in principle, be replaced by bun install, bun build, bun run, and bun test.

The consolidation argument was attractive to teams tired of maintaining build configurations across half a dozen tools. Bun 1.0 shipped in September 2023 with production-ready stability claims. Adoption grew steadily through 2024 and 2025, particularly in newer projects and in companies that valued the developer-experience improvements over Node’s ecosystem maturity.

In 2025, Bun was acquired by Anthropic. The acquisition was unusual. Anthropic is an AI company, not a developer-tools company, and the strategic rationale wasn’t immediately obvious to the outside world. Sumner stayed on at Anthropic to continue leading Bun’s development. The acquisition is recent enough that its long-term implications are still unsettled. The most defensible reading is that AI-generated code needs a fast and predictable JavaScript runtime to execute in, and Anthropic’s bet is that owning that runtime is strategically valuable. How that bet plays out — for Bun’s open-source status, for the broader ecosystem, for the dynamic between AI companies and developer infrastructure — is a story that will unfold over the next several years.

The runtime story isn’t complete without a brief look at what’s been happening to bundlers and build tools, because the two stories converge.

Through the 2010s, Webpack (by Tobias Koppers) was the dominant JavaScript bundler. Webpack’s design was extraordinarily flexible — it could be configured to do almost anything — and the cost of that flexibility was extraordinary configuration complexity. A production Webpack config in 2018 typically ran hundreds of lines and required deep knowledge of the tool’s plugin architecture.

Rollup (by Rich Harris, before Svelte) offered a cleaner bundler design focused on ES modules and tree-shaking. It became the standard for library authors but was less commonly used for application bundling.

In 2020, esbuild (by Evan Wallace, then CTO of Figma) shipped. Wallace had written esbuild in Go, and the bundler benchmarked 10–100× faster than Webpack on equivalent workloads. esbuild didn’t try to match Webpack’s feature surface, but its speed was a wake-up call for the rest of the bundler ecosystem.

The same year, Vite (by Evan You, also creator of Vue) shipped. Vite’s design used native ES modules during development (so no bundling was needed at all in dev mode) and used Rollup for production builds. The dev-server speed improvement over Webpack was dramatic — sub-second hot module replacement compared to Webpack’s 10–30 second rebuilds on large projects — and Vite spread quickly through the frontend ecosystem.

By 2024, the standard frontend toolchain had largely consolidated around Vite for application bundling and esbuild or Rollup underneath. Webpack remained widely deployed but was being replaced in new projects. SWC (originally by Donny Wang, written in Rust) had partially replaced Babel for transpilation, with similar speed-up factors. Biome (a fork of the original Rome project) and other Rust-based linters were doing the same for ESLint.

The pattern repeats. Tools written in Go, Rust, and Zig are replacing tools written in JavaScript, on the strength of speed. Bun is the most aggressive consolidator — it absorbs the runtime, the package manager, and the bundler into a single binary — but the broader move toward natively-compiled developer tooling is industry-wide.

One more piece of context, brief because the full picture belongs to a later chapter.

The proliferation of JavaScript runtimes — Node, Deno, Bun, plus the edge runtimes (Cloudflare Workers, Vercel Edge, AWS Lambda@Edge, Fastly Compute@Edge) — created a new compatibility problem. Code written for one runtime didn’t always work on the others, because each runtime exposed slightly different APIs.

The Web-interoperable Runtimes Community Group (WinterCG), established at the W3C in 2022, is the institutional response. WinterCG’s work is to standardize a subset of web APIs (fetch, Request, Response, URL, crypto.subtle, streams, and similar) that all server-side JavaScript runtimes should support. The goal is that code written to the WinterCG subset runs unmodified across runtimes.

This is the same kind of institutional work, in a different surface, as what TC39 does for the language itself and what the W3C and WHATWG do for browser standards. The shape is recognizable. Multiple competing implementations, a shared specification, conformance testing, a slow march toward predictable cross-platform behavior. The work is unglamorous and quietly responsible for whether server-side JavaScript can stay one ecosystem instead of several.

This chapter has been about the runtime story underneath the frontend toolchain. The next chapter is about what the packages shipped through those runtimes look like, and about the structural problem that ten million weekly downloads of left-pad and similar one-line utilities have created.

The runtime story matters because it’s the foundation. The dependency-ecosystem story matters because it’s where most of the cost of modern frontend development actually accumulates — and where, for the platform-first argument the rest of the book builds, the strongest case for shedding layers gets made.

Pick a frontend project you work on. Look at its package.json and identify, for each tool listed:

  1. What’s the tool’s purpose? (Bundler, transpiler, formatter, type checker, linter, test runner, dev server.)
  2. What runtime is it written in? (Native JavaScript, Go, Rust, Zig.)
  3. When was the tool first released? When was its most recent major version?
  4. Is the tool actively maintained? When was its last release?
  5. Does its current functionality overlap with another tool in the same project?

The point is to take stock. A modern frontend project routinely has more tools in its build pipeline than it has source files, and the cost of maintaining that pipeline shows up everywhere — in CI time, in onboarding time, in the time spent diagnosing why a tool that worked yesterday doesn’t work today. Knowing what’s there is the first step in deciding what’s worth keeping.