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:
- stops HTML parsing
- downloads the script
- 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
- blocks HTML parsing
- executes immediately after download
- runs in the order it appears in the document
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
- downloads in parallel with HTML parsing
- does not block parsing
- executes after the HTML document is fully parsed
- preserves script order
Why it is useful
This is a great choice for scripts that:
- depend on the DOM being available
- should run after parsing completes
- need to preserve execution order with other deferred scripts
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
- downloads in parallel with HTML parsing
- may execute as soon as the file is ready
- can interrupt HTML parsing when execution starts
- does not guarantee execution order
Best use cases
async is best for independent third-party scripts, such as:
- analytics
- ads
- heatmaps
- chat widgets
- tracking pixels
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
- downloads without blocking HTML parsing
- behaves similarly to
defer - supports
importandexport - runs in strict mode automatically
- has module scope, not global scope
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:
- modals
- charts
- editors
- admin panels
- feature-specific code
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:
- very small bootstrapping logic
- critical configuration values
- tiny scripts required before the main bundle
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
- good for independent scripts
- execution order is not guaranteed
- may execute before HTML parsing finishes
defer
- good for application scripts
- execution happens after HTML parsing
- execution order is preserved
Rule of thumb
- use
deferfor app code - use
asyncfor independent third-party code
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?
- all files start downloading in parallel
c.jsexecutes as soon as it finishes downloadinga.jsandb.jsexecute only after HTML parsing completesa.jsalways executes beforeb.js
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:
- load them with
asyncwhen possible - audit whether they are truly necessary
- delay non-critical scripts until after user interaction
- avoid loading too many tags on initial render
- monitor their cost in Lighthouse and DevTools
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:
- before interactive
- after interactive
- lazy onload
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
- the script must block parsing intentionally
- the code must run immediately before the browser continues
Use defer when
- the script powers the app
- the DOM should be fully parsed first
- multiple scripts depend on order
Use async when
- the script is independent
- order does not matter
- it is usually a third-party integration
Use modules when
- you want native ES module support
- you are building modern JavaScript applications
Use dynamic import when
- the feature is not needed on initial load
- you want code splitting and smaller first-load bundles
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:
- pages render sooner
- users see content earlier
- interaction feels faster
- bundle cost becomes easier to control
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.