Menu Search
Jump to the content X X
Smashing Conf San Francisco

We use ad-blockers as well, you know. We gotta keep those servers running though. Did you know that we publish useful books and run friendly conferences — crafted for pros like yourself? E.g. upcoming SmashingConf San Francisco, dedicated to smart front-end techniques and design patterns.

Optimizing Critical-Path Performance With Express Server And Handlebars

Recently, I’ve been working on an isomorphic React website. This website was developed using React, running on an Express server. Everything was going well, but I still wasn’t satisfied with a load-blocking CSS bundle. So, I started to think about options for how to implement the critical-path technique on an Express server.

This article contains my notes about installing and configuring a critical-path performance optimization using Express and Handlebars.

Prerequisites Link

Throughout this article, I’ll be using Node.js and Express. Familiarity with them will help you understand the examples.

Lifecycle of Critical Path Performance Optimization1
Lifecycle of critical-path performance optimization (View large version2)

tl;dr Link

I have prepared a repository3 with a quick and easy demo.

The Basics Link

Critical-path optimization is a technique that eliminates render-blocking CSS. This technique can dramatically increase the speed at which a website loads. The aim of this method is to get rid of the time that a user waits for a CSS bundle to load. Once the bundle has loaded, the browser saves it to its cache, and any subsequent reloads are served from the cache. Based on this, our objectives are the following:

  • Distinguish between the first and second (and nth) loads.
  • On the first load, load the CSS bundle asynchronously, and attach a load event listener so that we can find out when the bundle is ready to be served.
  • While the bundle is being loaded, inline some small critical CSS, to make the user experience as similar as possible to the end result.
  • Once the event listener reports that the CSS bundle is ready, remove the inline CSS and serve the bundle.
  • Ensure that other sources (JavaScript bundles, etc.) are not blocking the rendering.

Detecting The First Load Link

To detect the first load, we are going to use a cookie. If a cookie has not been set, then that means it’s the first load. Otherwise, it will be the second or nth load.

Loading The CSS Bundle Asynchronously Link

To start asynchronously downloading the CSS bundle, we are going to use a simple technique involving an invalid media attribute value. Setting the media attribute to an invalid value will cause the CSS bundle to download asynchronously but will not apply any styles until the media attribute has been set to a valid value. In other words, in order to apply styles from the CSS bundle, we will change the media attribute to a valid value once the bundle has loaded.

Critical CSS Vs. CSS Bundle Link

We will keep critical styles inline in the markup only during the downloading of the CSS bundle. Once the bundle has loaded, that critical CSS will be removed from the markup. To do this, we will also create some critical JavaScript, which will basically be a little JavaScript handler.

Lifecycle Link

To sum up, here is simple schema of our lifecycle:

Lifecycle of critical path performance optimization4
Lifecycle of critical-path performance optimization (View large version205)

Going Isomorphic Link

Now that you know more about this technique, imagine it in combination with an isomorphic JavaScript application. Isomorphic JavaScript, also called universal JavaScript, simply means that an application written in JavaScript is able to run and generate HTML markup on the server. If you are curious, read more about React’s approach regarding ReactDOM.renderToString6 and ReactDOM.renderToStaticMarkup7.

You might still be wondering why we need to generate HTML on the server. Well, think about the first load. When using client-side-only code, our visitors will have to wait for the JavaScript bundle. While the JavaScript bundle is being loaded, visitors will see a blank page or a preloader. I believe that the goal of front-end developers should be to minimize such scenarios. With isomorphic code, it’s different. Instead of a blank page and preloader, visitors will see the generated markup, even without the JavaScript bundle. Of course, the CSS bundle will also take some time to load, and without it our visitors will see only unstyled markup. Thankfully, using critical-path performance optimization, this is easy to solve.

Isomorphic JavaScript application8
Isomorphic JavaScript application (View large version9)

Preparing The Environment Link

Express Link

Express is a minimal and flexible Node.js web application framework.

First, install all of the required packages: express, express-handlebars and cookie-parser. express-handlebars is a Handlebars views engine for Express, and cookie-parser will help us with cookies later.

npm install express express-handlebars cookie-parser --save-dev

Create a server.js file with imports of those packages. We will also use the path package later, which is part of Node.js.

import express from 'express';
import expressHandlebars from 'express-handlebars';
import cookieParser from 'cookie-parser';
import path from 'path';

Create the Express application:

var app = express();

Mount cookie-parser:

