The Critical Rendering Path - Optimizing Script Loading

I recently did a weekend side project to ramp up my Chrome Dev Tools chops by using the Timeline tool to analyze and optimize a website's page load performance, as it relates to JavaScript loading and execution.

I quickly realized, though, that improving my Dev Tools proficiency meant first solidifying my understanding on the browser; specifically, the Critical Rendering Path.

You have to know what you're looking for, in order to analyze it, right? =)

TLDR; Optimizing the Critical Rendering Path involves optimizing the dependencies between HTML, CSS, and JavaScript and reducing the time required to process and turn them into rendered pixels.


Critical Rendering Path

Let's take a high level look at the Critical Rendering Path. (If you want to dig deep into the full path, check out Google's Web Fundamentals.)

The Critical Rendering Path can be separated into two main portions: network transmission, and browser processing.

Network Transmission: Involves the key steps to receive the HTML, CSS, and JavaScript assets.

Render Processing: Involves the processing steps to turn these assets into rendered pixels (first paint).

In order to render a page, the web browser needs to receive the HTML and CSS, and respectively build the Document Object Model (DOM) and the CSS Object Model (CSSOM). Thus, both HTML and CSS are render blocking resources.

In addition to render blocking HTML and CSS, by default (without the async attribute), JavaScript execution is also a render blocking resource as the execution of any included scripts will block the browser parser until the script's execution is completed.

Breaking Down the Critical Rendering Path

When you request a webpage (aka. type in a URL), the website's server sends back the page's HTML. HTML documents not only contain the content of the site, but also references to any external CSS and JavaScript resources.

Building the DOM and CCSOM

After the HTML is received, the browser begins parsing the received bytes to create the Document Object Model DOM.

Bytes → Characters → Tokens → Nodes → Document Object Model

While parsing the HTML, if the browser comes across an external resource reference, the browser will send off a network request to fetch the file. For example, when the browser comes across a link tag <link href="style.css" rel="stylesheet">, it will send off a network request to retrieve the CSS stylesheet .

When the CSS file is received, the same parsing process happens on the CSS file to create the CSS Object Model.

Bytes → Characters → Tokens → Nodes → CSS Object Model

JavaScript Blocks DOM Creation and Page Rendering

JavaScript resources also have the potential to block and delay the render of the page as the execution of any scripts will block the browser parser.

DOM, CSSOM, Parser Blocking Javascript Image: Parser blocking script files

When the browser comes across an inline <script> tag, the browser will pause/block the parser and DOM creation process for the entire time it takes to both fetch the script across the network, and for the JavaScript engine to execute the script.

This parser blocking behaviour is detrimental to user-experience, since a network roundtrip could add thousands of milliseconds of delay to the critical rendering path!

Furthermore, another important behaviour to be aware of is that script execution will only begin after all prior referenced CSS resources have been downloaded and their CCSOM has been constructed.

Basically, script execution is itself blocked until the CCSOM has been constructed.

As you can see, there are many ways that Javascript resources can block the Critical Rendering Path and time to first render.

Optimizing the Critical Rendering Path

Essentially, optimizing the Critical Rendering Path involves minimizing the total amount of time spent performing each step in the render path:

  1. Processing HTML markup and building the DOM tree
  2. Processing CSS markup and building the CSSOM tree
  3. Combining the DOM and CSSOM into a render tree
  4. Executing JavaScript files

Optimizing the CSSOM and the complexity of style calculations has it's own focus. I'll just be looking at optimizing script loading in this article.


Optimizing Script Loading

Okay, that was a lot of background info - let's dig into my take-aways on optimizing script loading.

Baseline - Regular Script Loading:
<script src="first.js"></script>  
<script src="//domain.com/second.js"></script>  

In this situation, the browser will block the parser while it downloads both scripts in parallel. The scripts will then be executed in order of declaration in the HTML.

  • second.js won’t execute until first.js has executed.
  • first.js won’t execute until the CSSOM has been fully parsed, and until any previous scripts are executed.
  • The Critical Rending Path is blocked while all this is happening.

Comments: This is the least optimal method of loading scripts.

Even if we try to prevent blocking the DOM by placing the <script> at the end of the document, right before the </body> tag, the browser won't fetch the script until the whole document has been parsed.

Ideally, we would like to begin fetching these scripts asynchronously, as soon as possible, and then execute them when they're received across the network.

Asynchronously Load Scripts with async

DOM, CSSOM, async JavaScript Critical Render Path Image: async script files

<script src="first.js" async></script>  
<script src="//domain.com/second.js" async></script>  

In this situation, the the async attribute tells the browser that the script is not part of the critical rendering path. The browser downloads both scripts in parallel, but the parser continues to construct the DOM while the resources are being fetched.

As soon as a script file is received across the network, it will be executed in the order it is received. This can introduce dependency issues as the order of declaration is not maintained!

  • The browser parser is not blocked while scripts are fetched.
  • Scripts will execute as soon as it is retrieved, regardless of the order it was defined in the document.
  • Scripts do not wait for the CSSOM to be fully parsed.

Comments: Using the async attribute is the best practice as it allows script references to be placed in the <head>, so that they can be fetched early on and asynchronously.

However, even async scripts still block the browser parser during script execution. So a long-running script could still potentially delay the first paint.

Defer Script Loading with defer

In this situation, the the defer attribute not only tells the browser to prevent the parser from being blocked while the scripts are being fetched, but to also delay script execution until after the document has been parsed (just before domContentLoaded is fired).

All deferred scripts will also maintain their declaration order.

<script src="first.js" defer></script>  
<script src="//domain.com/second.js" defer></script>  

Comments: The best uses for defer are for any non-critical scripts, like analytics, ad networks, social networks, etc. that don't provide any core functionality to your website / app.

Duncan Leung

Front End Developer at iHerb.com

Irvine, CA

Subscribe to Duncan Leung: Javascript and Front End Development

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!