This article is part 4 of Fly's Google Lighthouse Series, which is a series aimed at helping you achieve better Lighthouse scores through solutions to each Lighthouse audit recommendation. The preceding articles in this series talk about what a Lighthouse score is, how to run an audit for your site, how to serve images in next-gen formats, lazy loading offscreen images and properly sizing images. They can be found here, enjoy!

Running a Google Lighthouse audit against your site may surface up a plethora of issues you didn’t even know were majorly affecting your site’s performance. The long list of issues in bold, red text can seem daunting ... but fear not! It’s actually totally obtainable to achieve a great lighthouse score by making a few tweaks and changes to your app.

In this installment of our Google Lighthouse Series, we’ll be taking a swing at a silent but mighty performance issue from our Lighthouse audit that we probably don’t put much thought into during development ... reducing render-blocking scripts.

What is a render-blocking script?

Did you know that the location of JavaScript in your app actually has a direct impact on how your app is read and displayed by the browser? That's because JavaScript is executed at the exact point where it’s located in the document. So, when the HTML parser encounters a script tag, it pauses construction of the DOM and hands over complete control to the JavaScript engine. When the JavaScript is finished running, the browser picks up where it left off and continues building the DOM. If this is too many words, the pretty picture below should help clear things up.

script-render@2x-1

This image demonstrates the browser's process of constructing a web page. When the browser encounters a script tag, it's first step is to fetch the file (wherever it may be). In the meantime, DOM construction is put on pause until the script is fully fetched and executed. Working similarly to external JavaScript files, executing inline script also block DOM construction, which also delays the initial render.

Whether you use a <script> tag to run JS from an external file or a <script> tag to run inline JS, you’ll discover that both methods behave in a very similar fashion. In both cases, the browser pauses DOM construction and fully executes the script before processing and displaying the rest of the document. There is one major difference, however. In the case of an external JavaScript file, the browser must pause even longer to wait for the script to be fetched from a remote server, disk, cache, disneyland, or wherever it’s located. This can add tens to thousands of milliseconds of delay to the initial loading of our page.

In a nutshell, the placement of JavaScript in your app can and will cause the browser significant delays in processing and rendering the page.

Why is this bad for performance?

As we just learned, the fetching and execution of JavaScript blocks the parsing of our site. By default, all JavaScript is "parser blocking", meaning when the browser encounters a script in the document it must stop building the DOM, hand over control to the JavaScript runtime, and let the script execute before proceeding with DOM construction.

Why does the browser block the parsing of the page when it encounters JavaScript? Simply because it does not know what the script is planning to do on the page ... so it assumes the worst-case scenario and brings everything to a halt.

Some sort of signal to the browser that certain scripts do not need to be executed at the exact point where they're located would allow the browser to continue building the DOM and let the script execute when it is ready ... for example, after the file is fetched from the cache or a remote server. We’ll learn about some of these “signals” in a bit.

Overall, the browser finding JavaScript results in the blocking of the parser, which results in a pause in DOM construction. This "pause" causes several wasted seconds where the user is waiting for your page to display ... and if there's one thing that users hate, it's waiting for a web page to load.

The tricky thing is, some JS resources need to be downloaded and processed before displaying anything else. So, we really just need to leave those be. However, many JavaScript resources are conditional ... meaning they’re not needed to render above-the-fold content or they only apply to specific parts of the page. To produce the fastest, most enjoyable experience for our users and the best possible Lighthouse performance score, it's vital to eliminate any render-blocking scripts that aren’t required to display above-the-fold content.

How to identify a render-blocking script

After running a Lighthouse audit against your site, you may see the recommendation to “reduce render-blocking scripts”, with the information that “script elements are blocking the first paint of your page." and that you should "consider inlining critical scripts and deferring non-critical ones." Click here to learn the importance of a Lighthouse audit and how to run one.

Under this opportunity, you’ll see a list of all scripts that are blocking the parsing of your site and causing delays. You'll also notice the amount of loading time that's being wasted by these scripts because everything else that wants to load and display is put on pause (html, images, css) while the script is being fetched and executed.

Overall, the purpose of reducing render-blocking scripts is to optimize the critical rendering path, which refers to understanding and optimizing the dependency graph between HTML, CSS, and JavaScript. In other words, you should only initally load critical scripts needed to display above-the-fold content and load any other scripts later ... ultimately helping you achieve a faster First Meaningful Paint time (the time it takes for the browser to paint the content that users are interested in).

How to eliminate render-blocking scripts

At this point we’ve learned that JavaScript introduces a lot of new dependencies between the DOM (Document Object Model), the CSSOM (CSS Object Model), and JavaScript execution. We love JavaScript because it allows us to modify just about every aspect of the page: content, styling, and its response to user interaction ... but we don’t love when it stops DOM construction and slows down our apps.

