At a glance

Advanced

async

Script fetches in parallel with HTML parsing and executes as soon as it downloads, potentially before the DOM is complete. The CDN bundle handles this correctly — it defers its own auto-init to DOMContentLoaded internally, so you get the same result as defer at the cost of unpredictable execution order.

  • Fastest possible download start — no parse dependency
  • Safe with AMegMen CDN build (internal DOMContentLoaded guard)
  • Execution order not guaranteed when multiple async scripts exist
  • Slightly more complex mental model
Fallback

No attribute (blocking)

Script is fetched and executed synchronously, blocking HTML parsing. Appropriate only when the script must run before any subsequent HTML is parsed — rare for UI libraries. Placing the tag at the bottom of <body> mitigates the blocking, but defer is strictly better.

  • Guaranteed execution before anything that follows in the HTML
  • Blocks HTML parser — harms FCP and LCP
  • Prevents browser speculative pre-loading of later resources

Using defer (recommended)

Place the <script> tag anywhere — <head> or end of <body> — with the defer attribute. The browser fetches the script in parallel with HTML parsing. Execution happens after parsing completes, just before DOMContentLoaded fires.

Why this works with AMegMen: The CDN build (amegmen.cdn.js) listens for DOMContentLoaded to run auto-init. When the script is deferred, HTML parsing finishes before the script runs, so DOMContentLoaded fires synchronously after the script executes. The menu is always ready when the page becomes interactive — no race condition.
<!-- In <head> — preferred for earliest fetch -->
<link rel="stylesheet" href="dist/styles/amegmen.css" />
<script src="dist/scripts/amegmen.cdn.js" defer></script>

<!-- HTML nav markup (parsed before script executes) -->
<nav data-amegmen aria-label="Primary navigation">
  <ul>
    <li>
      <a href="/products">Products</a>
      <div class="amegmen-panel">
        <ul class="amegmen-panel-group">
          <li><a href="/software">Software</a></li>
          <li><a href="/services">Services</a></li>
        </ul>
      </div>
    </li>
    <li><a href="/pricing">Pricing</a></li>
  </ul>
</nav>

When to use defer: This is the default choice for any production deployment. It maximises parse performance, ensures the DOM is ready before the script runs, and preserves script execution order when multiple deferred scripts are present.

See the Lighthouse demo for a complete example combining defer with critical CSS inlining and stylesheet preloading for a perfect performance score.

Using async

The async attribute also fetches the script in parallel with HTML parsing, but execution happens as soon as the download finishes — the parser is paused briefly at that point. Because the download may complete before the document is fully parsed, a naive script that reads the DOM immediately would fail. The AMegMen CDN build handles this transparently by checking document.readyState and deferring to DOMContentLoaded when necessary.

Internal guard in amegmen.cdn.js: The CDN auto-init code reads document.readyState at execution time. If the DOM is already complete (readyState 'interactive' or 'complete'), init runs immediately. Otherwise it registers a one-time DOMContentLoaded listener. This makes the bundle safe for both async and defer.
<!-- async: script executes as soon as it downloads -->
<link rel="stylesheet" href="dist/styles/amegmen.css" />
<script src="dist/scripts/amegmen.cdn.js" async></script>

<!-- AMegMen's internal DOMContentLoaded guard makes this safe -->
<nav data-amegmen aria-label="Primary navigation">
  <ul>
    <li><a href="/products">Products</a></li>
  </ul>
</nav>

When to use async: Consider async when the mega menu is not above the fold on initial load, or when you're aggregating many independent scripts and execution order between them does not matter. For most single-nav pages, defer is simpler and equally fast.

Caveats: If you also load other scripts that depend on AMegMen (for example, a script that calls new AMegMen(…) programmatically), those scripts must not rely on AMegMen being available in global scope unless they also guard against load order. Use defer in that case — deferred scripts execute in source order, making dependency sequencing predictable.

No attribute — blocking script

Omitting both defer and async causes the browser to pause HTML parsing, fetch the script, execute it, then resume parsing. This is rarely the right choice for a UI library.

<!-- End-of-body placement reduces (but does not eliminate) blocking -->
<!-- Prefer defer instead. -->
<script src="dist/scripts/amegmen.cdn.js"></script>
</body>

The one valid case: If your HTML is assembled server-side with all navigation markup inlined and you have strict control over when every byte of the page is delivered, a blocking script at the very end of <body> works. But a deferred script in <head> starts downloading earlier and imposes less latency overall — it is always the better option.

Strategy comparison

Comparison of defer, async, and blocking script loading strategies
Strategy Blocks HTML parser? Execution timing Order guaranteed? Safe with AMegMen CDN?
defer No After DOM parsed, before DOMContentLoaded Yes — source order Yes
async Briefly on execute As soon as download completes No Yes — internal guard
None (blocking) Yes — fully Immediately during parse Yes — source order Yes (with DOM-ready guard)
None (end of body) Effectively no After all preceding HTML Yes — source order Yes

Module bundlers (webpack, Vite, Rollup, Parcel)

When importing AMegMen through a bundler, the defer/async distinction is handled by the bundler's output configuration. The ESM entry point has no auto-init side effect — you control initialization explicitly.

// Bundler import — no CDN, no global, tree-shakeable
import { AMegMen, autoInit } from 'amegmen';
import 'amegmen/dist/styles/amegmen.css';

// Manual init: called after your framework mounts the DOM
autoInit('[data-amegmen]');

// Or per-element:
const menu = new AMegMen(document.querySelector('#main-nav'), {
  openOnMouseover: true,
});

The package declares "sideEffects": false, so tree-shaking removes any unused exports from the final bundle.

Related demos