HTML Script Tag Placement (Web Dev Performance)

Choose a script’s loading behavior based on what it needs: classic scripts without attributes block HTML parsing, defer waits until parsing ends, async runs when ready, and modules are deferred by default. Check dependencies and measure the page after changes. No single placement rule guarantees faster loading or good Core Web Vitals.

A script can affect how quickly a page appears, when its controls work, and whether other code runs in the right order. Moving every script to the end of the body may seem like an easy fix, but it does not account for dependencies or newer loading options.

I start by asking what each script needs: finished HTML, another script, or neither. Then I choose how it loads and test the result. This guide shows how to do that without trading a small loading gain for broken behavior.

Script loading strategies: what each option does

A script’s loading strategy tells the browser when to fetch and run its JavaScript in relation to HTML parsing. The main choices are a classic script with no attribute, defer, async, and type="module". Each has different effects on parsing and execution order.

Script form While HTML is parsed When it runs Typical fit
Classic, no attribute Parsing pauses while it fetches and runs At its place in the document Code that must run at that exact point
External classic with defer Fetching can happen during parsing After parsing, in document order Code that needs the parsed page or depends on an earlier deferred script
External classic with async Fetching can happen during parsing As soon as it is ready; order is not guaranteed Independent code
Module, type="module" Fetching can happen during parsing Deferred by default; follows module import rules Code organized as JavaScript modules

When should I use defer?

defer lets an external classic script download while the browser parses HTML, then runs it after parsing finishes. Deferred scripts run in document order, and before DOMContentLoaded. It does not change how an inline classic script runs.

For example, if app.js uses a button in the page, defer lets the browser finish parsing the button before the script runs:

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

If several deferred classic scripts depend on one another, list them in the needed order. The browser runs them in document order. Keep in mind that defer applies to external classic scripts, not inline classic scripts.

When should I use async?

async allows an external script to run as soon as it has downloaded. It may run before HTML parsing is complete, and its order relative to other async scripts is not guaranteed. Use it only when the script does not rely on parser-completed markup or another script running first.

<script async src="https://example.com/independent-tool.js"></script>

A common risk is adding async to a script that calls a function provided by another script. If the dependent script runs first, the page may show an error or miss a feature. Do not use async simply because a script is large or seems slow; first check what it depends on.

How do module scripts behave?

A module script uses type="module" and is deferred by default. The browser processes its imports as part of the module graph. Adding defer does not add a further delay or change its default behavior.

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

Use import statements to show dependencies between modules rather than relying on unrelated scripts to run in a certain order. A module can use async when it should run as soon as its module graph is ready. Test that behavior if the module needs page markup.

Key takeaway: Choose based on dependencies and timing needs, not a blanket rule about where scripts belong.

HTML parsing, execution order, and placement

HTML parsing is the browser’s process of reading markup and building the page’s document structure, called the DOM. A parser-blocking script pauses that process while it fetches and runs. Knowing when the DOM is ready helps you avoid delays and missing elements.

What happens with a classic script without attributes?

When the browser reaches a parser-inserted classic script without async, defer, or type="module", it pauses parsing, fetches the script if needed, and executes it. Only then does it continue reading the HTML.

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

If the script searches for an element that appears later in the document, that element may not exist yet. A blocking script in the head can also delay the browser from reaching and displaying later page content. Sometimes code must run at that position, but make that need explicit.

Moving a classic script to the end of <body> can let the browser parse more HTML first. However, it is not a universal fix: it changes when the code runs, and it does not explain dependencies. defer often offers a clearer choice for external classic scripts that need the completed document.

What should I check before choosing?

Make a short inventory before changing markup. For each script, note whether it is inline or external, whether it needs the DOM, and whether another script must run first.

  • Needs a preceding script: Keep the required order with deferred classic scripts, or use module imports where suitable.
  • Needs parsed page elements: Consider defer for an external classic script, or a module script.
  • Independent external code: Consider async, after confirming it can run at any time.
  • Must run at a specific point: Keep blocking behavior only when the timing is necessary.

DOMContentLoaded fires after the document has been parsed and deferred scripts have run. It does not wait for async scripts. If your code depends on a particular script finishing, use a clear dependency rather than assuming this event will provide the order.

Worked examples: tracking code and a page feature

These examples show how to reason from browser behavior; they are not reports of measured client results. In the first, a page has an independent external measurement script. If it does not need the page’s full markup or another script, async may fit. But its execution can interrupt parsing, so test its effect rather than assuming it is free.