app.use(cookieParser());

Our CSS bundle will be available at /assets/css/bundle.css. To serve static files from Express, we have to set the path name of the directory where our static files are. This can be done using the built-in middleware function express.static. Our files will be in a directory named build; so, the local file at /build/assets/css/bundle.css will be served by the browser at /assets/css/bundle.css.

app.use(express.static('build'));

For the purpose of this demonstration, setting up a single HTTP GET route (/) will suffice:

// Register simple HTTP GET route for /
app.get('/', function(req, res){
  // Send status 200 and render content. Content, in this case, is a non-existent template. For me, rendering the layout is important.
  res.status(200).render('content');
});

And let’s bind Express to listen on port 3000:

// Set the server port to 3000, and log the message when the server is ready.
app.listen(3000, function(){
  console.log('Local server is listening…');
});

Babel And ES2016 Link

Given the ECMAScript 2016 (or ES2016) syntax, we are going to install Babel and its presets. Babel is a JavaScript compiler that enables us to use next-generation JavaScript today. Babel presets are just a specific Babel transformation logic extracted into smaller groups of plugins (or presets). Our demo requires React and ES2015 presets.

npm install babel-core babel-preset-es2015 babel-preset-react --save-dev

Now, create a .babelrc file with the following code. This is where we’re essentially saying, “Hey Babel, use these presets”:

{
  "presets": [
    "es2015",
    "react"
  ]
}

As Babel’s documentation says10, to handle ES2016 syntax, Babel requires a babel-core/register hook at the entry point of the application. Otherwise, it will throw an error. Let’s create entry.js:

require("babel-core/register");
require('./server.js');

Now, test the configuration:

$ node entry.js

Your terminal should log this message:

Local server is listening…

However, if you navigate your browser to http://localhost:3000/11, you will get this error:

Error: No default engine was specified and no extension was provided.

This simply means that Express doesn’t know what or how to render. We’ll get rid of this error in the next section.

Handlebars Link

Handlebars12 is referred to as “minimal templating on steroids.” Let’s set it up. Open server.js:

// register new template engine
// first parameter = file extension
// second parameter = callback = expressHandlebars
// defaultLayout is the name of default layout located in layoutsDir.
app.engine('handlebars', expressHandlebars(
{
  defaultLayout: 'main',
  layoutsDir:    path.join(__dirname, 'views/layouts'),
  partialsDir: path.join(__dirname, 'views/partials')
}
));
// register new view engine
app.set('view engine', 'handlebars');

Create the directories views/layouts and views/partials. In views/layouts, create a file named main.handlebars, and insert the following HTML. This will be our main layout.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Critical-Path Performance Optimization</title>
    <link rel="stylesheet" href="assets/css/bundle.css" id="cssbundle" media="none"/>
  </head>
  <body>
  </body>
</html>

Also create a file named content.handlebars in views directory, and insert the following HTML.

<div id="app">magic here</div>

Start the server now:

$ node entry.js

Go to http://localhost:3000151413. The error is gone, and the layout’s markup is ready.

Critical Path Link

Our environment is ready. Now, we can implement the critical-path optimization.

Determining The First Load Link

As you will recall, our first objective is to determine whether or not a load is the first. Based on this, we can decide whether to serve critical styles or the CSS bundle from the browser’s cache. We will use a cookie for this. If a cookie is set, then that means it’s not the first load; otherwise, it is. The cookie will be created in the critical JavaScript file, which will be injected inline in the template with the critical styles. Checking for the cookie will be handled by Express.

Let’s name the critical JavaScript file fastjs. We must be able to insert the content of fastjs in the layout file if a cookie doesn’t exist. I’ve found Handlebars partials to be pretty easy to use. Partials are useful when you have markup that you want to reuse in multiple places. They can be called by other templates and are mostly used for the header, footer, navigation and so on.

In the Handlebars section, I’ve defined a partials directory at /views/partials. Let’s create a /views/partials/fastjs.handlebars file. In this file, we’ll add a script tag with an ID of fastjs. We will use this ID later to remove the script from the DOM.

<script id='fastjs'>
</script>

Now, open /views/layouts/main.handlebars. Calling the partial is done through the syntax {{> partialName }}. This code will be replaced by the contents of our target partial. Our partial is named fastjs, so add the following line before the end of the head tag:

<head>
…
{{> fastjs}}
</head>

The markup at http://localhost:3000151413 now contains the contents of the fastjs partial. A cookie will be created using this simple JavaScript function.

<script id='fastjs'>
// Let's create a cookie named 'fastweb', setting its value to 'cache' and its expiration to one day
createCookie('fastweb', 'cache', 1);

// function to create cookie
function createCookie(name,value,days) {
  var expires = "";
  if (days) {
    var date = new Date();
    date.setTime(date.getTime()+(days*24*60*60*1000));
    var expires = "; expires="+date.toGMTString();
  }
  document.cookie = name+"="+value+expires+"; path=/";
}
</script>

You can check that http://localhost:3000151413 contains the cookie named fastweb. The fastjs content should be inserted only if a cookie doesn’t exist. To determine this, we need to check on the Express side whether one exists. This is easily done with the cookie-parser npm package and Express. Go to this bit of code in server.js:

app.get('/', function(req, res){
  res.status(200).render('content');
});

The render function accepts in the second position an optional object containing local variables for the view. We can pass a variable into the view like so:

app.get('/', function(req, res){
  res.status(200).render('content', {needToRenderFast: true});
});

Now, in our view, we can print the variable needToRenderFast, whose value will be true. We want the value of this variable to be set to true if a cookie named fastweb does not exist. Otherwise, the variable should be set to false. Using cookie-parser, checking for the cookie’s existence is possible with this simple code:

//Check whether cookie named fastweb is set to a value of 'cache'
req.cookies.fastweb === 'cache'

And here it is rewritten for our needs:

app.get('/', function(req, res){
  res.status(200).render('content', {
    needToRenderFast: !(req.cookies.fastweb === 'cache')
  });
});

The view knows, based on value of this variable, whether to render the critical files. Thanks to Handlebars’ built-in helpers — namely, the if block helper — this is also easy to implement. Open the layout file and add an if helper:

<head>
…
{{#if needToRenderFast}}
{{> fastjs}}
{{/if}}
</head>

Voilà! The fastjs content gets inserted only if a cookie doesn’t exist.

Injecting Critical CSS Link

The critical CSS file must be inserted at the same time as the critical JavaScript file. First, create another partial named /views/partials/fastcss.handlebars. The contents of this fastcss file is simple:

<style id="fastcss">
  body{background:#E91E63;}
</style>

Just import it as we did the fastjs partial. Open the layout file:

<head>
…
{{#if needToRenderFast}}
{{> fastcss}}
{{> fastjs}}
{{/if}}
</head>

Handling The Loading Of The CSS Bundle Link

The trouble now is that, even though the CSS bundle has loaded, the critical partials still remain in the DOM. Fortunately, this is easy to fix. Our layout’s markup looks like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Critical-Path Performance Optimization</title>
    {{#if needToRenderFast}}
    <link rel="stylesheet" href="assets/css/bundle.css" id="cssbundle" media="none"/>
    {{> fastcss}}
    {{> fastjs}}
    {{/if}}
  </head>
  <body>
  </body>
</html>

Our fastjs, fastcss and CSS bundle have their own IDs. We can take advantage of that. Open the fastjs partial and find the references to those elements.

var cssBundle = document.getElementById('cssbundle'),
fastCss = document.getElementById('fastcss'),
fastJs = document.getElementById('fastjs');

We want to be notified when the CSS bundle has loaded. This is possible using an event listener:

cssBundle.addEventListener('load', handleFastcss);

The handleFastcss function will be called immediately after the CSS bundle has loaded. At that moment, we want to propagate styles from the CSS bundle, remove the #fastjs and #fastcss elements and create the cookie. As mentioned at the beginning of this article, the styles from the CSS bundle will be propagated by changing the media attribute of the CSS bundle to a valid value — in our case, a value of all.

function handleFastcss() {
  cssBundle.setAttribute('media', 'all');
}

Now, just remove the #fastjs and #fastcss elements:

function handleFastcss() {
  cssBundle.setAttribute('media', 'all');
  fastCss.parentNode.removeChild(fastCss);
  fastJs.parentNode.removeChild(fastJs);
}

And call the createCookie function inside the handleFastcss function.

function handleFastcss() {
  createCookie('fastweb', 'cache', 1);
  cssBundle.setAttribute('media', 'all');
  fastCss.parentNode.removeChild(fastCss);
  fastJs.parentNode.removeChild(fastJs);
}

Our final fastjs script is as follows:

<script id='fastjs'>
var cssBundle = document.getElementById('cssbundle'),
fastCss =  document.getElementById('fastcss'),
fastJs =  document.getElementById('fastjs');

cssBundle.addEventListener('load', handleFastcss);

function handleFastcss() {
  createCookie('fastweb', 'cache', 1);
  cssBundle.setAttribute('media', 'all');
  fastCss.parentNode.removeChild(fastCss);
  fastJs.parentNode.removeChild(fastJs);
}
function createCookie(name,value,days) {
  var expires = "";
  if (days) {
    var date = new Date();
    date.setTime(date.getTime()+(days*24*60*60*1000));
    var expires = "; expires="+date.toGMTString();
  }
  document.cookie = name+"="+value+expires+"; path=/";
}
</script>

Please note that this CSS load handler works only on the client side. If client-side JavaScript is disabled, it will continue using the styles in fastcss.

Handling The Second And Nth Load Link

The first load now behaves as expected. But when we reload the page in the browser, it remains without styles. That’s because we’ve only dealt with the scenario in which a cookie doesn’t exist. If a cookie does exist, the CSS bundle must be linked in the standard way.

Edit the layout file:

<head>
  …
  {{#if needToRenderFast}}
  <link rel="stylesheet" href="assets/css/bundle.css" id="cssbundle" media="none"/>
  {{> fastcss}}
  {{> fastjs}}
  {{else}}
  <link rel="stylesheet" href="assets/css/bundle.css" id="cssbundle" media="all"/>
  {{/if}}
</head>

Save it, and view the result.

Result Link

The GIF below shows the first load. As you can see, while the CSS bundle is downloading, the page has a different background. This is caused by the styles in the fastcss partial. The cookie is created, and the bundle.css request ends with a status of “200 OK16.”

Critical-path performance optimization: first load

The second GIF shows the reloading scenario. A cookie has already been created, the critical files are ignored, and the bundle.css request ends with a status of “304 Not modified17.”

Critical-path performance optimization: second and nth load

Conclusion Link

We’ve gone through the whole lifecycle shown in the schema above. As a next step, check that all requests to scripts, images, fonts and so on are asynchronous and do not block rendering. Also, don’t forget to enable gZip compression on the server; nice Express middleware18 is available for this.

Lifecycle of critical-path performance optimization19
Lifecycle of critical-path performance optimization (View large version205)

(rb, ml, mh, vf, al, il)

Footnotes Link

  1. 1 https://www.smashingmagazine.com/wp-content/uploads/2016/07/01-criticalcss-article-opt.png
  2. 2 https://www.smashingmagazine.com/wp-content/uploads/2016/07/01-criticalcss-article-opt.png
  3. 3 https://github.com/FilipBartos/express-handlebars-criticalcss
  4. 4 https://www.smashingmagazine.com/wp-content/uploads/2016/07/02-criticalcss-graph-opt.png
  5. 5 https://www.smashingmagazine.com/wp-content/uploads/2016/07/02-criticalcss-graph-opt.png
  6. 6 https://facebook.github.io/react/docs/top-level-api.html#reactdomserver.rendertostring
  7. 7 https://facebook.github.io/react/docs/top-level-api.html#reactdomserver.rendertostaticmarkup
  8. 8 https://www.smashingmagazine.com/wp-content/uploads/2016/07/03-criticalcss-schema-opt.png
  9. 9 https://www.smashingmagazine.com/wp-content/uploads/2016/07/03-criticalcss-schema-opt.png
  10. 10 https://babeljs.io/docs/setup/#babel_register
  11. 11 http://localhost:3000/
  12. 12 http://handlebarsjs.com/
  13. 13 http://localhost:3000
  14. 14 http://localhost:3000
  15. 15 http://localhost:3000
  16. 16 https://httpstatuses.com/200
  17. 17 https://httpstatuses.com/304
  18. 18 https://github.com/expressjs/compression
  19. 19 https://www.smashingmagazine.com/wp-content/uploads/2016/07/02-criticalcss-graph-opt.png
  20. 20 https://www.smashingmagazine.com/wp-content/uploads/2016/07/02-criticalcss-graph-opt.png
  21. 21 https://www.smashingmagazine.com/2015/04/react-to-the-future-with-isomorphic-apps/
  22. 22 https://www.smashingmagazine.com/2015/08/understanding-critical-css/
  23. 23 https://developers.google.com/web/fundamentals/performance/critical-rendering-path/?hl=en
  24. 24 https://www.igvita.com/2015/06/25/browser-progress-bar-is-an-anti-pattern/

↑ Back to top Tweet itShare on Facebook

Filip Bartos is freelance front-end developer crafting unique digital experiences through modern, responsive websites & web apps built using HTML5/CSS3 and cutting edge technologies like React. From time to time Filip writes about web development on his blog and sometimes contributes to open-source projects.

  1. 1

    Please include the code for content.handlebars template in the tutorial. Otherwise we will get a error like ‘Failed to lookup view “content” in views directory’

    0
  2. 3

    Does anyone besides me think Google has a screw a bit loose about the amount of emphasis they put on this? Sure, it some situations you can eek out “a little” extra speed on the initial page load. But it’s no small amount of trouble to set up. The JS trickery that this article explains (which I think does fine job), is going to be the smaller part. The bigger hassle is going to be the extra time writing enough CSS to keep your look/layout together enough until the bundle loads. Of course sans Bootstrap or any other dependencies.

    For a few milliseconds, in most cases it’s not going to pay off. The fact that Google Page Speed Insights now makes this the single biggest factor (at least for my projects), is without any merit. You’re much better off leaving the main optimized CSS alone and focusing on an initially small Javascript bundle. Then lazy load as needed. Now that’s an area that can make a difference. If you really need the fastest initial load, lazy load anything you can below the the fold. CSS just isn’t modular enough to break off unless you’re subscribe to inlining it, which I think is a mistake.

    1
    • 4

      I’m with you, Google Pagespeed’s emphasis on this is extreme. For large and complex websites, it’s become extremely hard to score in the non-red, and sometimes impossible.

      Whilst I credit the author for the robust tutorial, I’ve always found it a ridiculous optimization. Not in the sense that it does not work, it does, but the fact that developers have to do this insane amount of trickery to influence rendering. I believe it is way too low level in the abstraction chain, and also a very hard technique to scale.

      0
      • 5

        Yes, my comment was mainly about the amount of emphasis and how they are taking something very bleeding edge and trying to make it industry standard just by the virtue of the amount of clout they have. I think I wasn’t as clear as I could have been, but my beef is especially around the CSS part of the emphasis. Unlike javascript, CSS is global in nature. I think that you will find those that are advocating this approach believe that global CSS is the same type of sin that global JS is, so they attempt to go with CSS modules and forgo the huge community of CSS frameworks and libraries that are out there. I guess they have a lot of time on their hands to reinvent the wheel.

        On the other hand Javascript bundles are usually much bigger than CSS, and thus affect load times more. Isomophic rendering is a great technique that will help, but I agree that in cases where you’ve still have a large JS bundle this technique could be useful. But before even considering it, you should be making multiple bundles (Webpack and code splitting is nice) for the different views or anything that can be triggered by the user, without needing to check a cookie. IF you still have a big initial bundle after all of that, then this technique could be considered. But that’s not what Google is doing. They are giving you a big fat red mark simply because you don’t do it, even if you have a static one page site with 10 lines of css in an external file. No matter if the thing loads and renders in under 500ms ;) It reminds me of when YSLOW first emphasised using a CDN… way before CDN’S were a big thing (or affordable).

        0
  3. 6

    Jason Kendall

    August 6, 2016 6:14 pm

    I’ve spent nearly 2 months refactoring our ecommerce site for loading our critical path assets. I did a lot of research on this topic and explored many different techniques. The one that has worked best for us was to break all of our JavaScript into es6 modules and use WebPack’s code splitting features. WebPack makes it extremely easy to optimize you JS and CSS for critical path. You can also configure all of your JavaScript modules to load on only the pages you want them on, instead of loading them all on every page and use it to inject your above the fold CSS into the head. Amazing stuff, but to tell you the truth the critical path technique is all just an illusion of speed. It actually takes longer to fully load but to the user it appears to be loading extremely fast, they may not be able to fully interact with anything yet but it looks fully loaded because they can see a styled page immediately.

    0
  4. 7

    Thanks for the sharing, Filip. A quick question: how to make this technology work if HTML markup is served through a CDN?

    1
  5. 8

    Did you really add the complexity of Babel in this example just to be able to write import rather than require?

    0
    • 9

      Hello Lasse, thanks for the notice. Of course, you are right that for the purpose of this article could be Babel part skipped. As written at the beginning, these are notes related to development of React application (using ES6 syntax). I left it there just to keep the article as some kind of “boilerplate”. I’ll think about adding some information about this.

      0

↑ Back to top