JavaScript Application Structure and Rendering
Rendering JavaScript in Browser
Changing the Page on the Fly
You already know that the browser builds a DOM tree from HTML and a CSSOM tree from CSS. JavaScript's job is to interact with these trees to make web pages dynamic. It can add, remove, or change any element or style on the page after it has loaded.
For example, imagine you have a simple heading in your HTML:
<h1 id="greeting">Hello, world!</h1>
JavaScript can find that <h1> element by its ID and change its text content. This directly manipulates the DOM.
// Find the element with the id 'greeting'
let heading = document.getElementById('greeting');
// Change its text
heading.textContent = 'Welcome!';
Similarly, JavaScript can change an element's style, which modifies the CSSOM. It can alter colors, sizes, positions, and more.
// Change the color of our heading
heading.style.color = 'blue';
When JavaScript changes the DOM or CSSOM, the browser has to do more work. It might need to recalculate the layout of the page (reflow) and repaint the pixels on the screen (repaint). Frequent or complex changes can slow things down, making the page feel sluggish.
The Critical Rendering Path
The browser follows a sequence of steps to get from the initial HTML to pixels on the screen. This sequence is called the critical rendering path. It includes constructing the DOM, constructing the CSSOM, and combining them to create the render tree before painting the page.
JavaScript can interrupt this process. By default, when the browser's HTML parser sees a <script> tag, it pauses everything. It must stop building the DOM, download the script file (if it's external), and then execute the script. Only after all that can it resume parsing the HTML.
A synchronous script blocks HTML parsing. The page won't display any content below the script tag until the script is downloaded and run.
This blocking behavior is a major source of slow page loads. If you have a large JavaScript file in the <head> of your document, the user will stare at a blank white screen until that file is finished. The browser can't build the render tree and paint the page because it's stuck waiting on the script.
Getting Out of the Way
Since blocking the render path is so bad for performance, we need ways to tell the browser to handle scripts differently. HTML provides two attributes for the <script> tag that do just that: async and defer.
asyncanddeferboth tell the browser to download the script file without pausing HTML parsing.
The difference is in when the script is executed.
-
A script with
asyncwill execute as soon as it's finished downloading. This can happen at any time, even in the middle of HTML parsing. This is useful for independent scripts, like analytics, that don't rely on the full DOM being ready. -
A script with
deferwill wait to execute until after the entire HTML document has been parsed. If you have multipledeferscripts, they will execute in the order they appear in the HTML.
<!-- Will download and execute whenever it's ready -->
<script async src="analytics.js"></script>
<!-- Will download in parallel and execute after HTML parsing -->
<script defer src="main-app.js"></script>
| Attribute | Parsing Blocked? | Execution Timing |
|---|---|---|
| (none) | Yes | Immediately after download |
async | No | As soon as it's downloaded |
defer | No | After all HTML is parsed |
Best Practices for Speed
Optimizing JavaScript for rendering is all about minimizing its disruption to the critical rendering path. Here are some key strategies:
Use async or defer: Always use one of these attributes for external scripts unless you have a very specific reason to block rendering. For scripts that modify the page, defer is usually the safest choice.
Minimize JavaScript: Send only the code that's needed for the current page. Techniques like code splitting break up large JavaScript bundles into smaller chunks that can be loaded on demand.
Minify your code: Minification is an automated process that removes unnecessary characters from code, like whitespace and comments, to reduce file size. Smaller files download faster.
Place scripts wisely: If you can't use async or defer, place <script> tags at the end of your <body>. This way, the browser can parse and render the entire page before it encounters the blocking script.
By reducing the amount of JavaScript executed on page load and by scheduling hydration work intelligently, this architecture aims to improve key performance metrics-including First Input Delay (FID) and Time to Interactive (TTI)-without sacrificing rich interactivity.
Let's test your understanding of how JavaScript affects rendering.
What is the primary negative effect of placing a standard <script> tag in the <head> of an HTML document?
You have a script that needs the full DOM to be parsed before it can run safely. Which attribute is the best choice for the <script> tag?
By managing how and when JavaScript runs, you can ensure a much faster and smoother experience for your users.