Nobody pauses before writing this in Node, Vite or esbuild:
import { Chart } from "chart.js";
In a browser, the same line hits a basic problem: where exactly is chart.js? import is valid syntax, but the browser has no reason to infer npm, inspect a package.json, choose an export or rummage through node_modules.28
“JavaScript without a build” skips over this boundary rather neatly. ES modules standardized module loading. The browser did not become a package manager.
Julia Evans documented this in 2024 while trying to use several frontend libraries without a build system.1 Two years later, import maps are far less exotic: MDN now marks them as widely available across browsers since March 2023.3 The choices she ran into remain. Somebody must resolve names, convert incompatible formats and decide whether the dependency graph reaches the browser file by file or in an already packaged form.
Browsers want URLs
Browsers are much happier when the address is already there:
import { draw } from "./lib/draw.js";
import confetti from "https://example.com/confetti.js";
The first path is relative to the current module. The second is already a URL. There is little to infer.2
import "chart.js" is different. It is a bare specifier, a name with no ./, ../, / or URL scheme. Node has package-resolution rules and reads fields such as main, exports, type and conditional exports from package.json.8 In a web document, that name needs to be mapped to a URL by an import map.23
<script type="importmap">
{
"imports": {
"chart.js": "/vendor/chart.js"
}
}
</script>
With that table, chart.js stops being a mysterious nickname. scopes can even change resolution for different parts of the graph, including cases where versions need to coexist.3
But the map has to exist before modules that depend on it. And the final answer is still a URL. npm magic did not move into the import keyword; the browser got an explicit lookup table.
2024 got older
In November 2024 Evans still considered import maps new enough that she would lean toward experimental or limited-audience projects.1 That caution made sense for the moment she was writing in. It is no longer a good summary of browser support in 2026.
MDN now marks type="importmap" as Baseline Widely available, generally available since March 2023.3 modulepreload, another useful part of a native-ESM delivery strategy, is marked widely available since September 2023.4
By 2026, worrying first about Safari is mostly outdated. The annoying question comes next: what did the package actually publish for the web?
An npm package, after all, is not a browser format.
Three packages, three lives
Evans falls back to a pleasantly practical bit of detective work: ignore the .js suffix, inspect what the package shipped, and sort it into three families — classic/UMD, ESM or CommonJS.1
A UMD file can already contain the packaging work a browser needs. For Chart.js, Evans picks chart.umd.js, loads it with a plain <script src> and copies the file into her repository.1 Her project now has no local build. The file is still a build artifact produced by the library author.
An ESM package can be served directly when its imports form a graph the browser can resolve. If every import is relative or absolute, the path is straightforward. If the package itself imports nanoid, @atcute/client or other bare specifiers, the import map must cover them too.13
A CommonJS package uses mechanisms such as require() and module.exports. Those are not native web-module syntax. Evans hits this with @atproto/oauth-client-browser and uses esm.sh to obtain an ESM entry the browser can load.1
esm.sh's own documentation ruins the illusion nicely: by default the service transforms the source and bundles when necessary.5
The build is still there. It just runs on somebody else’s machine.
The map becomes a manifest
Two import-map entries are easy to maintain by hand. Twenty subpaths plus a few transitive dependencies are less charming.
That is exactly what JSPM Generator automates. Its API accepts specifiers, traces their dependencies, chooses resolutions for environments such as browser and module, then emits the map.6 Its online tooling can generate HTML with the import map, integrity metadata and preloads already injected.7
Even the most standards-based workflow therefore sneaks resolution tooling back in. It does not have to merge your application into a large generated file; it can simply calculate the graph the browser needs to navigate.
This separation is useful. A bundler often performs several jobs behind one command. An import map pulls one job — resolution — into explicit data.
The cost is that the data becomes part of deployment. Versions, URLs, scopes and possibly integrity hashes need to move with dependency updates.
Graphs cost requests
A browser has no philosophical objection to fifty modules. The network still sees up to fifty resources.
With a plain entry module, the browser fetches it, parses it, discovers its imports and requests those dependencies. Then it discovers the next level. MDN describes rel="modulepreload" as the way to start this work earlier: modules can be fetched, parsed and compiled before normal discovery reaches them.4
Evans saw a very concrete version of the problem. Her import-map setup required dozens of JavaScript files, and her local development server sometimes failed to keep up.1 She notes that the issue went away in production, so this is not evidence that native ESM is generally unreliable. It is a useful reminder that removing the bundle exposes the network graph.
modulepreload can shorten discovery latency. It does not turn forty files into one.
Conversely, esm.sh says it bundles package submodules by default partly to reduce network requests. Its documentation also warns that bundling can duplicate shared modules in some cases or affect side effects and import.meta.url semantics.5
This is not performance versus purity. The decision is duller and more useful: generated chunks, or an explicit HTTP graph?
HTTP enters the workshop
Remove the elaborate dev server and a few stubbornly web-shaped rules reappear.
MDN notes that cross-origin module scripts use CORS and modules must be served with a valid JavaScript MIME type.2 Opening index.html directly over file:// can trigger security errors; a local HTTP server is normally required.2
A bundler often hides these details during development because its dev server already serves files, rewrites paths and emits assets in forms it understands. Remove that tool and those platform rules are visible again.
For a small page, seeing those web constraints again can be a relief: three readable files, a static server and a ten-line import map may be all you need. The win comes from reducing your machinery, not from making the platform's rules disappear.
What actually went away?
The term gets much more useful once you name the step that actually disappeared.
Locally copied UMD: no build in your project and very little graph management, but you consume a build artifact made by the package author.
Native ESM + import map: no mandatory transformation or bundle; files stay separate and resolution becomes explicit. A generator such as JSPM can still prepare the map.67
Import from esm.sh: no local build pipeline, but a remote service performs conversion and sometimes bundling.5
Raw local CommonJS: not a browser workflow. Transform it or choose another distribution.1
The 2026 browser is far more capable than its decade-old ancestor: import, import maps and modulepreload are native tools. It still does not know your npm installation, choose whichever conditional export happens to suit you, or swallow CommonJS, TypeScript and JSX out of goodwill.
The bundler can disappear from your terminal.
Its responsibilities still need an address.