Article start

JavaScript Loading: async, defer, Modules and Code Splitting

Understand when JavaScript downloads and executes, how async differs from defer, why modules are deferred and where dynamic import reduces initial work.

August 1, 2026·1
JavaScript modules and browser loading paths represented as a connected technology workflow
Choosing between a classic script, async, defer, a module and dynamic import changes discovery, ordering and execution.

JavaScript loading performance is not only about file size. The browser must discover a script, fetch it, parse it and execute it, often on the same main thread needed for input and rendering. A small script placed at the wrong point can block HTML parsing, while a large optional feature loaded up front can delay useful interaction.

The correct loading mechanism depends on dependencies and timing. This guide explains classic scripts, async, defer, module scripts and dynamic import(). It also shows how to split optional features without turning the page into dozens of poorly coordinated requests. If execution cost is affecting interaction responsiveness, review the Core Web Vitals guide.

How a classic script blocks parsing

When the HTML parser reaches a classic external script without async, defer or type="module", it normally pauses parsing while the resource is fetched and executed. That behavior preserves a predictable order, but a script in the document head can delay discovery and rendering of content that appears later in the HTML.

This does not mean every script belongs at the bottom of the page. It means the loading behavior should be explicit. An application entry point that needs the parsed DOM is usually better expressed with defer or as a module.

async and defer are not interchangeable

MechanismFetchExecutionOrder
ClassicParser pausesImmediately when encounteredDocument order
asyncAlongside parsingAs soon as readyNo document-order guarantee
deferAlongside parsingAfter parsing, before DOMContentLoadedDocument order
ModuleDependency graph is fetchedDeferred by defaultModule dependency rules

Use async for an independent script that does not rely on another script or the parsed DOM, such as carefully isolated analytics. Use defer when classic scripts depend on document order or on the DOM being parsed. Deferred classic scripts prevent DOMContentLoaded from firing until they finish.

<!-- Preserves order and waits for HTML parsing -->
      <script defer src="/vendor.js"></script>
      <script defer src="/app.js"></script>

      <!-- Suitable only for independent code -->
      <script async src="https://example.com/analytics.js"></script>

The attributes affect fetching and scheduling; they do not move JavaScript execution to a worker. A long script can still block the main thread when it executes.

Module scripts are deferred automatically

A script with type="module" uses JavaScript module semantics. Imports are resolved as a dependency graph, bindings live in module scope, strict mode applies automatically and the module is deferred without adding a defer attribute. Modules are also subject to cross-origin and MIME-type rules, so local testing should use a web server rather than a file: URL.

<script type="module" src="/main.js"></script>
// main.js
      import { mountDashboard } from './dashboard.js';

      mountDashboard(document.querySelector('#app'));

Keep module side effects deliberate. Importing the same module from multiple places does not mean its top-level code should initialize several independent widgets unpredictably. Prefer exported setup functions with clear ownership.

Load optional features with dynamic import

The import() expression returns a Promise that resolves to a module namespace object. It is useful when a feature is needed only after a route or interaction. A rich editor, export tool or chart designer should not necessarily increase the initial cost of a simple dashboard.

const editorButton = document.querySelector('#open-editor');

      editorButton.addEventListener('click', async () => {
        editorButton.disabled = true;
        try {
          const { openEditor } = await import('./editor.js');
          openEditor();
        } finally {
          editorButton.disabled = false;
        }
      });

Provide a loading state if the chunk may take noticeable time, handle failures, and test repeat interaction. Bundlers generally reuse the loaded chunk, but the user experience still needs a clear response during the first load.

Choose chunk boundaries around user journeys

Code splitting works best when a boundary corresponds to something optional: another route, an authenticated administration area, a rarely opened dialog or a heavy visualization. Splitting every small utility can add request overhead and make execution order harder to reason about. Measure total transferred bytes, parse and execution cost, cache reuse and interaction delay.

  • Initial shell: include only code required to show and operate the first useful view.
  • Route chunks: separate areas users may never visit in the session.
  • Interaction chunks: load a heavy optional feature at intent or activation.
  • Shared chunks: avoid duplicating a large dependency across several lazy boundaries.

Use preload hints carefully

modulepreload can help the browser fetch an important module and dependencies earlier. A preload is a promise that the resource is important soon, so excessive hints compete with CSS, fonts and an LCP image. Confirm the waterfall before and after adding one.

The same principle applies to third-party scripts. Each origin can add connection setup, unpredictable server time, main-thread execution and privacy obligations. Load only what has a clear product purpose and an accountable owner.

Common pitfalls

  • Using async for two scripts that depend on one another.
  • Adding defer to an inline classic script and expecting it to defer.
  • Adding defer to a module even though modules are already deferred.
  • Downloading an optional chunk only after a click without showing progress or handling failure.
  • Reducing transfer size but ignoring parse, compile and execution time.
  • Loading several third-party tags before critical first-party content.

Frequently asked questions

Does async run JavaScript on another thread?

No. Fetching proceeds without blocking parsing, but ordinary script execution still uses the main thread and can delay rendering or input.

Do module scripts need defer?

No. Module scripts are deferred automatically. The defer attribute has no additional effect on them.

Is dynamic import always faster?

No. It improves the initial path when the separated code is truly optional. A poorly chosen boundary can delay a common action or duplicate dependencies.

After choosing a script strategy, use the audit checklist to verify its network and main-thread impact. MDN provides detailed references in the script element guide and the JavaScript modules guide.

Test your frontend skills

Apply script ordering, module, rendering, image and accessibility concepts in the linked assessment.

Quick quiz
Web Development

Modern Frontend Performance & Accessibility Mock Test

A practical assessment of Core Web Vitals, React rendering, JavaScript loading, responsive images, caching, semantic HTML, and accessible forms.

15 questions25 min
Inline play

Start the trivia-style player right inside the article.

View details

Discussion

0 comments

Sign in to share a question or add to the discussion.
Start the discussion

Ask a question or share what stood out to you.