Chapter 76: JavaScript in HTML

The nomodule Attribute for Legacy Fallbacks

The differential serving pattern: shipping modern lightweight ES6+ code to 98% of users while safely supporting legacy browsers.

LEARNING OBJECTIVES
  • Master the mechanics of the Differential Serving (Module / NoModule) architecture.
  • Explain how modern browsers ignore nomodule while legacy browsers ignore type="module".
  • Quantify the performance and bundle size benefits of stripping legacy polyfills for modern clients.
  • Mitigate historical browser quirks (such as the Safari 10.1 double-execution bug).
🎬 INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
🌐
1. Input
Directives & Tags
⚙️
2. Parse
Tokenizer & AST
🌳
3. Layout
Box Model & Flow
🎨
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

📖 The Mental Model & Story (Intuitive Foundation)

Imagine an international airline operating flights between global hubs.

98% of your passengers speak modern international English. 2% of your passengers only speak a rare, ancient regional dialect.

  • The Bad Approach (Monolithic Transpilation): You hire an interpreter who slowly translates every single flight safety instruction into both modern English and the ancient dialect back-to-back. Every announcement takes 10 times longer, annoying and slowing down 98% of the passengers for the sake of the 2%.
  • The Modern Approach (Differential Serving): You hand out high-speed modern digital tablets (type="module") to everyone who can read them. At the back of the plane, you keep a stack of printed legacy translation pamphlets with a sign that says: "Do not take if you have a tablet" (nomodule).
+---------------------------------------------------------------------------------------------------+
|                                  DIFFERENTIAL SERVING ARCHITECTURE                                |
+---------------------------------------------------------------------------------------------------+

                          HTML Contains Both Tags:
      <script type="module" src="modern.js"></script>
      <script nomodule src="legacy-polyfilled.js"></script>

                /                                      \
   [ Modern Browser (Chrome, Safari, Edge) ]    [ Legacy Browser (IE11, Old Mobile) ]
                |                                                  |
   - Recognizes `type="module"` -> EXECUTES                        - Does NOT recognize `type="module"` -> SKIPS
   - Recognizes `nomodule`      -> SKIPS                          - Does NOT recognize `nomodule`      -> EXECUTES
                |                                                  |
   Result: 70 KB modern bundle!                  Result: 320 KB ES5 + polyfills bundle!

Technical Deep Dive & Specifications

The Mechanics of Differential Serving

The Differential Serving Pattern relies on two complementary browser behaviors defined across different specification eras:

  1. Modern User Agents:
    • Parse and execute <script type="module" src="modern.js">.
    • Recognize the nomodule boolean attribute on <script nomodule src="legacy.js"> and refuse to fetch or execute it.
  2. Legacy User Agents (e.g., Internet Explorer 11, older Android WebViews):
    • Encounter <script type="module">. Because type="module" is an unrecognized MIME type in legacy engines, they skip the script entirely.
    • Encounter <script nomodule src="legacy.js">. Because they do not know what the nomodule attribute means, they treat it as an unknown HTML attribute, ignore the attribute, and fetch and execute the legacy script.

The Bundle Size Dividend

By splitting your build pipeline into modern and legacy targets:

Feature Dimension Modern Bundle (type="module") Legacy Bundle (nomodule)
Language Target ES2020+ (Native Classes, Arrow Functions, Optional Chaining ?.) ES5 (Transpiled via Babel to function prototypes)
Async/Await Native V8/JavaScriptCore microtasks Massive regeneratorRuntime state machine boilerplate
Polyfills Included None (Native fetch, Promise, URLSearchParams, Array.flat) Heavy core-js polyfills for 40+ legacy standard library gaps
Average Bundle Size ~85 KB ~380 KB (+347% larger!)
Parse & Compile Time Fast (Direct machine instructions) Slow (Complex wrapped function calls)

The Safari 10.1 Double-Execution Edge Case

When Apple released Safari 10.1 (iOS 10.3), they shipped partial support for ES modules: Safari 10.1 recognized type="module", but did not implement the nomodule attribute. As a result, Safari 10.1 executed the modern module and executed the legacy script, causing double-execution bugs.

