Back

Script Loading Strategies in JavaScript

Published - May 17, 2026

How we load JavaScript has a direct impact on page speed, rendering, and user experience.

A script that loads at the wrong time can block HTML parsing, delay first paint, slow interaction, or even break code that depends on DOM elements not yet available.

That is why script loading strategy matters so much in frontend development.

Why script loading matters

Browsers parse HTML from top to bottom. When the browser encounters a traditional script tag like this:

<script src="/app.js"></script>

it usually does three things in sequence:

  1. stops HTML parsing
  2. downloads the script
  3. executes the script

Only after that does it continue parsing the rest of the document.

This behavior can hurt performance if the script is large or if the network is slow.

1. Default script loading

Example

<script src="/main.js"></script>

Behavior

When to use it

Use default loading only when the script must run immediately and before the rest of the page continues, which is rare for most modern apps.

2. defer

defer is one of the most useful attributes for external scripts.

Example

<script src="/main.js" defer></script>

Behavior

Why it is useful

This is a great choice for scripts that:

Example with multiple files

<script src="/vendor.js" defer></script>
<script src="/app.js" defer></script>

Even if app.js downloads faster, vendor.js executes first because deferred scripts respect document order.

3. async

async is designed for scripts that do not depend on DOM order or other scripts.

Example

<script src="/analytics.js" async></script>

Behavior

Best use cases

async is best for independent third-party scripts, such as:

Important warning

Do not use async for dependent scripts like this:

<script src="/library.js" async></script>
<script src="/main.js" async></script>

main.js might execute before library.js, which can cause runtime errors.

4. type="module"

Modern browsers support ES modules natively.

Example

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

Behavior

Example

// main.js
import { formatPrice } from './utils.js';
 
console.log(formatPrice(299));

Why modules are useful

Modules improve code organization, reduce global namespace pollution, and make modern build workflows more predictable.

5. Dynamic import

Dynamic import lets us load JavaScript only when it is needed.

Example

button.addEventListener('click', async () => {
  const { openModal } = await import('./modal.js');
  openModal();
});

This is useful for:

Instead of loading everything upfront, we split code and fetch heavy features on demand.

6. Inline scripts

Example

<script>
  console.log('Inline script');
</script>

Inline scripts execute immediately where they appear in the document and block parsing during execution.

They can be useful for:

But overusing inline scripts makes code harder to maintain and can complicate Content Security Policy setups.

async vs defer

This is one of the most common interview and real-world topics.

async

defer

Rule of thumb

Example timeline

Imagine this HTML:

<script src="a.js" defer></script>
<script src="b.js" defer></script>
<script src="c.js" async></script>

What happens?

Resource hints that support script loading

Script loading is not only about async and defer. Browsers also support resource hints.

preload

Use preload when a resource is definitely needed soon.

<link rel="preload" href="/critical.js" as="script">

This tells the browser to fetch the file earlier.

preconnect

Useful for third-party origins.

<link rel="preconnect" href="https://example-cdn.com">

This reduces setup time for DNS, TCP, and TLS connections.

dns-prefetch

<link rel="dns-prefetch" href="//example-cdn.com">

This is a lighter hint that only resolves DNS early.

Loading scripts at the end of body

Before defer became the common solution, developers often put scripts just before the closing </body> tag.

  <script src="/main.js"></script>
</body>

This can still work because most of the DOM is already parsed by then, but defer is usually more explicit and flexible.

Third-party scripts

Third-party scripts are often some of the heaviest resources on a page.

Best practices:

Script loading in modern frameworks

Frameworks like Next.js, Nuxt, and Astro often provide higher-level script loading tools.

For example, Next.js offers strategies such as:

These abstractions exist because loading timing has a real effect on performance.

Even when a framework helps, understanding the browser behavior underneath is still important.

Common mistakes

1. Using async for dependent scripts

This can create race conditions.

2. Loading too much JavaScript on initial render

Even if scripts are deferred, large bundles still slow down interactivity.

3. Ignoring script cost from third parties

A fast app can become slow because of analytics, ads, or widgets.

4. Depending on global variables from scripts that may not be ready

Always know whether the script order is guaranteed.

Choosing the right strategy

Here is a practical guide:

Use default script loading when

Use defer when

Use async when

Use modules when

Use dynamic import when

Final thoughts

JavaScript performance is not only about writing efficient code. It is also about loading the right code at the right time.

If you choose your loading strategy carefully:

For most application scripts, defer is the safest default. For independent third-party scripts, async is usually the right fit. For large optional features, dynamic import is often the best choice.