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
| Mechanism | Fetch | Execution | Order |
|---|---|---|---|
| Classic | Parser pauses | Immediately when encountered | Document order |
async | Alongside parsing | As soon as ready | No document-order guarantee |
defer | Alongside parsing | After parsing, before DOMContentLoaded | Document order |
| Module | Dependency graph is fetched | Deferred by default | Module 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
asyncfor two scripts that depend on one another. - Adding
deferto an inline classic script and expecting it to defer. - Adding
deferto 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.
Related guides and sources
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.
A practical assessment of Core Web Vitals, React rendering, JavaScript loading, responsive images, caching, semantic HTML, and accessible forms. Start the trivia-style player right inside the article.Modern Frontend Performance & Accessibility Mock Test



Discussion
0 comments
Ask a question or share what stood out to you.