Issues with the <script> Tag
- Blocks browser parsing
- HTML parsing is blocked until the scripts are downloaded and executed
- In the past,
<script>s were placed at the bottom of<body>to mitigate this issue - However, placing
<script>s at the end of<body>doesn't solve the page loading speed issue, as the script still needs to be loaded and executed after parsing the HTML
- Shares scope between
<script>-
<script>s share the same scope, which can lead to scope pollution if not handled carefully.
-
TL;DR
defer and async resolve the parsing-blocking issue, module prevents scope pollution.
- When the browser encounters an inline script or
<script>withoutasync,defer, ortype="module"during parsing,it pauses parsing to fetch and execute the script before continuing.(Render blocking) - Using
moduleallows scripts to be modularized, ensuring that each module has its own scope.
defer
The browser downloads scripts with the defer attribute(hereafter referred to as deferred scripts) in parallel with HTML parsing, allowing the page to continue rendering while the script is being fetched in the background.
As as result, HTML parsing is not blocked while a deferred scripts is being downloaded. Additionally, execution of the deferred scripts are delayed until the page has finished rendering.
The defer attribute only applies to external scripts. if a <script> tag does not have a src attribute, the defer attribute is ignored, and the script executes immediately, potentially blocking HTML parsing.
<div>...Content before the script...</div>
<!-- The DOMContentLoaded event fires after the deferred script is executed -->
<script>
document.addEventListener('DOMContentLoaded', () => {
alert("The `defer` script has executed, and the DOM is now ready!");
});
</script>
<script defer src="bigscript.js"></script>
<!-- This content is visible immediately! -->
<div>...Content after the script...</div>
Key Characteristics of Deferred Scripts
- Deferred scripts never block page rendering.
- Deferred scripts execute after the DOM is fully parsed but before the
DOMContentLoadedevent fires.- The
DOMContentLoadedevent waits for all deferred scripts to finish executing. - In the example above, the alert appears only after the DOM tree is complete and the deferred script has executed.
- The
- Deferred scripts execute in the same order they appear in the HTML document.
- Even if a smaller script downloads faster, it will execute in the order it appears in the document.
async
Scripts with the async attribute (async scripts) operate completely independently from the rest of the page.
- Like
deferscripts,async** scripts are downloaded in the background.**- This means the HTML page continues processing and rendering its content without waiting for the script to finish downloading.
- However, when an
asyncscript executes, HTML parsing is temporarily blocked until the script finishes running.
- Unlike defer,
DOMContentLoadedandasyncscripts do not wait for each other, meaning their execution order is not guaranteed.- If the
asyncscript finishes downloading after the page has fully loaded,DOMContentLoadedmay fire before the script executes. - Conversely, if the
asyncscript is small or cached, it may execute beforeDOMContentLoadedfires.
- If the
-
Other scripts do not wait for async scripts, and async scripts do not wait for other scripts.
- Because of this, if there are multiple
asyncscripts on a page, their execution order is unpredictable. - Scripts execute as soon as they finish downloading, regardless of their order in the document.
- Because of this, if there are multiple
-
asyncscripts are generally used for tasks that do not affect DOM rendering, such as Google Analytics.
type="module"
Declares the script as a JavaScript module. Scripts with the module type always behave as if they have the defer attribute, meaning they load resources in parallel and do not block HTML parsing.
<script type="module">
// Code inside the script is also treated as a module.
</script>
<script type="module" src="function.js"></script>
Since the script is treated as a module, module requests are loaded and executed only once.
<script src="classic.js"></script>
<script src="classic.js"></script>
<!-- classic.js executes multiple times. -->
<script type="module" src="module.mjs"></script>
<script type="module" src="module.mjs"></script>
<script type="module">import './module.mjs';</script>
<!-- module.mjs executes only once. -->
- Module scripts are executed only once, even if they are included multiple times in the HTML or imported from other modules.
- This ensures that resources like modules are loaded only once, preventing redundant executions.
To treat a script as a module, the type="module" attribute must be set. This is necessary for using module code inside the <script> tag or in a file loaded via the src attribute.
- Otherwise, you’ll encounter the error:
Uncaught SyntaxError: Cannot use import statement outside a module. (The import statement cannot be used outside of a module.)
Solving Scope Pollution with module
Code written inside a <script> tag is available globally, which increases the risk of scope pollution.
<script>
const a = "a";
</script>
<script type="module">
console.log(a, c); // "a" "c"
const b = "b";
</script>
<script src="c.js">
// c.js code
// const c = "c";
// The variable c is initialized with "c".
</script>
<script>
console.log(a); // "a"
console.log(c); // "c"
console.log(b); // Uncaught ReferenceError: b is not defined
</script>
- Even when dividing the code across multiple
<script>tags or external files, all scripts share the same global scope, which poses a risk for errors and unintended side effects. - By using modules, each script has its own isolated scope, making it safer and easier to manage code.
- Code declared inside a module can only be accessed through
exportandimportstatements.
- Code declared inside a module can only be accessed through
- In the example above, the script with the
moduletype is handled asynchronously, meaning it is executed last. - Note: When using the
srcattribute in a<script>tag, inline scripts will not be executed.

Top comments (0)