In the second, a page has a library followed by a feature script that uses it. With classic scripts, defer on both preserves their document order while allowing parsing to continue:

<script defer src="/js/library.js"></script>
<script defer src="/js/feature.js"></script>

If the feature needs markup, deferred execution also means the browser has finished parsing it. If these are modules, express the dependency with an import instead of relying on separate script tags to coordinate behavior.

Key takeaway: The right placement is the one that gives the code its required DOM and dependencies while avoiding needless parser blocking.

Test script changes and measure page performance

A code change is not proven faster just because an attribute was added. Test that the page still works, inspect when scripts load and run, and compare performance before and after under realistic conditions. Lab tests help diagnose changes; field data shows how real users experience them.

How do I test execution and dependencies?

I use a repeatable check rather than judging the page from one quick reload. First, open the page with the browser’s developer tools and look at the Network panel. The waterfall shows when each script request starts and finishes. The Performance panel can help show main-thread work and interruptions to parsing.

Then test these conditions:

  • Reload with the cache disabled and inspect the request order and timing.
  • Use network and CPU throttling to check slower conditions.
  • Watch the Console for errors, especially missing functions or elements.
  • Confirm that buttons, forms, menus, and other script-based features still work.
  • Repeat the test, since network timing can vary.

Check the page’s behavior as well as its loading trace. A fast request can still run at the wrong time, and a page may appear loaded before a delayed feature is usable.

Which performance measures matter?

Core Web Vitals are user-focused measures of loading, interaction, and layout stability. The “good” thresholds apply at the 75th percentile of page visits: Largest Contentful Paint (LCP) should be 2.5 seconds or less, Interaction to Next Paint (INP) 200 milliseconds or less, and Cumulative Layout Shift (CLS) 0.1 or less.

Measure What it reflects “Good” threshold at the 75th percentile
LCP How quickly the main content appears ≤ 2.5 s
INP How quickly the page responds to user interactions ≤ 200 ms
CLS How much visible content shifts unexpectedly ≤ 0.1

Script placement alone cannot ensure these results. For example, changing loading behavior may alter when a feature runs or how much work happens during an interaction. Compare lab results with field data when available, and investigate changes in the actual measure rather than treating any one script attribute as a performance score.

Key takeaway: Record a baseline, make one focused change, and retest both behavior and performance.

Common questions about script placement

These answers summarize the main loading rules and their practical limits. The right choice still depends on each script’s needs, so check order, DOM access, and user-facing results after making a change.

Should I put every script at the end of the body?

No. Moving a script can let more HTML parse first, but it changes when the code runs and does not manage dependencies. For external classic scripts that need the parsed page, defer is often a suitable option. Choose placement based on behavior, then test it.

Does defer work on inline scripts?

No. The defer attribute has no effect on inline classic scripts. An inline classic script runs when the parser reaches it. If code needs the completed document, use an appropriate external deferred script, a module, or another design that waits for the needed elements.

Do deferred scripts run in order?

External classic scripts with defer run in document order, after parsing and before DOMContentLoaded. This can suit a sequence where one classic script depends on an earlier one. For modules, use imports to express dependencies rather than relying on separate tags for coordination.

Do async scripts run in order?

No. An async external script runs as soon as it is ready, so its timing can vary with download speed. Do not use it for code that depends on another script running first, or on markup that may not yet be parsed.

Should I add defer to a module script?

Usually not for performance or timing. Module scripts are deferred by default, so defer does not add a further change. If a module uses async, its timing differs; test that choice when it depends on page elements or other code.

Does async always make a page faster?

No. It changes when a script runs, but it does not guarantee better user experience or Core Web Vitals. An async script can interrupt parsing when it executes, and incorrect timing can break features. Measure the page and confirm the script is independent before using it.

Does DOMContentLoaded wait for async scripts?

No. It fires after HTML parsing and deferred scripts have completed, but it does not wait for async scripts. If a feature requires an async script, coordinate with that script directly rather than assuming the event means every script has finished.

What is the safest first change for a blocking script?

Check what the script needs before changing it. If it is external, uses the parsed page, and does not need to run during parsing, test defer. Then check the console, feature behavior, request waterfall, and performance measures under throttled conditions. A passing load test alone is not enough.

Conclusion: Treat each script as a separate timing decision. Identify its dependencies, select a loading strategy, and test the result under more than one condition. That process is more reliable than applying one placement rule to every page.

(This article was written by one of our staff writers, James Holloway. Visit our Meet the Team page.)

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *