A Go engineer once told me they could onboard to any Go project in 20 minutes because the toolchain is always the same: go build, go test, go run. JavaScript projects, by contrast, might use Webpack 4, Webpack 5, Vite, Parcel, esbuild, Rollup, Turbopack, or some bespoke Gulp pipeline — and you won't know which until you've read the package.json, the README, three Stack Overflow threads, and the closed GitHub issues.
This is not a new complaint. But I think the framing is usually wrong. People say JavaScript tooling is complex. I'd say it's fragmented by design, and the fragmentation is a deliberate cultural choice that the ecosystem keeps making.
How we got here
JavaScript's browser-first origins meant there was never a blessed build tool. Python has pip and setuptools. Rust has cargo. Java has Maven or Gradle (two is annoying, but two is manageable). JavaScript has... npm, yarn, pnpm, bun, deno — just for package management — and a completely separate category of bundlers, transpilers, and task runners.
Each wave of tooling was a genuine improvement. Grunt replaced raw shell scripts. Gulp replaced Grunt. Browserify made CommonJS work in browsers. Webpack added tree shaking. Babel let us use ES2015 syntax before browsers supported it. Each was solving a real problem.
The issue is the tools never died. Projects that should have been deprecated are still running in CI pipelines at Fortune 500 companies because nobody wants to touch the working Webpack config. The new tools layered on top rather than replacing below.
The config tax
Here's a realistic count of config files in a moderately complex React project in 2025:
package.json — npm scripts, dependencies
tsconfig.json — TypeScript settings
tsconfig.node.json — separate TS config for Vite config file
vite.config.ts — bundler config
eslint.config.js — linting (new flat config format)
.prettierrc — formatting
.prettierignore — what not to format
postcss.config.js — CSS processing
tailwind.config.ts — utility CSS config
vitest.config.ts — test runner
playwright.config.ts — e2e tests
.env — environment variables
.env.example — committed version of above
.gitignore
.dockerignore
.nvmrc — Node version pin
.node-version — alternate Node version pin for other tools
Seventeen config files. I haven't even added Storybook, i18n, or CI configs yet.
A new developer joining this project needs to understand the interactions between all of these before they can be productive. TypeScript's path aliases need to be mirrored in Vite's config. Vitest's globals need to be declared in the TypeScript config. PostCSS runs inside Vite which runs inside Vitest which needs its own config. It's turtles all the way down.
Bun and Deno: genuine improvements
I want to be fair: the ecosystem is actually getting better. Bun is remarkable. A TypeScript file just runs. Tests use the same runtime as production code. The bundler is built-in. The package manager is fast enough that I don't think about it. Node's own improvements (native TypeScript stripping in v22, the --experimental-strip-types flag) are finally addressing the compile-step problem at the runtime level.
Deno 2's "no config by default" philosophy gets at something real: the best configuration is no configuration. But Deno has its own incompatibilities that make adoption painful in practice, and the npm compatibility story has only recently become usable.
The real problem
The JavaScript community has a status signal problem. Using the newest tool shows you're keeping up. Configuring it in novel ways shows expertise. The cognitive load of your setup correlates, in some communities, with how seriously you're taken as an engineer.
This is backwards. The best engineers I know are ruthlessly boring with their toolchains. They use the default settings. They don't customize their linter rules. They don't have opinions about Prettier configuration. They spend their attention on actual problems.
The next time you're configuring something, ask: am I actually solving a problem, or am I just configuring? The answer is usually uncomfortable.