To neutralize this in production, web architects inject an inline micro-guard before any scripts:

<!-- Safari 10.1 nomodule double-execution mitigation snippet -->
<script>
  (function() {
    var check = document.createElement('script');
    if (!('noModule' in check) && 'onbeforeload' in check) {
      var support = false;
      document.addEventListener('beforeload', function(e) {
        if (e.target === check) {
          support = true;
        } else if (e.target.hasAttribute('nomodule')) {
          e.preventDefault();
        }
      }, true);
      document.head.appendChild(check);
      check.remove();
    }
  })();
</script>

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL example.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

💻 Interactive Code Playground

Starter Code

Line-by-Line Code Breakdown

  • Lines 14–30 (<script type="module">): Contains modern JavaScript featuring ES2020 optional chaining (??) and class public fields. Modern browsers execute this code and skip the subsequent nomodule tag.
  • Lines 33–42 (<script nomodule>): Contains ES5-compatible markup. Modern browsers ignore this tag completely. Legacy engines (which do not recognize type="module") skip the module and execute this legacy block.

Expected Browser Render Output


SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...
Differential Serving Engine

[ ES Module Bundle ]
Modern Engine OK (Navigator Cores: 8)

(DevTools Console Output):
[Runtime]: Modern ES Module engine activated.
(Notice: "[Runtime]: Legacy ES5 fallback..." is NEVER printed!)

🏋️ Hands-On Exercise

🎯 The Challenge: Implement a Differential Serving Architecture with Polyfill Guard

You are configuring the production deployment for a financial portal that must support both ultra-modern mobile devices and legacy enterprise terminals running outdated browsers.

Instructions:

  1. Configure a <script type="module"> tag pointing to https://cdn.example.com/app.modern.js.
  2. Configure a fallback <script nomodule> tag pointing to https://cdn.example.com/app.legacy.js.
  3. Add defer to the legacy script so it matches the automatic deferred execution timing of the module script.
  4. Ensure both scripts are properly placed in <head> for optimal Preload Scanner discovery.

🏁 Starter Code Sandbox

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

⚠️ Common Pitfalls

  1. Forgetting defer on nomodule Scripts: Module scripts are deferred by default, but classic nomodule scripts are synchronous by default. If you omit defer from <script nomodule src="legacy.js">, legacy browsers will pause HTML parsing, degrading legacy performance unnecessarily.
  2. Serving Both Bundles to Modern Browsers: Forgetting to add nomodule to your legacy bundle will cause modern browsers to download and execute both the modern and legacy scripts, causing duplicate event listeners and state corruption.
  3. Over-polyfilling in Modern Era: Today, native ES modules are supported by >98.5% of global users. If your business analytics show 0% legacy IE11 traffic, you can safely drop nomodule bundles entirely, simplifying your build pipeline.

💡 Pro Tips

  1. Vite and Modern Bundlers: Modern build tools like Vite default to shipping pure ES module bundles. If you need legacy support, Vite provides the official @vitejs/plugin-legacy which automatically generates the differential type="module" and nomodule script tags.
  2. Conditional Polyfill CDNs: As an alternative to shipping dual application bundles, you can use services like Polyfill.io (or self-hosted equivalents) with User-Agent gating to inject only the exact missing polyfills required for that specific browser.

📌 Key Takeaways

  • The Differential Serving Pattern delivers modern lightweight ES6+ code to modern browsers while providing legacy transpiled code to older browsers.
  • Modern browsers execute <script type="module"> and skip <script nomodule>.
  • Legacy browsers skip <script type="module"> (unrecognized MIME type) and execute <script nomodule>.
  • Modern bundles eliminate heavy polyfills (core-js, regeneratorRuntime), reducing payload sizes by up to 75%.
  • Always add defer to <script nomodule> to ensure identical execution timing across modern and legacy paths.
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

How does a modern browser (such as Chrome 120 or Safari 17) handle a script tag declared as <script nomodule src="legacy.js"></script>?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

Why does an older browser like Internet Explorer 11 execute a script marked with nomodule?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

Why is it best practice to add defer to <script nomodule src="legacy.js">?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP