Wednesday, 2 March, 2022 UTC


Summary

By definition, Critical Rendering Path is nothing but a collection of steps that make a path between the time your browser gets an HTML page and starts building the webpage for users to visualize. During this process, you need to optimize the steps your browser does.

The Document Object Model

First things first, every webpage has a Document Object Model or a DOM. This is an object-based representation of the entire HTML page, which is in the parsed state. Once the HTML is parsed, a DOM tree is built. The DOM Tree contains objects.
Let’s begin our discussion with a simple piece of HTML Code. The code below has three parts: a header, main section, and a footer. This could be the simplest piece of HTML you can render in your browser. The stylesheet: “style.css” is an external file loaded for formatting the HTML page.
<html>
  <head>
  <link rel="stylesheet" href="style.css">
  <title>101 Javascript Critical Rendering Path</title>
  <body>
    <header>
      <h1>The Rendering Path</h1>
      <p>Every step during the rendering of a HTML page, forms the path.</p>
    </header>
    <main>
         <h1>You need a dom tree</h1>
         <p>Have you ever come across the concepts behind a DOM Tree?</p>
    </main>
    <footer>
         <small>Thank you for reading!</small>
    </footer>
  </body> 
  </head>
</html>

When the above HTML code is parsed by the browser into a DOM Tree Structure, it would appear like this.
Every browser takes some time to parse the above HTML. Clean semantic markup helps in reducing the time required for the browser to parse the HTML.

CSSOM Tree

Next would be the CSSOM tree. This is nothing but the CSS Object Model. Once again, the CSS Object Model is an object-based tree. It takes care of the styles which are present in association with the DOM tree. In general, CSS Styles can be inherited or declared explicitly.
header{
   background-color: white;
   color: black;
}
p{
   font-weight:400;
}
h1{
   font-size:72px;
}
small{
   text-align:left
}
For the above CSS declaration, your CSSOM tree will appear as follows.
On a general note, the CSS is considered as a render-blocking resource.
What is Render-Blocking? Rendering Blocking resource is a component that would not allow the browser to render the entire DOM tree until the given resource is completely loaded. CSS is a rendering blocking resource because you cannot render the tree until the CSS is loaded fully. In the beginning, CSS was served from a single source. Now, developers have come up with different techniques where the CSS files are split and only critical styles are served at an early stage of rendering.

Executing JavaScript

Moving on, JavaScript is a language used to manipulate the Document Object Model. We may think about different use cases, such as a popup, or a carousel that interacts with the DOM. The problem is when these interactions start to take time and reduce the overall loading time of your website. That is why JavaScript code is known as the “Parser Blocking” Resource.
What is Parser Blocking? The browser pauses execution and the construction of the DOM tree when JavaScript code needs to be downloaded, and executed. The moment the JavaScript code is executed, the construction of the DOM tree continues.
“This is why JavaScript is an expensive resource”

Let’s work on some real-time examples

Here is the demo of a simple piece of HTML code, which displays some text and an image. As you can see, the entire page took only around 40ms to be displayed. Even with an image, the time taken for the page to be displayed was less. This is because images are not treated as critical resources, when it comes to making the first paint. Remember, the critical rendering path is all about HTML, CSS and Javascript. Even though we must try and get the images displayed as quickly as possible, they are not going to block the initial rendering.
Now, let’s add css to the piece of code.
As you can see, an extra request is being fired. Even though the time taken for the html file to load is less, the overall time for processing and displaying the page has increased by nearly 10 times. Why?
  1. Plain HTML doesn’t involve much fetching and parsing. But, with a CSS file, a CSSOM (as given above) has to be constructed. Both the HTML DOM and the CSS CSSOM have to be built. This is definitely a time consuming process.
  2. Also, if the script contains JavaScript, the domContentLoaded event will not be fired. Mainly because there is a good chance that the JavaScript would query the CSSOM. This means, the CSS file has to be completely downloaded and parsed, before the execution of any JavaScript.
Note: domContentLoaded is fired when a HTML DOM is completely parsed, and loaded. The event does not wait for images, subframes or even stylesheets to be completely loaded. The only target is for the Document to be loaded. It is possible to add events in the window interface, to see if the DOM is parsed, and loaded or not. Your event listener will be as follows:
window.addEventListener('DOMContentLoaded', (event) => {
    console.log('DOM Content Loaded Event');
});
  1. Even if you choose to replace the external JavaScript file with an inline script, performance would not change drastically. Mainly because the CSSOM needs to be constructed. If you are considering external scripts, we have a proposed solution. That would be to add “async”. This will unblock the parser. More details about async are given later.

Let’s get the Terms Right!

Before we solve the issue, let’s learn the right terms for describing the Critical Rendering Path.
  1. Critical Resource: All resources which can block the rendering of the page.
  2. Critical Path Length: Total number of round trips, required to fetch all the critical resources required for building the page.
  3. Critical Bytes: Total number of bytes transferred as a part of completing and building the page.
In our first example, with a plain HTML script, the above fields would be:
  • 1 Critical Resource
  • 1 Round Trip
  • 192 Bytes of data
In our second example, a plain HTML and external CSS script, the values would be:
  • 2 Critical Resources
  • 2 Round Trip
  • 400 Bytes of data.
If you wish to optimize the Critical Rendering Path in any framework, or plain HTML+CSS+Javascript code, you need to work on the above metrics  and improve them.
  • It is crucial to have as few critical resources as possible. This will impose less work on the CPU, and browser.
  • Next, the dependency between the time taken to download, and the size of the resource is unbreakable. If the resource is big, the critical path length will increase. There will be a bigger number of roundtrips to fetch the resource.
  • Last but certainly not the least, the critical bytes have to be lessor. If possible the bytes to be downloaded has to be converted to a non-critical resource or completely omitted. But, if the bytes should be downloaded as a part of the critical, blocking resource - you need to optimize the transfer by compressing.

How to Cut Down on Render-Blocking Resources CSS?

In any webpage, there will be content before the initial scroll point (fold) and after the scroll point. Content that comes before the fold should be carefully mapped. Make sure that the styles of any content before the fold are loaded. These are critical styles. The rest of the styles can load later on. By doing so, you can boost the speed of your web page. Also, you can get rid of unnecessary render-blocking styles.
Let’s understand Render-Blocking Resources, with a real time example. And, how small changes can make your code much better.

How to Cut Down on Parser Blocking Resources?

Lazy Loading

The key to loading would be “Lazy Loading”. Websites like Amazon and Facebook have so much content. How do they load? As you scroll, the content gets loaded and it does not feel sluggish. This is because they make use of a concept called Lazy Loading. Any media, CSS, JavaScript, Images, and even HTML can be loaded lazily. The amount of content loaded into the page, at a time will be limited. This will improve your Critical Rendering Path Score.
  1. Imagine you have an overlay on your page.
  2. Don’t load the CSS, JavaScript, and HTML of this overlay while loading the page.
  3. Instead, add an event listener to a button, and load the script only when the user clicks the button.
  4. Use a Webpack to accomplish this feature.
Here are a few techniques for implementing Lazy Loading, in Pure JavaScript.
Let’s begin with images, and iFrames. How would you lazy load images which are non-critical? Or, how would you load images that need to be shown only after a user interaction? In these cases, we can make use of the default loading attribute that comes with the <img> and <iframe> tag. When the browser sees this tag, it defers the loading of the iframe, and image. Your syntax for achieving this behavior is as follows:
<img src="image.png" loading="lazy">
<iframe src="tutorial.html" loading="lazy"></iframe>
Note: Lazy loading with loading=lazy should not be used on images within the first visible viewport. It must be applied only to images after the fold.
In browsers where you cannot leverage loading=lazy, you can use IntersectionObserver. This is an interface which allows you to make use of the Intersection Observer API. This API sets a root, and configures ratios for every element’s visibility from the root. When an element is visible in the viewport, it gets loaded. Below is a simple code snippet to help you understand this API.
  1. We observe all elements which have the class “.lazy”.
  2. When elements with class “.lazy” are on the viewport, the intersection ratio drops above zero. If the Intersection Ratio is zero or below zero, the target is not within view. And, nothing needs to be done.
  3. Now, a predefined set of operations will be carried out on these elements.
var intersectionObserver = new IntersectionObserver(function(entries) {
  if (entries[0].intersectionRatio <= 0) return;

  //intersection ratio is above zero
  console.log('Loading Lazy Items');
});
// start observing
intersectionObserver.observe(document.querySelector('.lazy));
Async, Defer, Preload
Note: Async, and Defer are attributes to be used on external scripts.
When you use Async, you will be allowing the browser to do something else, while a JavaScript resource gets downloaded. The downloaded JavaScript resource will be executed, as soon as the download is completed.
  1. JavaScript is asynchronously downloaded.
  2. Execution of All Other scripts will be paused.
  3. DOM rendering will happen simultaneously.
  4. DOM rendering will pause only when the script is executed.
  5. Render blocking JavaScript issues can be solved using the async attribute.
“If a resource is not important, don’t even use async, omit it completely”
Example:
<p>...content before scripts...</p>

<script>
  document.addEventListener('DOMContentLoaded', () => alert("DOM ready!"));
</script>

<script async src=""></script>

<!-- will be visible after the above script is completely executed –>
<p>...content after scripts...</p>
When you use Defer, the JavaScript resource will be downloaded while the HTML rendering happens. However, the execution would not happen as soon as the script is downloaded. Instead, it waits for the HTML file to be completely rendered.
  1. Defer goes a step beyond async
  2. Execution of script happens only after rendering is done.
  3. Defer can make your JavaScript resource absolutely non-render-blocking
Example:
<p>...content before script...</p>

<script defer src=""></script>

<!-- this content will be visible immediately -->
<p>...content after script...</p>
When you use Preload, it is used on files that are not found in the HTML file, but while rendering or parsing a JavaScript or CSS file. With Preload, the browser would download the resource and execution will happen the moment the resource is available.
  • Use Preload wisely. The browser will download the files, even if it is unnecessary on your page.
  • Too many preloads will bring down the speed of your page.
  • The inherent priority of using Preload will be affected when there are too many preloaded files.
  • Only Preload files that are required above the fold contents. This will boost your Google PageSpeed Insight Score.
  • Only Preload files would be discovered when another file is rendered. For example, you add a link to a font face inside a CSS file. The need for the new font face would not be known until the CSS file is parsed. If the font face was downloaded before, it will boost your site speed.
  • Preload is used only with the <Link> tag.
Examples of Preload
<link rel="preload" href="style.css" as="style">
<link rel="preload" href="main.js" as="script">
Write Vanilla JS, avoid 3rd party scripts
Vanilla JS translates to Performance and Accessibility. For a given use case, you don’t need to do everything a 3rd party solution does. The libraries often solve a bunch of problems. Relying on heavy libraries to solve simple problems will cause a performance dent in your code.
A survey done by the WebAIM team figured that nearly a million top websites have frameworks with serious accessibility issues. If you care about your users, go ahead and write in Vanilla JS.
The ask is not to avoid frameworks and write 100% fresh code. The ask is to use helper functions and small-sized plugins.
Caching, and Expiring Content

If assets are used over and over again on your page, it would kill to load them all the time. It is similar to loading the site every time. Caching will help in preventing this cycle. Give expiry data to the content in their headers. Clear the cache and load again, only when they expire.
In order to achieve caching in any piece of Frontend Code, the browser looks for four important header attributes in the HTTP Response:
  1. ETag
  2. Cache-Control
  3. Last-Modified
  4. Expires
Etag is also known as the Entity Tag. This is nothing but a string which validates the cache token. This token can be used by the browser to decide if the request can be satisfied with a copy in the cache or not. If the resource has not changed, the server will return the same hash token. And, the body will be empty. This is when you see the Response Code 304. In case the resource has expired, the body will be populated with the most recent data.
Cache Control allows your application to decide the browser’s caching policy for a given Request. You can choose from four different options: no-cache, no-store, private or public.
Last Modified is quite similar to ETag, but it depends on the Last-Modified Header of the request. The date, and time of modification helps the client decide if a new request needs to be raised or not.
Expires is one of the widely used tags for deciding the validity of data. Always, your application should use data that has not expired. If the header field has an expiration date which is reached, then you can consider the resource as invalid.
In pure JavaScript, you have the freedom to make use of service workers to decide if data needs to be loaded or not. For instance, I have two files: styles.css and script.js. I need to load these files, and I can use a service worker to decide if the resources have to be freshly loaded, or a cache can be used. In the next few days, we have a bigger and a more in-depth post about Progressive Web Pages and Service Workers (stay tuned).
/*Install gets executed when the user launches the single page application for *the first time
*/

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheName).then(function(cache) {
      return cache.addAll(
        [
          'styles.css',
          'script.js'
        ]
      );
    })
  );
});

//When a user performs an operation
document.querySelector('.lazy').addEventListener('click', function(event) {
  event.preventDefault();
  caches.open('lazy_posts’).then(function(cache) {
    fetch('/get-article’).then(function(response) {
      return response;
    }).then(function(urls) {
      cache.addAll(urls);
    });
  });
});

//When there is a network response
self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open('lazy_posts').then(function(cache) {
      return cache.match(event.request).then(function (response) {
        return response 
      });
    })
  );
});
Let’s Talk, in terms of React!
Phew, so much theory! But, glad you reached here. If you have read so far, you would be aware of what critical rendering path is, and why your code plays a major role in the performance of your web application. In the next section, we are going to read about how to handle performance, and make the critical rendering path as short as possible. The framework of choice in our upcoming examples is React. The optimization techniques are broken into two stages. One, before the application gets loaded. And, Stage two is for those who want to optimize after the application has loaded.

Stage One

Let’s build a simple application with a:
  1. Header
  2. Sidebar
  3. Footer
In our application, only when the user is logged in, should the sidebar be seen. Webpack is a great tool that can help with code splitting. If we have enabled code splitting, we can make use of React Lazy loading right from App.js or from the Route component.
So, what is lazy loading? This is a practice where we break our code into logical pieces. The logical pieces are loaded only when the application needs it. As a result, the overall weight of the code remains less.
For instance, if the Sidebar component has to be loaded only if the user logs in, we have a couple of ways for improving the performance of our application. To begin with, we can inject the concept of lazy loading into our Routes. As seen below, the code is broken into three logical chunks. Each chunk will be loaded only when the user chooses a specific route. This means, our DOM does not have to consider Sidarbar code as a part of its “Critical Bytes” during the initial paint. Likewise, we can achieve lazy loading from the parent App.js as well. The choice depends on the developer, and their use case. Yet, let’s see how Lazy Loading is achieved from the parent component:
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
 |- index.js
 |- Header.js
 |- Sidebar.js
 |- Footer.js
 |- loader.js
 |- route.js
|- /node_modules
import { Switch, browserHistory, BrowserRouter as Router, Route} from 'react-router-dom';
const Header = React.lazy( () => import('Header'));
const Footer = React.lazy( () => import(Footer));
const Sidebar = React.lazy( () => import(Sidebar));

