Lean Software
Argue that a website does not need a compile step and the reply arrives within the hour, usually polite and always the same shape. An engineer put it to me recently in a single clause that is hard to improve on: if you own the build step for the browser, you also own the cache invalidation. He added the detail that makes it bite. The browser may well work out that the raw file changed. An optimised import is often left exactly as it was.
It is the best objection in the set, which is why it deserves a full accounting rather than a shrug. The answer is not that the problem goes away. It does not. The answer is that the bill lands on a different desk, and that it is smaller when it gets there.
The Job the Bundler Does Well
Grant the mechanism its due first, because it is genuinely good engineering.
HTTP caching is unforgiving. A stale asset served from a browser cache is a production fault that nobody can recall, and the traditional remedies were all a bit of a palaver: query strings that proxies handled inconsistently, and version numbers that somebody eventually forgot to raise. Fingerprinting solved it properly. Put a hash of the file's contents into its name, and any cache anywhere treats a changed file as a different file, with no ambiguity to negotiate.
That unlocks the pairing every serious deployment uses now: HTML
served with no-cache so it is always revalidated, and
every fingerprinted asset served with
max-age=31536000, immutable, which tells the browser
not to so much as ask for a year. Webpack, Vite and Rollup do all
of this without being asked. If the question is whether the build
step earned its keep here, the honest answer is that it did.
The Part of the Bill Nobody Reads Out
Now the line item underneath.
When a bundler fingerprints a file, the new name has to appear somewhere, and where it appears is inside every file that imports it. Change one small module and its hash changes, which changes the text of each importing chunk, which changes their hashes, which changes the text of anything importing those. Philip Walton, who documented this in detail, puts the mechanism plainly: the hashes appear in the contents of other files, which causes those files to be invalidated when the hashes change.
The practical shape of that is a returning visitor downloading a substantial part of your application because one utility function gained a line. Not the changed file. The chain above it, on their mobile data, for a change they will never see.
And the failure runs in the other direction too, which is the observation that opened this piece. The browser is perfectly capable of noticing that a raw file has changed. What it cannot notice is that an optimised import still points where it always pointed. Correctness here has never depended on the browser being clever. It depends on somebody keeping the names honest.
Walton's recommended remedy is worth quoting for its provenance as much as its content, since it comes from the person describing the problem rather than from anyone selling the alternative: an indirection layer, and of the available ones he calls import maps the simplest and easiest to implement.
One Place Instead of Every Place
An import map is a small block of JSON sitting in the page, and all
it does is answer one question: when this code asks for a module by
name, which file do you actually fetch? Source files go on
importing something called utils. The map, and only
the map, knows that utils currently means
utils.7f3a91.js. Change the file, change that one
line, and every importer is redirected without having been
touched.
<script type="importmap">
{
"imports": {
"utils": "/js/utils.7f3a91.js",
"cart": "/js/cart.a91f04.js"
}
}
</script>
Move the hashes there and the cascade has nowhere to travel. One module changes, its entry in the map changes, and every other file on the machine keeps the name it had and therefore keeps its place in the cache. The team at Banno, who run this in production, report exactly that: the unchanged parents retain their hashes and their cached copies while the map points them at the new child. Since last year the map can carry integrity metadata as well, so the same block that resolves the name also states the expected hash for subresource integrity.
Notice what the work has become. Something still has to compute hashes and write them into a map. That something reads your files and produces a catalogue entry. It does not rewrite your sources, and what it emits is one small document rather than a new edition of everything.
Where Bookkeeping Turns Back into Building
The distinction holds precisely as long as that stays true, and it is worth being strict about the boundary, because it is easy to cross without noticing.
A process that hashes files and updates a map is bookkeeping. A process that rewrites your source into something else and ships that instead is a build, whatever you call the script. If your map generation starts transpiling, or quietly bundles the vendor chunk on the way past, then the build step has not gone anywhere. It has changed its name and kept its habits.
And the limits deserve stating, since the alternative has them. Browsers discover imports by executing the module graph rather than by scanning the HTML, so a preload scanner will not find them for you and the first visit needs help through preloading or a service worker. Build tooling has never treated the map as a first-class output, which means the plumbing is yours. And beyond a few dozen entries, maintaining a map is real work rather than a rounding error. For an application with a genuinely large module graph, the bundler is still doing something for you that you would have to replace deliberately.
The Point
The objection was correct in its premise and wrong in its conclusion. Cache invalidation is neither optional nor free, and it does not disappear when the compile step does.
What happens instead is that it changes owner. It moves out of a factory that reprints the whole edition because one word changed on page four, and into a catalogue where somebody corrects a single card. Both are work. Only one of them scales with the size of everything you did not touch.
A build step compiles. A map remembers. The difference shows up in what your users have to download when you fix a typo.