JavaScript Script Loading

Last update: 2025-09-19 20:13:39 +0800 +0800
Available translations: 日本語 / 中文(简体) / 中文(繁體)

The HTML parser, script fetcher, and JavaScript engine can work concurrently, but execution constraints differ by script type and attributes. Choose a mode from the dependency graph and readiness contract rather than adding async or defer only as a performance hint.

Understand Parser-Blocking Classic Scripts #

A classic external script without async or defer blocks the parser at its element. The browser fetches and executes the script before parsing subsequent markup:

html
<script src="/library.js"></script>
<script src="/application.js"></script>

The second script can read globals created by the first because document order is execution order. An inline classic script also runs immediately at its parser position, so it can access only the DOM parsed up to that point.

Parser blocking can be intentional when a script must call document.write or establish state before later markup is interpreted. Application code should avoid those dependencies because network latency then delays both DOM construction and first rendering.

Defer Ordered Classic Scripts #

defer applies to external classic scripts. Fetching proceeds in parallel with HTML parsing, then deferred scripts execute in document order after parsing and before DOMContentLoaded:

html
<script defer src="/library.js"></script>
<script defer src="/application.js"></script>

The second script keeps its dependency on the first without blocking the parser. The defer attribute has no effect on an inline classic script; use a module or an explicit readiness callback instead.

A deferred script can still delay DOMContentLoaded through slow fetching or synchronous execution. Stylesheets needed by the script can also postpone its execution because script code may depend on computed style.

Load Independent Scripts Asynchronously #

An external classic script with async executes as soon as it is ready. Fetch does not block parsing, but execution pauses the parser if the response arrives while parsing is still in progress:

html
<script async src="https://analytics.example/sdk.js"></script>

Two async scripts have no execution-order guarantee. They must not depend on one another, on a complete DOM, or on running before DOMContentLoaded. DOMContentLoaded does not wait for them, while the window load event waits for scripts that are part of the document’s outstanding load work.

Use an explicit callback or promise supplied by the integration when later code depends on async initialization. Observing that a global exists on one network does not establish a valid ordering contract.

Load ES Modules as Dependency Graphs #

type="module" changes both loading and execution:

html
<script type="module" src="/main.js"></script>

Module scripts are deferred by default, including inline modules. Static import declarations form a graph whose dependencies are fetched before a module evaluates. Modules use strict mode, have their own top-level scope, and are evaluated once for each resolved module identity rather than once per <script> element.

Module fetches use CORS. A cross-origin module and every cross-origin dependency must return an appropriate Access-Control-Allow-Origin response, and a JavaScript-compatible media type is required. Classic-script behavior is not a fallback for a failed module fetch.

Adding async to a module script lets its graph evaluate as soon as it is ready, removing normal document-order behavior:

html
<script type="module" async src="/independent-widget.js"></script>

Top-level await suspends evaluation of modules that depend on the awaiting module. Do not treat DOM readiness as proof that application initialization has completed; export or publish an initialization promise when consumers must wait for asynchronous module work.

Insert Scripts Dynamically #

A script element created by JavaScript is force-async by default:

js
function loadScript(src) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = src;
    script.addEventListener("load", resolve, { once: true });
    script.addEventListener("error", reject, { once: true });
    document.head.append(script);
  });
}

The returned promise defines a usable completion boundary:

js
await loadScript("/library.js");
await loadScript("/application.js");

A loader can set script.async = false before insertion to place dynamically inserted classic scripts in an ordered queue. Explicit promise sequencing is clearer when failures need to stop dependent loads. Dynamic import() is the corresponding module mechanism and resolves only after the requested module graph evaluates.

Dynamically adding a script after DOMContentLoaded does not fire that event again. Adding load-delaying work before the window load event can postpone load; adding it afterward cannot replay the event.

Select the Correct Readiness Event #

DOMContentLoaded means that parsing is complete and parser-deferred scripts have run. It does not mean that images, async scripts, dynamically started application work, or every custom element is ready:

js
function start() {
  // The parsed DOM is available here.
}

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", start, { once: true });
} else {
  start();
}

The readyState branch matters for modules and dynamically loaded code that may begin after the event. Use load only when layout or behavior requires load-complete resources such as images or frames. Prefer resource-specific promises such as image.decode(), document.fonts.ready, or a module’s own initialization promise when only one dependency matters.

Parser-blocking scripts provide immediate ordering, defer provides ordered post-parse execution, async provides independent opportunistic execution, and modules provide a CORS-checked dependency graph. Matching the mechanism to the dependency contract removes timing assumptions that disappear under a different cache or network.

Comments

Read or comment on GitHub (sign-in required to comment)