What does the nomodule attribute on an HTML <script> tag do, and how does pairing it with <script type="module"> let one page serve two different bundles?
answer
- two ignore rules, not a feature check
- the unknown type is half the trick
- exactly one of the pair executes
- a pattern to recognise, then delete
basics
~20 snomodule tells any browser that supports ES modules to skip that script. Paired with a type="module" tag, modern browsers run the module bundle and ignore the fallback, while older engines ignore the unknown module type and run the nomodule one.
solid answer
~50 s`nomodule` is a boolean attribute on a classic `<script>` that means "skip me if you support modules". The pairing works because of two independent behaviours that happen to be complementary: a browser that supports `type="module"` honours `nomodule` and ignores that tag, while a browser that predates modules does not recognise `type="module"` as a script type at all and ignores *that* tag instead. So exactly one of the pair executes, with no feature detection and no script running to decide. Teams used this around 2018 to ship a small modern bundle plus a transpiled, polyfilled legacy bundle from the same HTML. Its practical value has largely gone: every current browser supports modules, so the legacy half is dead weight for essentially all traffic, and a few transition-era browsers had bugs where the `nomodule` file was downloaded anyway. Today the pattern is mainly something to recognise in an existing template and remove.
code
html · 5 lines<!-- runs only where ES modules are supported -->
<script type="module" src="/bundle.modern.js"></script>
<!-- runs only where they are not -->
<script nomodule defer src="/bundle.legacy.js"></script>go deeper
Know that nomodule marks a script to be skipped by browsers that support ES modules, and that it exists to pair a modern bundle with a legacy fallback.
Explain the two complementary rules — modern browsers honour nomodule, old ones ignore the unrecognised type="module" tag — and that no feature detection or script execution is involved.
Judge whether the pattern still earns its place: near-universal module support makes the legacy bundle dead weight, transition-era browsers had bugs that broke the mutual exclusion, and the build cost is doubled for negligible traffic.
Own the browser-support policy behind it: state which engines are actually supported and why, decide whether a differential build is worth the pipeline complexity, and make that decision explicit rather than inherited from an old template.
## The two behaviours that make the pattern work ```html <script type="module" src="/bundle.modern.js"></script> <script nomodule defer src="/bundle.legacy.js"></script> ``` Nothing in this markup detects anything. It relies on two separate rules meeting in the middle: 1. A browser that implements ES modules must honour `nomodule` — it sees the attribute on a classic script and does not execute it. 2. A browser that does not implement modules does not recognise `type="module"` as a supported script type. An unknown `type` means the element is treated as a data block and not executed. It also does not know what `nomodule` means, and unknown attributes are simply ignored, so it runs the classic script normally. The result is a mutually exclusive pair with no runtime cost and no flash of wrong behaviour: exactly one bundle executes per browser. ## What it was for When module support landed across browsers, teams were transpiling everything to ES5 and shipping polyfills to every visitor, including the large majority whose browsers needed none of it. The module/`nomodule` pair let one HTML document serve a lean modern build to capable engines and the old heavyweight build to the rest. Because module scripts defer by default, `defer` was typically written on the `nomodule` tag so the two halves had matching timing. ## Why it has faded Module support has been universal in evergreen browsers since 2018, so the legacy bundle is downloaded by almost nobody while still needing to be built, tested and deployed. Meanwhile the transition era left real bugs: Safari 10.1 supported modules but did not honour `nomodule`, so it fetched and ran the legacy bundle as well — the reason the well-known inline "nomodule fix" snippet existed. Edge 16–18 had related issues. Those quirks mean the pattern was never quite the free lunch it looked like. Seeing it in a template today is usually a sign of an old build configuration rather than a live requirement. Removing it means deleting the legacy tag and the build target that produces it — and, if you support a specific old browser deliberately, being explicit about which one instead of relying on an accidental double-negative. ## Facts to keep straight - `nomodule` is boolean: its presence is the whole signal, and `nomodule="false"` still means "skip me", because the value is never read. - It is meaningful only on classic scripts. Putting it on a `type="module"` tag makes no sense — that tag runs precisely in the browsers that would skip it. - It is a capability signal about module support, not about any other modern syntax. A browser can support modules and still lack a specific newer language feature, so `nomodule` is a coarse proxy, not a guarantee that your modern bundle parses. ## Answering well Explain the two complementary ignore rules — not "the browser checks whether it supports modules", which misses that no check happens — then state plainly that the pattern is historical, and that the honest recommendation for a new page is one module bundle and no fallback.
- Why does an old browser skip the type="module" tag rather than trying to run it as JavaScript?Because the `type` attribute names the script's language, and a browser that does not support the named type must not execute the element — it treats it as an inert data block. Modules were specified with a new `type` value precisely so that pre-existing browsers would silently skip module tags instead of choking on module syntax.
- Would you add the module/nomodule pattern to a page you are building today?No. Every current browser supports modules, so the fallback bundle is built, deployed and downloaded for almost no one while doubling the build matrix. If a specific legacy browser genuinely matters, name it and target it deliberately rather than relying on module support as a proxy for the whole language level you compile to.
saying these in an interview costs you the question
- Says the browser feature-detects module support here
- Thinks both scripts execute in modern browsers
- Believes nomodule works on a type=module tag
- Claims nomodule="false" makes the script run
- Treats the pattern as current best practice