Improve Page Load Performance With Modern Browser Hints & Best Practices
April 01, 2019·
Table of Contents
- Why Page Load Performance Is Crucial
- Intro To Critical Render & Default Resource Priorities
- Prioritise & Hint Your Way To Better Performance
- Hint - 1 : Preload Critical Resources
- Hint - 2 : Prefetch Non-Critical Resources
- Further Reading
String asked Integer out on a date. Integer responded and said, “you are not my type” - Anonymous
Today we are building more complex web apps, using more frameworks, tools, and content to deliver the experiences our users need. Delivering more capability and functionality to users has often led to more resources being sent from the server to the browser.
These resources are not equal! They are not of the same type and do not have the same privileges!
Image credit - jonsuh.com
- We are not caching as much as we should. According to HTTP Archive, 69% of resources don’t have any caching headers or are cacheable for less than one day. This ****forces users to always fetch these resources every time they visit the website, no matter how often they do so.
- FMP (First Meaningful Paint) and FID (First Input Delay) are some of the most critical user-centric performance metrics to keep an eye on. FMP is a measure of when the primary content of a page is visible, while FID is a measure of how long a user might have to wait before the browser can respond to their first interaction on the web app. They are both severely impacted by too many or too heavy resources required to be loaded by the browser during a navigation, and underline how your users perceive your website/app. The first 6 minutes of this Chrome Dev Summit video sheds more light on these metrics.
The browser is trying to render the page for the user and does so through a process called the critical rendering path. While doing this and discovering sub-resources along the way, the browser has to occasionally halt its work to download and completely process certain types of these sub-resources.
These types of resources are called render blocking, and rightly so, because the browser needs them to be completely processed before it can proceed, given that they can control or alter the course of subsequent work the browser has to do to finally render the page.
The browser won’t display anything until it has the DOM (built incrementally) and the CSSOM (built wholly by waiting to get and parse CSS).
CSS - A Render-blocking Resource
Image credit - sitepoint.com
CSS controls the layout and presentation of a web page. The browser will pause any on-going rendering work when it encounters CSS in a page. This is why it is standard practice to include CSS early (within the head element) in the page, so that they are quickly discovered, downloaded and used to construct the CSSDOM, needed to render the page.
Developers hardly include CSS within the body of web pages, but often forget that CSS “media types” and “media queries” allow us tailor our presentation to specific use cases. Rather than loading one single huge CSS file for your entire app, you could split them into separate files, targeting specific use cases, such as for display versus for print, for desktop versus for mobile, and also for dynamic conditions such as changes in screen orientation, resize events, and more.
When declaring your style sheets, you should be paying more attention to media types and queries, as they greatly impact the critical rendering path performance. They enable you hint the browser, e.g, that this CSS file contains rules for mobile and so does not need to block the render of this page if it is being loaded on a desktop, and vice versa.
Image credit - sitepoint.com
It does this because, well, there’s no telling if the script will use
document.write(...) to alter or recreate the DOM, thereby invalidating on-going page render work.
By default, putting scripts within the document’s HEAD means they’ll be downloaded before the browser gets a chance to display any content. This could be bad for FMP - you generally don’t want to do this.
Putting scripts within the BODY will delay render of any DOM elements after such scripts, and is the reason why we’ve traditionally placed scripts at the very bottom of pages. Well, there are now better ways to hint the browser on how to approach fetching and executing scripts, irrespective of where they are placed within the page.
Default Priorities By default, the browser will download and apply resources as it discovers them within a page, such that the order in which they appear in the source file can serve as a pseudo priority indicator from the developer. This is not guaranteed to work since browsers have internal semantics of how they will prioritise resources for the critical rendering path.
This is 2019, and there are now modern primitives to hint about how we (as developers) want the browser to prioritise fetching and applying resources to our web apps. These hints allow us express a preference, but they are not 100% guaranteed to be applied by the browser. The browser can still wait till after a network congestion to actually load resources using our hints, but understanding and leveraging hints is always a better way to go, and a huge win win.
General Best Practices Before delving into the specifics of prioritisation hints to the browser, it’s more important to discuss adhering to general best practices and patterns that impact page load performance.
For instance, you want to ensure you are only loading resources that are needed for that page or that navigation. So on the home page, as much as possible, you don’t want to be loading all the scripts for your entire app, e.g scripts needed to validate and submit the form in the contact page. That’s a no-no!
Furthermore, even within a given page, you want to actually prioritise what the user will see and interact with first and only later, load resources needed for content below the fold (what they will only see after scrolling). So, if there’s an image gallery below the fold, you really want to not have its images and scripts compete for network and CPU resources with what is needed for the topmost navigation bar, hero images/videos , and critical text or a call to action buttons.
Image credit - samuelvolterra.wordpress.com
Preloading is an age-long trick in web development, but we now no longer have to hide behind libraries or hacks to utilise it in our apps. We can now link-preload critical resources, and the keyword here is critical resources.
<head> ... <link rel="preload" href="main.css" as="style" /> <link rel="preload" href="main.js" as="script" /> ... </head>
Link-preload hints the browser that a critical resource needed for the current navigation should start being downloaded as soon as possible. This is a mandatory instruction to the browser, and can also be set as part of the response header of the web page, allowing the browser to quickly pick up the instruction and handle it without first having to parse the HTML response of the page.
This only downloads the resource into local memory and does not apply it to the page. You will still need to use the regular HTML tag syntax to use the downloaded resource, except this time, it will be executed much faster.
From the code sample above, values for the
as attribute is not limited to just
script since you can preload fonts and media assets as well. Similarly, the
type attribute allows you specify the MIME for the resource, while a
crossorigin attribute provides coverage for CORS, especially when pre-loading resources like fonts.
A simple but more complete example might look like this :
<!DOCTYPE html> <html lang="en"> <head> ... <!-- preload critical resources --> <link rel="preload" href="https://fonts.googleapis.com/css?family=Roboto:400,500" as="style"> <link rel="preload" href="main.css" as="style" /> <link rel="preload" href="./images/hero-banner.jpg" as="image" type="image/jpeg" /> <link rel="preload" href="main.js" as="script" /> <!-- use preloaded resource --> <link rel="stylesheet" href="main.css" /> </head> <body> ... <h3>Welcome to my page</h3> <!-- use preloaded image --> <img src="./images/hero-banner.jpg" alt="Banner Image" /> <!-- use preloaded script --> <script src="./main.js"></script> </body> </html>
Since CSS is generally prioritised over other resources by default, preloading a script will bump up its priority, making it compete for network and CPU resources required to produce the first pixels the user will see. Use this for scripts only when you are sure you need it. Even so, test and measure the outcome. Again, you want to be careful about the number and weight of what you preload, otherwise your FMP and FID could suffer and negatively impact your users.
When using link-preload, you can also use the
media attribute (accepts media types or media queries) to achieve responsive pre-loading, ensuring you are conditionally preloading just what the user needs.
<head> ... <link rel="preload" href="main.css" as="style" media="(max-width: 600px)" /> <link rel="preload" href="./images/hero-banner.jpg" as="image" type="image/jpeg" media="(max-width: 600px)" /> <link rel="stylesheet" href="main.css" media="(max-width: 600px)" /> </head> <body> ... <img src="./images/hero-placeholder.jpg" alt="Banner Image" /> ... <script> // if viewport is 600px or below // set the Image src to ./images/hero-banner.jpg </script> </body>
Link-rel-preload (as it is often called) has good browser support and can be used as a progressive enhancement feature, allowing browsers with no support to ignore it. See more details on caniuse.com
If preload is for fetching critical resources even much earlier, including having to first promote a low priority resource before preloading it, then prefetch is for fetching non-critical resources.
Prefetch does not try to make something critical happen faster. Instead, it tries to make something non-critical happen earlier, but using the browsers idle time. This makes prefetch more suitable for when you have predicted what the user will interact with next. It lets you hint the browser to fetch resources you believe the user will need, either for a later or non-critical interaction in the current page, or for a later page altogether.
When prefetching, you are mostly either doing
dns-prefetch. There’s also
preconnect which we will discuss as a sibling of
<head> ... <!-- fetch these after you are done with fethcing important stuff for this page --> <link rel="prefetch" href="below-the-fold-font.css" as="style" /> <link rel="prefetch" href="below-the-fold-image.jpg" as="image" type="image/jpeg" /> <link rel="prefetch" href="where-users-tend-to-navigate-to-from-this-page.html" /> ... </head>
As seen below, link-prefetch also has good browser support
With link-prefetch, you want to make sure the resources are cacheable, else you risk the browser fetching them again and wasting resources which obviously includes the users data, CPU and maybe battery power!
As always, you want to put the user at the centre of your decision making - from building features all the way to performance tweaks. This is why it makes more sense to only prefetch for users who can afford it. A user on a slow network connection, or one who has turned on “data saver” should be saved the extra cost and bytes from any preemptive fetches meant to power interactions not meant for the current page.
Consequently, Chrome will not do pre-fetching on 2g, it will only
preconnect. You can build something similar and universal (but as a progressive enhancement) into your apps, using the new Network Information API
dns-prefetch DNS prefetching allows the browser to perform DNS lookup early, in the background, even while the browser is busy with other critical things. Hence, by the time the user performs the anticipated or dependent interaction, the DNS lookup roundtrips has already taken place and results in reduced latency. There is no guarantee that DNS resolution will occur ahead of time because of this hint, but browsers can use it to signal their internal pre-resolution algorithms.
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
<head> ... <!-- fasttrack DNS resolution for the intro video --> <link rel="dns-prefetch" href="https://www.youtube.com" /> <link rel="dns-prefetch" href="https://i.ytimg.com" /> <link rel="dns-prefetch" href="https://yt3.ggpht.com" /> <!-- fasttrack DNS resolution for the Twitter widget --> <link rel="dns-prefetch" href="https://twitter.com" /> ... </head> <body> ... <div id="videobox" class="modal"> <div class="modal-content"> <iframe width="100%" height="450" src="https://www.youtube.com/embed/hlGGW0nsWNg" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen > </iframe> </div> </div> <div> <a class="twitter-timeline" data-height="600" data-theme="light" href="https://twitter.com/Andela?ref_src=twsrc%5Etfw" >Tweets by Andela</a > <script src="https://platform.twitter.com/widgets.js"></script> </div> ... </body>
preconnect Idealy, preconnect does not fall under a prefetch hint, but I chose to discuss it as a sibling to dns-prefetch because they are both very similar. Preconnect does the exact same thing as dns-prefetch, and then more.
dns-prefetch does DNS lookup,
preconnect does DNS lookup and then TLS negotioation (required for HTTPS), as well as TCP handshake.
<link rel="dns-prefetch preconnect" href="https://fonts.gstatic.com" crossorigin>
Do bear in mind that while preconnect (like dns-prefetch) is cheap, it can still take valuable CPU time especially in secure connections (impacting FID). This can be bad if the connection isn’t used immediately, as the browser could close it , thereby wasting all the early pre-connection work. Chrome will warn after about 10 seconds of detecting ‘no use’ for a preconnect-ed domain.
Image credit - Steve Souders, speaking at performance.now() 2018
Normal Script Execution
<body> ... <div id="videobox" class="modal"> <div class="modal-content"> <iframe width="100%" height="450" src="https://www.youtube.com/embed/hlGGW0nsWNg" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen > </iframe> </div> </div> <div> <a class="twitter-timeline" data-height="600" data-theme="light" href="https://twitter.com/Andela?ref_src=twsrc%5Etfw" >Tweets by Andela</a > <script src="https://platform.twitter.com/widgets.js"></script> </div> ... </body>
In the example above, we might be using
preconnect to warm up DNS resolution and TLS negotiation for the twitter.com domain, ensuring the widget.js script is loaded quickly. Depending on where the twitter widget is used in the page, the script could be blocking further HTML parsing and rendering, which could hurt percieved performance by the user.
The image below, depicts normal script execution for the code snippet above.
*default script execution* - *Image credit* - *w3reign*.*com*
Since scripts are evaluated after being dowloaded, we can improve their overall CPU handling and parser interference by paying closer attention to where and how we include them within pages, which in turn determines how they get downloaded, evaluated and executed.
Though we can apply link-preload, link-prefetch, dns-prefetch and preconnect to almost any web resource, including scripts, the most powerful and effective ways I have found to control script execution that positively impacts FID and CPU time - is to use
defer You simply add a defer attribute to a script tag
... <script defer src="vendor/lib.js" /></script> <script defer src="vendor/lib2.js" /></script> <script defer src="app.js" /></script> ...
A deferred script will be downloaded in parallel with HTML parsing and DOM rendering, meaning it does not block them. This could be great for FMP.
*deferred script execution* - *Image credit* - *w3reign*.*com*
Deferred scripts only execute after HTML parsing is complete, and the DOM / CSSDOM is ready, making it safe to access or modify the DOM / CSSDOM from within such scripts. Deferred scripts are also guaranteed to execute in their specified order, so
app.js will execute only after
lib2.js, which in turn executes after
Note that ESModules, e.g
<script type="module" src="./main.js"></script>, are deferred by default. This means they are executed like deferred scripts, and so will run in order but only after HTML parsing and DOM building is complete.
defer, you just need to add the async attribute to a script tag to make it load and run asynchronously.
... <script async src="vendor/lib.js" /></script> <script async src="vendor/lib2.js" /></script> <script async src="app.js" /></script> ...
All three scripts in the above example will be downloaded in parallel with HTML parsing and rendering. However, the
async scripts will be executed immediately they are available, but not necessarily in the specified order (lib2.js could finish downloading and execute before lib.js). Recall that this is different for a
defer script, which will only execute after the page has been fully parsed and rendered.
asynchronous script execution - *Image credit* - *w3reign*.*com*
Since async scripts are executed right after being downloaded, it is not uncommon to find them used with page analytics scripts and the likes.
With defer and async, there is now no critical need to keep scripts at the bottom of the page. Placing a deferred or asynchronous script within the head of the page just means they get discovered and downloaded early, but could mean they compete with other critical (pre-loaded) resources you are trying to squeeze through the network and CPU. As with every performance recommendation, you should first measure and investigate, to figure out what works best for your app, your use case, and your users.
- If the content of the page is constructed by client-side scripts, you should investigate what aspects are needed for above-the-fold functionality and probably inline them in the HEAD or as the first scripts at the bottom of the BODY. Similar page construction scripts for below-the-fold UI and functionality can be placed at the bottom of the page, deferred, or loaded asynchronously.
- Scripts loaded with
asynccould further block render if they are already cached. This is because their fetch from cache would be almost instant and then they’ll execute immediately. Might be better to just
deferasynchronous scripts, after all, they are of low priority to your app and their execution can wait.
deferdefers script execution till after the document is completely parsed, but before DOMContentLoaded is fired, making it
loadblocking. What this means is that your page could be registering a longer to completely load, so measure, investigate and iterate as you apply defer and async. That said, perceived performance and user-centric metrics like FMP and FID are more important than final load time for a web page.
Performance is critical when designing and building your web app. The user’s perceived performance of your web app supersedes everything else we care to measure and report, and so, you should be paying critical attention to user-centric metrics like First Meaningful Paint (FMP), and First Input Delay (FID).
preconnect are very powerful progressive enhancement primitives you can use to deliver even better resource loading performance in your web apps, today!
async appropriately to delver optimal script-dependent experiences to your app users.
Written by Charles Odili.
Follow on Twitter