With that being said, we want to work on clearing any unnecessary JS from the critical rendering path so that users can see meaningful content as quickly as possible. The basic idea is to move unnecessary JavaScript out of the way so that this content can quickly render. This is typically done by adding special "instructions" to our JS files which let the browser know when and how to load our JavaScript. Let’s dive deeper into some of these methods...

1, 2, 3, action!

Earlier we talked about the possibility of having some sort of “signals” in our app that would let the browser know if a certain script does not need to be executed at the exact time it’s found so that parsing doesn't need to stop. Here are a few ways we can achieve this...

The most common way that this is done is by adding either the async or the defer attribute to the script HTML elements that call JavaScript resources.

Async

You can actually tell certain scripts to load “asynchronously”, meaning that they will load at the same exact time that everything else is loading and that the loading of other resources and assets will not be paused. The async attribute tells the browser to run the script as soon as possible but the great thing is it does not block parsing while the script is being fetched. So, the DOM continues building the page if it encounters a script tag with the async keyword while it waits for the script to become available for execution, which can significantly improve performance by minimizing the amount of time it takes to render a web page.

async@2x-1

The image above represents the browser constructing a web page when it encounters a JS file that has the async attribute. As you can see, the fetching and downloading of the external script file is not blocking the construction of the DOM. However, take note that the execution of the script is still blocking the construction of the DOM.

Not every single situation will be ideal for asynchronously loading JavaScript simply because you cannot guarantee the order in which async scripts are run. Here are some ideal times to use the async attribute:

  • When loading JavaScript from a third party you should do it asynchronously so in the case that the third-party is temporarily down or is performing slow, your page won't be held up.
  • For script files that are not dependent on other files. For these files, we do not care exactly when the script is executed, so we can use async so that the latency of loading one script does not affect another.

We can mark a script as async like this:

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

Defer

Google recommends to defer JavaScript files that interfere with the loading of above-the-fold content. The defer attribute tells the browser to wait until the HTML document has been fully parsed and then execute the script.

Like an asynchronously loaded script, a deferred script downloads at the same time that the document is parsing, causing no major interruptions. However, it waits until parsing is completely done before actually executing the script.

defer@2x-1

The image above represents the browser constructing a web page when it encounters a JS file that has the defer attribute. Notice, the file is being fetched while the document is parsing, with no unnecessary interruptions. Once the document is fully parsed, then the script is executed. Ideal situations to use this method are:

  • If the script file contains functionality that requires interaction with the DOM.
  • If the script file has a dependency on another file.

In these cases, the DOM must be fully parsed before the script can be executed. Typically, JavaScript files are placed at the end of a page to ensure that everything before it has been fully parsed. However, in situation where, for whatever reason, a particular file needs to be placed elsewhere, the defer attribute should be used ... so long as it has no dependencies.

We can mark our script as defer like this:

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

Asynchronous and deferred execution of scripts are usually more relevant if the <script> element, for some reason, cannot be placed at the very end of the document. AS we know, HTML documents are parsed in order, from the first opening <html> element to the last closing </html> element. So, if an external JavaScript file is placed right before the closing </body> element, it becomes much less appropriate to use the async or defer attribute. By that point, the parser will have already finished the vast majority of the document, so the script wouldn't be blocking much anyway.

Lazy-loading JavaScript

Let's say you have an embedded video halfway down the homepage of your site. Or you have an ad all the way down in the footer of your site. Users would only see these two resources if and when they scroll down the page. These scripts can be a major contributor to slow page speed. Lazy-loading techniques can be used to only load certain resources when necessary (when a user scrolls to them) so it moves them out of the critical rendering path.

You can efficiently load external JavaScript files with a lazy-loading method that uses the Intersection Observer API. IntersectionObserver is a browser API that allows us to easily detect when a certain element enters the browser's viewport. This way, if the given element requires a script in order to be displayed, the script can be loaded and executed then and there, rather than during the initial page render/ DOM parsing. An example using this method can be found here, and easily implemented into your app to lazy-load JavaScript.

Let's sum it up

In this article we learned that when a browser loads a web page, JavaScript resources usually prevent the web page from being displayed until they are downloaded and processed by the browser. We also learned that this is no good for our site's performance. Running a Lighthouse audit against your site will reveal your site's performance score, and if your site contains render blocking scripts, your score will decrease. Luckily, Google will let you know exactly which scripts are causing these delays.

Implement some of the methods above into your own site and watch your Lighthouse score rise! Also, feel free to share your results with us on twitter.

Stay tuned for more interesting ways you can use Fly to boost your Lighthouse score...