Script: async & defer
Optimize Evaluation of JavaScript
To avoid confusion I use the term “script” in this post. But “script” could interchangeably mean JavaScript.
It was 2013 after my graduation, the time when I was a very novice. From Vancouver Public Library I was creating my own profile website for the first time. Into the website, I wanted to implement parallax effects so that that would impress the audience. I found a free, nice plugin, read its document, downloaded the files, wrote the code, and refreshed my web page in the browser. Nothing happened. I reproofed the code, but it seemed there is no wrong of doing. Being frustrated I kept refreshing the page five times. Sure enough, there is no effect yet. Why?
I restarted coding with the example file provided from the plugin. One line by one, I replaced the example code with mine. When I finished, the parallax was there. I could have never minded what was wrong with my old code. But I wanted to get to the bottom of the problem.
The Internet is useful. But when you don’t have any clue what to search, it is dumb. The search keyword “javascript not working” is too ambiguous; I get tons of the results that have nothing to do with my problem. I needed to be more specific, but I didn’t know how to be specific. So I needed to resolve by myself.
With the working code in my hand. I began replacing the code back with my old non-working code one block after another. Every replacement I refreshed the page, checked if the effect was still there. The time had come. I moved my script tag from the bottom of the HTML document to the inside the head tag, the effect was gone. I found out the problem, and the problem was “scripts must be loaded after all other document contents are loaded.”
The assessment “scripts must be loaded after all other document contents are loaded” is wrong, or at least not precise. The more correct statement is “scripts must be loaded after the certain contents—that are to be manipulated by the scripts—are loaded.” In this case the script that manipulates a global header can be loaded right after the DOM of the global header being parsed, <script> right after <header id=“global-header”>.
This self-taught lesson has been engraved in my head. From then on, I have never made the same mistake again. It was worth five hours of painful digging when I looked back.
And yet time has changed.
You can provide async for defer attribute to a script tag, and still can have script work even if the script tags are not written at the end of document.
TL;DR
The async:
- Does’t block parsing HTML
- Evaluates scripts as soon as they are loaded
The defer:
- Does’t block parsing HTML
- Evaluates script after all HTML contents are parsed
This is what other online resources tell, and I don’t have to write this post. So I want to take a closer look at “loading”, “parsing”, and “rendering”, how those are connected to async and defer.
How Browsers Handle HTML
Let’s first take a look at how one HTML document is loaded, parsed, and displayed to the audience’s eyes.
According to Populating the page: how browsers work from MDN, the following steps are how browsers process an HTML document.
- Navigation
- DNS Lookup
- TCP Handshake
- TLS Negotiation
- Response
- Parsing
- Render
- Style
- Layout
- Paint
- Compositing
- Interactivity
To get clearer sense of the process, I want to see one more post. Chrome Developers also provides an amazing blog post on the same issue Inside look at modern web browser (part 3), and it states the process as:
- Parsing
- Construction of a DOM
- Subresource loading
- Style calculation
- Layout
- Paint
- Compositing
- Dividing into layers
- Raster and composite off of the main thread
MDN covers the broader aspects of the mechanism. What I want to focus here is Parsing and Render(Style, Layout, Paint). From the both posts, and the performance review taken later, the process could be summarized as follows:
To validate the process described in the two posts, I created the following HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="style.css">
<script src="head.js"></script>
</head>
<body>
<header>
<a href="#" role="banner">Logo</a>
<nav><ul><li><a href="#">Item</a></li><li><a href="#">Item</a></li></ul></nav>
</header>
<script src="header.js"></script>
<main><h1>Heading</h1><p>Some text...</p></main>
<script>console.log('After <main>');</script>
<footer>© 2022 All rights reserved.</footer>
<script src="app.js"></script>
</body>
</html>
Each external file contains one line of code, which calls console.log().
I run PHP local server, navigated to the document in Chrome, and checked the performance with DevTools. There are two types of threads critical to parsing and rendering, Network and Main. The figure 1 illustrates the overview of how those threads look like.
You can see that the external files are (down)loaded one by one, and every time each loading completes, the browser undergoes certain tasks like parse, script, and render.
From the figure above, the following process is how the browser handles an HTML document:
- Parse HTML (from top to bottom)
- Stop parsing HTML when it encounters stylesheet or script
- Load stylesheet or script
- Recalculate style or evaluates script
- Paint if necessary
- Resume to parse HTML
- Repeat 1–6 to the end of document
The problem of this process is that while script is being loaded and evaluated, the parse of HTML pauses. One exception is the inline script; it is evaluated parallel to Parse.
The figure 3 is a screenshot of DevTools analyzing the performance of the HTML document above.
In main thread Evaluate Script begins right after head.js is loaded, and after that Parse HTML, Recalculate Style, Layout, Update Layer Tree, and Paint continue. Notice that the Main thread waits for head.js being loaded and do nothing until the loading completes.
This intermission—time between the parsing reaches to the script to the completion of loading and evaluating script—is not user-friendly. If there is a massive script inside head tag, the audience won’t see the contents until that the evaluation of that script is finished.
The async and defer come in rescue; they don’t block parsing HTML.
Async
The async, so to speak, is asynchronous.
Here is an updated HTML document with async attribute given to the script tags.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Async</title>
<link rel="stylesheet" href="style.css">
<script src="head.js" async></script>
</head>
<body>
<header>
<a href="#" role="banner">Logo</a>
<nav><ul><li><a href="#">Item</a></li><li><a href="#">Item</a></li></ul></nav>
</header>
<script src="header.js" async></script>
<main><h1>Heading</h1><p>Some text...</p></main>
<script>console.log('After <main>');</script>
<footer>© 2022 All rights reserved.</footer>
<script src="app.js" async></script>
</body>
</html>
I checked the performance again with Chrome DevTools, and the figure 4 shows the overview.
The difference from the non-async document is that the browser postpones scripting, taking HTML parsing as a first priority.
The number of frames to x-axis didn’t change from the previous performance review; it is seventeen. But the browser enabled to reduce one Parse frame. Unlike the previous review, the inline script blocks parsing HTML.
Oxford English Dictionary grasps the concept of async better than any online resource. It defines asynchronous as (Computing & Telecommunications) of or requiring a form of computer control timing protocol in which a specific operation begins upon receipt of an indication (signal) that the preceding operation has been completed.
In this case, a specific operation—evaluation of script—begins upon receipt of an indication—completion of loading scripts.
Evaluation Timing Problem
Since my async document is so short and simple, and each script is so small, the browser loads and evaluates the scrips in order of their appearances in the document.
In practical applications that contain many scripts scattered throughout the document, the script evokes amid of parsing HTML as the scripts are loaded, and this scripting pauses the parsing. I predict that there would be more significant difference of performance in real applications than one I see in my review. Let’s consider the next code.
<script async src="jquery.min.js"></script>
<script async src="jquery.smooth-scroll.min.js"></script>
The async is present to the both script tags. As of the version 3.6, the minified jQuery file is 90kb. jQuery plugin Smooth Scroll is only 4kb. Even if the request for the jQuery is made before the one for Smooth Scroll, given that each task is asynchronous, it might be that loading and evaluating Smooth Scroll completes before that of jQuery. Having completed the tasks for Smooth Scroll, the browser at that moment does not know what jQuery or $ is that it depends on. So you get the error “jQuery is not defined.” Your application corrupts.
When timing or order matters, the async is not the solution.
When to Use async
The async is for:
- Manipulating DOM inside the first view
- Fetching data from the server
- Third-party libraries
- When the timing of evaluating script does’t matter
Defer
Here is an another updated HTML document with defer present to the script tags.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Defer</title>
<link rel="stylesheet" href="style.css">
<script src="head.js" defer></script>
</head>
<body>
<header>
<a href="#" role="banner">Logo</a>
<nav><ul><li><a href="#">Item</a></li><li><a href="#">Item</a></li></ul></nav>
</header>
<script src="header.js" defer></script>
<main><h1>Heading</h1><p>Some text...</p></main>
<script>console.log('After <main>');</script>
<footer>© 2022 All rights reserved.</footer>
<script src="app.js" defer></script>
</body>
</html>
The figure 5 shows the overview of the deferred document:
The defer–as its name suggests—defers script loading and evaluations to the last part of the Main thread, but before DOMContentLoaded event.
This time the number of frames to x-axis down by one from the previous two reviews, sixteen in total. The change from async occurs after the third and last Parse; the browser does not take a Render task.
Just like the previous async performance test, the inline script blocks the Parse, which causes an additional rendering and painting.
Besides the defer enables to decrease the tasks in the thread, one advantage–sometimes disadvantage—of it to async is that defer scripts will be evaluated in order of appearance in the document.
Real World Problem
The defer solves the problems it is supposed to solve, putting off evaluation later. But still there are number of problems that developers need to solve by their own.
One of them is when the script want to manipulate the DOM based on its block width or height. In such case the script must be evaluated after the certain Paint task is done. So instead of adding defer to the script tag, you have to do:
window.addEventListener('load', () => {
doSomethingWithWidth();
});
If the order of script evaluation does not matter, the code above can be called in a script file with async attribute in the middle of the document.
When to Use defer
The defer is for:
- Prioritizing to parse HTML
- Timing the scripts (in order of appearance in the document)
- Making sure that evaluation begin after all DOM has been built
- When there is no way you can add script tags at the end of the HTML document, due to templating.
Summary
The figure 6 is an comparison of performance reviews.
One does not fit all. Simply adding defer to every script is not as nice as non-defer scripts. If one script file is huge, appears early in the document, and does not affects any other scripts, evaluation of which blocks other scripts being loaded and evaluated.
The best practice is to understand the nature of each mechanism, and use them accordance with their benefits.
Resources
- <script>: The Script element | MDN
- Scripts: async, defer | JavaScript.info
- Script Tag - async & defer | Stack Overflow
- HTML <script> defer Attribute | W3Schools