const Routes = (props) => {
return isServerAvailable ? (
<Router history={browserHistory}>
           <Switch>
             <Route path="/" exact><Redirect to='/Header’ /></Route>
             <Route path="/sidebar" exact component={props => <Sidebar {...props} />} />
             <Route path="/footer" exact component={props => <Footer {...props} />} />
          </Switch>
</Router>
}
Next, we can render the component conditionally from App.js. In the below piece of code, we would not load Sidebar, if props.user is empty. The rendering of Sidebar is conditional. When the value of user.props changes, React and Webpack will be informed about the change. Hence, the required chunk of script will be loaded. But, during the initial rendering, when props.user is empty, this piece of code does not have to be rendered. If Sidebar is the application’s heavy lifting component, then the initial loading will become much smoother and faster. Why? We wouldn’t load it the first time a user visits the page. Conditional rendering can be used throughout the application.
const Header = React.lazy( () => import('Header'));
const Footer = React.lazy( () => import(Footer));
const Sidebar = React.lazy( () => import(Sidebar));

function App (props) {
return(
<React.Fragment>
   <Header user = {props.user} />
   {props.user ? <Sidebar user = {props.user /> : null}
   <Footer/>
</React.Fragment>
)
}

Talking about conditional rendering, React allows us to load components even with a click of a button. For instance, if the user clicks on login, in our header component, and if the side bar has to be loaded, our code can be modified as below.
//Sidebar.js
export default () => {
  console.log('You can return the Sidebar component here!');
};
import _ from 'lodash';
function buildSidebar() {
   const element = document.createElement('div');
   const button = document.createElement('button');
   button.innerHTML = 'Login';
   element.innerHTML = _.join(['Loading Sidebar', 'webpack'], ' ');
   element.appendChild(button);
   button.onclick = e => import(/* webpackChunkName: "sidebar" */ './sidebar).then(module => {
     const sidebar = module.default;
     sidebar()   
   });

   return element;
 }

document.body.appendChild(buildSidebar());
As a practice, it is important to keep all the routes or components loaded lazily, inside another component called Suspense. The role of Suspense is to provide the application a fallback content, when a lazy loaded component is getting loaded. The fallback content could be anything like a loader, or a message to tell the user why the page is not yet painted. Let’s modify our Routes component, with Suspense.
import React, { Suspense } from 'react';
import { Switch, browserHistory, BrowserRouter as Router, Route} from 'react-router-dom';
Import Loader from ‘./loader.js’
const Header = React.lazy( () => import('Header'));
const Footer = React.lazy( () => import(Footer));
const Sidebar = React.lazy( () => import(Sidebar));

const Routes = (props) => {
return isServerAvailable ? (
<Router history={browserHistory}>
    <Suspense fallback={<Loader trigger={true} />}>
           <Switch>
             <Route path="/" exact><Redirect to='/Header’ /></Route>
             <Route path="/sidebar" exact component={props => <Sidebar {...props} />} />
             <Route path="/footer" exact component={props => <Footer {...props} />} />
      </Switch>
     </Suspense>
</Router>
}

Stage Two

Now that the application is safely loaded, you need to be aware of how React works, to optimize further. React is an interesting framework where you have a Host Tree and Host Instances. The Host Tree is nothing but the DOM. And, the Host Instances represent the nodes. React comes with a React DOM for bridging the gap between the Host environment, and the application. Your smallest item in the React DOM would be a javascript object. These objects are tossed every time a new object is created. Why? The Objects are highly immutable. Whenever a change happens, React updates the Host Tree so that it matches perfectly with the React DOM Tree. This process is widely known as reconciliation.
Using the Right State Management Methods
  • Everytime the React DOM Tree is modified, it forces the browser to reflow. This will have a serious impact on the performance of your application. Reconciliation is used to make sure that the number of re-renders is reduced. Likewise, React uses State management to prevent the re-renders. For example, you have a useState() hook.
  • If you are building a class component, make use of the shouldComponentUpdate() lifecycle method. Try to create classes that always extend a PureComponent. And, the shouldComponentUpdate() hook has to be implemented in the PureComponent. When you do this, a shallow comparison happens between the states and props. Hence, the chances of re-rendering is reduced drastically.
Make use of React.Memo
  • React.Memo takes components and memoizes the props. When a component needs to be re-rendered, a shallow comparison is done. This method is widely used for performance reasons.
function MyComponent(props) {
}
function areEqual(prevProps, nextProps) {
  /*
  return true if passing nextProps to render would return
  the same result as passing prevProps to render,
  otherwise return false
  */
}
export default React.memo(MyComponent, areEqual);
  • If you are building a functional component, make use of useCallback() and useMemo() .
Conclusion
Now that you know what the critical rendering path is, try to analyze the code you write. Every line, every resource and every file included in your project adds to the critical rendering path. Also, think about the fold content of your web page. If you have not leveraged the tips and tricks for improving website performance, now might be the best time to start. Performance is crucial in any web application. As it grows in complexity and size, every millisecond makes a difference. Nevertheless, remember that premature optimization can be catastrophic. Always measure and then attempt to work on optimization.
Happy Coding!