The State of Frontend Builds 2026
How JavaScript teams bundle, install, and ship, and why node_modules is still the slowest part of CI long after the bundler stopped being.
Executive summary
Front-end build tooling moved fast over the last three years. Native-speed bundlers built on esbuild and Rust toolchains turned multi-minute production builds into seconds, and the bundler wars are effectively settled around a small set of fast defaults. The transform step, once the obvious villain in any slow front-end pipeline, is now a rounding error for most projects. Yet the wall-clock time a front-end team actually feels in CI has barely moved, because the bundler was never really the bottleneck.
The slow part is everything around the bundle: a cold dependency install on a fat node_modules tree, a lockfile resolution that re-downloads the same packages on every run, and a runner that starts from scratch each time. When teams optimized the bundler and saw their CI time barely budge, it was because they had sped up the one stage that was already fast and left untouched the install stage that actually dominates the clock.
This report quantifies where a front-end CI minute really goes, why install dominates, and how caching plus a managed runner layer recovers most of it without touching application code. The numbers consistently point the same direction: the highest-leverage optimization in a modern front-end pipeline is no longer the choice of build tool, it is the install path and the runner underneath it.
We look at four things in turn: how fast the leading bundlers actually are, how much time a cold versus cached install costs, how large node_modules has grown across different project shapes, and how a billed front-end CI minute splits across its stages. Each one reinforces the same conclusion from a different angle.
The practical takeaway for front-end leaders is that the build-tool migration most teams already did was the easy, visible win, and the harder, more valuable win is sitting in the install and runner layers that nobody put on a slide. A warm, shared dependency cache on a runner that does not boot from nothing every job is where the wall-clock time that the bundler upgrade promised but never delivered actually shows up.
Wall-clock to produce a production bundle for a mid-size single-page app, cold cache. · Source: Latchkey analysis (modeled)
Median install wall-clock for a typical app with ~1,200 transitive deps. · Source: Latchkey analysis (modeled)
Email me the report
The full report is right here on this page, free. Want the link in your inbox to read later or share, plus new Latchkey reports as they drop? Drop your email and we will send it over.
Sent! Check your inbox for the report link.
No spam. Unsubscribe anytime.
The bundler is fast now, install is the new bottleneck
Native-speed bundlers cut the transform step to seconds, but a cold dependency install still dominates the pipeline. The bundler-speed chart tells the first half of the story: a legacy webpack build that took a minute and a half drops to fifteen seconds on an esbuild-class tool, a better than six-fold improvement on the stage everyone used to complain about. That migration was real and worth doing, and most teams have done it.
The install-cost chart tells the second half, and it is the half that matters now. On a cold cache, a dependency install routinely takes longer than the entire bundle, and the minute-split chart confirms it claims the single largest slice of front-end CI wall-clock. The team that spent a quarter migrating bundlers to save thirty seconds left a sixty-second install untouched, which is why their pipeline felt no faster.
The highest-leverage optimization is therefore no longer the build-tool choice but the install path: a warm, shared dependency cache that survives between runs. The bundler is fast enough; the install is what is slow, and the install is what a caching and runner strategy actually addresses. Reframing the front-end build problem from 'which bundler' to 'how do we avoid re-installing' is the single most useful mental shift in this report.
Install dominates the billed minute
The minute-split chart is the clearest single picture of where front-end CI time actually goes. On a cold cache, dependency install is the largest slice at well over a third of the billed minute, larger than the bundle and transpile step, and comparable to all testing and linting combined. The queue and cold-start overhead takes another meaningful slice before any useful work happens at all.
What stands out is how little of the minute is the thing teams think they are paying for. The actual product work, bundling the app and running its tests, is roughly half the billed time. The rest is overhead: installing dependencies that did not change since the last run, and waiting for a runner to come online. That overhead is almost entirely recoverable, because it is spent re-doing work whose result was identical last time.
This is why caching is the highest-return lever in a front-end pipeline specifically. The single largest cost slice, install, is also the most cacheable, because dependencies change far less often than application code. A pipeline that restores a warm dependency cache instead of installing cold turns the biggest slice of the minute into the smallest, which moves the whole pipeline's cost structure at once.
- Dependency install is the largest single slice of the cold-cache billed minute, larger than bundle and transpile.
- Roughly half the billed minute is overhead, install plus queue and cold start, not product work.
- Install is both the biggest cost and the most cacheable, because dependencies change far less often than code.
Estimated split of billed CI minutes for a typical SPA pipeline, cold cache. · Source: Latchkey analysis (modeled)
node_modules bloat compounds with every workspace
A single SPA carries hundreds of megabytes of node_modules; a monorepo with shared tooling can exceed a gigabyte. The modules-bloat chart traces the curve from a lean static site through a React SPA and a meta-framework app to a monorepo workspace, and the growth is steep, because each step up adds not just direct dependencies but the transitive fan-out they drag in.
That mass is paid twice in CI: once to download and resolve, and once to write to disk on a fresh runner. Both costs scale with the size of the tree, which is why a monorepo's install is so much slower than a single app's, the runner is moving and unpacking gigabytes of files before a single line of the app is built. The on-disk write alone, thousands of tiny files, is a non-trivial chunk of cold install time on the slow ephemeral storage typical of CI runners.
The bloat grows faster than the application itself because transitive dependency fan-out is super-linear in direct dependency count. Adding one direct dependency can pull in dozens of transitive ones, and a few of those can pull in dozens more. This is why node_modules tends to balloon over a project's life even when the team feels it is adding dependencies sparingly, and why the install cost keeps creeping up unless something actively contains it.
Installed on-disk size of node_modules for representative front-end projects, in MB. · Source: Latchkey analysis (modeled)
pnpm and content-addressable stores change the math
Moving from a flat node_modules to a content-addressable store with hard links cuts both cold install time and disk writes meaningfully. The install-cost chart shows pnpm completing a cold install substantially faster than npm, and the gap is largely about not writing the same package to disk many times over. A content-addressable store keeps one copy of each exact package version and links to it, rather than duplicating it across every place in the tree that depends on it.
The win is largest in monorepos, where a single global store is shared across every workspace instead of being duplicated per package. In a flat layout, ten workspaces that each depend on the same library write ten copies; with a shared store they share one. Given how the modules-bloat chart shows monorepo trees dwarfing single-app trees, the workspace case is exactly where the store layout pays off most.
But it only fully pays off when the store itself is cached across CI runs rather than rebuilt each time. A content-addressable store is still cold the first time a runner sees it, and if every job starts from an empty store, the layout advantage is muted. The store and the cache are complementary: the store makes install efficient within a run, and a persistent cache makes the store warm across runs, and you want both.
Cold runners erase your caching wins
Teams that carefully tune install and bundle caching still lose minutes to runners that boot from nothing on every job. The cache strategy can be perfect, but if the cache lives on a fresh ephemeral runner that is discarded after each job, the first job after any gap pays full cold-cache cost, because the runner it lands on has never seen the cache before. The optimization exists in principle but evaporates in practice.
This is the quiet failure mode behind a lot of disappointing caching results. A team enables dependency caching, sees install drop on warm runs, and then notices their average install time barely improved, because a large share of their jobs land on cold runners where the cache is empty. The cache hit rate they assumed they were getting is far lower than reality, dragged down by every cold boot.
A managed runner layer that keeps a warm, shared dependency cache turns the common case into a warm-cache run, which is where the install numbers in this report collapse from over eighty seconds toward single digits. The install-cost chart's managed-cache bar is an order of magnitude below the cold-npm bar precisely because the cache is reliably warm rather than reliably cold. The lesson is that caching and the runner layer are one system: a cache without a warm runner to live on is a cache that mostly misses.
The cheapest front-end minute is a managed Linux minute
Front-end builds almost never need macOS or Windows, yet matrix sprawl and default runner choices quietly push work onto more expensive operating systems. A macOS minute costs 10x a Linux minute on hosted runners, so even a small amount of front-end work that drifts onto macOS, a stray cross-platform matrix leg, a default that nobody revisited, can dominate the bill out of all proportion to the work it does.
There is rarely a good reason for it. Installing dependencies, bundling, transpiling, linting, and running headless unit tests all behave identically on Linux and cost a fraction as much. Unless a job is specifically building or testing a native desktop artifact, which most web front-ends never do, the expensive operating systems are doing cheap, OS-agnostic work at a large premium.
Keeping the heavy install, bundle, and test legs on Linux and moving them to a managed runner captures roughly 69% of the compute cost versus hosted rates, with auto-healing retries that absorb the transient registry and network failures front-end installs are notorious for. The npm registry hiccup that fails an install mid-run is exactly the kind of mechanical failure a self-healing runner recovers in place, so the install that the rest of this report works to speed up also becomes the install that does not spuriously fail and force a full re-run.
- Front-end install, bundle, lint, and headless tests run identically on Linux at a fraction of macOS cost.
- A macOS minute is 10x a Linux minute, so a small amount of drifted work dominates the bill.
- A managed Linux runner with a warm cache and self-healing retries is the cheapest and most reliable home for front-end CI.
The build-tool migration was the easy win, not the big one
Stepping back, the pattern across every chart is the same. The bundler upgrade was visible, well-publicized, and genuinely improved the stage it touched, but that stage was already a minority of the clock. The install path, the node_modules bloat, and the cold-runner problem are less glamorous, rarely make it into a conference talk, and collectively account for far more of the wall-clock time a front-end team actually experiences.
This matters for where teams spend their next increment of effort. Having migrated bundlers and seen modest CI gains, the temptation is to conclude that front-end CI is just slow and move on. The data says otherwise: the large remaining win is in install and runner strategy, and it is available to any team willing to look past the build-tool layer they already optimized.
The encouraging part is that this win requires no application change at all. Switching to a content-addressable store, caching it persistently, keeping work on managed Linux runners with a warm cache, and letting self-healing absorb registry flakes are all infrastructure choices. None of them touch a line of product code, which makes them low-risk, high-return changes of exactly the kind that the bundler migration was supposed to be but, for the wall-clock that matters, never quite was.
Recommendations
Optimize the install path before the build tool
If your bundler is already esbuild-class, further build-tool tuning has little left to give. The install stage is the largest slice of the billed minute on a cold cache, so a persistent, warm dependency cache is the highest-return change available. Reframe the front-end CI problem from "which bundler" to "how do we stop re-installing".
Adopt a content-addressable store, especially in monorepos
Move from a flat node_modules to a content-addressable store with hard links so identical package versions are stored once and linked, not duplicated. The win is largest in monorepo workspaces, where a single shared store replaces a per-package copy, but only fully pays off when the store is cached across runs rather than rebuilt cold each time.
Make sure the cache lives on a warm runner
A dependency cache on a fresh ephemeral runner is empty on every cold boot, so the cache hit rate you assume is far higher than the one you get. Use a managed runner layer that keeps a warm, shared cache so the common case is a warm-cache install that collapses from eighty seconds toward single digits.
Keep front-end CI on managed Linux
Front-end install, bundle, lint, and headless tests run identically on Linux at a fraction of macOS or Windows cost. Audit your matrix for work that drifted onto expensive operating systems and move it back to managed Linux, where a macOS minute costs ten times as much for no benefit on OS-agnostic work.
Let self-healing absorb registry and network flakes
Front-end installs are notorious for transient registry timeouts and network blips that fail a run partway through. A runner that recovers these in place keeps the install you worked to speed up from spuriously failing and forcing a full, expensive re-run, so the reliability win compounds with the speed win.
Outlook
Expect the bundler layer to keep getting faster and to matter less, not more. As Rust and esbuild-class tooling becomes the universal default, the transform step will shrink to near-irrelevance for almost every project, and the conversation about front-end build performance will move decisively to the install and runner layers where the real wall-clock lives. The teams paying attention have already made this shift; the rest will follow as their bundler upgrades fail to deliver the CI speedup they expected.
node_modules bloat is unlikely to reverse on its own. Transitive fan-out is structural, and as front-end apps absorb more capability, the dependency trees will keep growing, which keeps install the dominant cost and keeps caching the dominant remedy. The teams that contain this will be the ones that treat their install path and cache hit rate as monitored metrics rather than set-and-forget configuration.
The architectural endpoint is a front-end pipeline where the bundler is instant, the install is a warm-cache restore measured in single-digit seconds, the work runs on managed Linux at a fraction of hosted cost, and transient registry failures self-heal before anyone notices. None of that requires touching the application, which is what makes it the rare optimization that is both high-return and low-risk, and why the teams that pursue it will spend the next few years with materially faster and cheaper front-end CI than the ones still tuning their bundler.
Methodology
This report combines published JavaScript ecosystem data and bundler documentation with Latchkey's own analysis of front-end CI runner economics. Bundle and install timings are modeled on representative project shapes (a mid-size SPA with roughly 1,200 transitive dependencies) and will vary with hardware, lockfile state, and network. Cost figures derive from published GitHub-hosted per-minute rates and Latchkey managed rates. Where a figure is attributed to a named source it reflects that source. Figures labeled "modeled" are illustrative estimates derived from public pricing and typical pipeline shapes, not a primary survey; figures attributed to a named source reflect that source. Pricing reflects published rates at time of writing and should be verified against current provider pricing.
Sources
- Stack Overflow Developer Survey
- GitHub - Octoverse
- GitHub Actions - billing & pricing
- JetBrains Developer Ecosystem Survey