Defining a Critical CSS Workflow for WordPress

As performance-minded developers, keeping up with latest (although not always greatest) best practices when it comes to building fast sites is just part of the job. But testing and implementing solutions that yield meaningful results can be frustrating. It’s easy to get bogged down in trying shave off a few more milliseconds based on what performance measuring tools tell you, but often the amount of time spent researching and testing is disproportionate to the payoff. Optimizing the critical rendering path is not one of those times.

Optimizing the critical rendering path refers to prioritizing the display of content that relates to the current user action.

Ilya Grigorik, Developer at Google

In other words, put consumable content in front of the user as fast as possible. More technically speaking, the critical rendering path refers to the processing the browser does between receiving HTML, CSS and Javascript, and when a rendered pixel first appears on screen. By optimizing for this step, we are making the browser’s job easier and therefore faster. This is especially crucial for mobile – 53% of visits are abandoned if a mobile site takes longer than 3 seconds to load.1

Over the last few years, the low-hanging fruit of critical rendering path optimization has become commonplace, namely loading Javascript files from the footer, or from the header with the async or defer attribute. Web fonts are no longer an issue –  asynchronous font loading can be enabled natively in some cases (ie; Typekit), and with tools like Web Font Loader. That leaves CSS as the main render-blocking offender.

What is Critical CSS?

When a browser receives the css stylesheet from the server, it halts rendering on the rest of the page until the CSS file is fully downloaded. This often causes a flash of unstyled content, which is a less than ideal user experience. To avoid this, we will inline the “critical” styles needed to render the page above the fold.2 This dramatically improves perceived performance as the user will see the “critical” portion rendered before the full page is loaded.

The remaining styles are then deferred by using rel="preload". Preload essentially specifies that an asset is needed very soon after the page has loaded, and that the browser should start preloading early in the lifecycle.

Critical CSS Workflow

We had been somewhat weary of implementing Critical CSS in our workflow because of maintenance concerns. But with the help of Grunt and cookies, we constructed a fully automated build process that will generate the critical css, inject it into it’s corresponding template, and handle cache busting.

Install Dependencies

Setup Gruntfile.js

grunt-critical generates the css needed to render above the fold based on the viewport width and height, and the src url passed to it. It then spits that css out to a defined file or template.

grunt-number generates a number and increments on that every time grunt runs. This number is also printed out to a location of our choice.

critical: {
	templateA: {
		options: {
			base: './',
			css: [
					'assets/css/style.css'
			],
			width: 1400,
			height: 800
		},
		src: 'https://site.test/',
		dest: 'assets/css/critical/critical-a.css'
	},

	templateB: {
		options: {
			base: './',
			css: [
					'assets/css/style.css'
			],
			width: 1400,
			height: 800
		},
		src: 'https://site.test/page-b',
		dest: 'assets/css/critical/critical-b.css'
	}
},

buildnumber: {
	options: {
		field: 'build'
	},
	files: ['assets/css/bust-cache.json']
}

Set a cookie and define our css version number

From here on out, we will be working in functions.php. First we need to get the build number from the previous step and set that as our version number. The version number is what determines if the server will return inlined critical css along with the full stylesheet (with rel=preload), or a request to the cached stylesheet. Note that the cookie must be set during the ‘init’ action.

$str = file_get_contents('wp-content/themes/kd/assets/css/bust-cache.json');
$id = json_decode($str);
$hash = $id->build;

define(CSS_VERSION, $hash);

function critical_css_cookie() {
	$cookie_name = 'csscache';

	setcookie($cookie_name, CSS_VERSION, time() + (60*60*24*365), COOKIEPATH, COOKIE_DOMAIN);
}
add_action('init', 'critical_css_cookie');

Check if cookie is set and has the current version number

If the browser has the latest version, we serve up a request to the cached stylesheet. If it does not, we need to deliver the inlined critical css, the stylesheet with rel="preload" and a polyfill for rel="preload" into the head element. We also want to update our cookie’s value with the latest version number. In this case we need to use Javascript because the wp_head action fires too late to use PHP.

function critical_css() {
	$styles = '';
	$template = '';

	if(is_page_template('page-template-a.php')) {
		$template = 'a';
	} elseif(is_page_template('page-template-b.php')) {
		$template = 'b';
	} 

	if(isset($_COOKIE['csscache']) && $_COOKIE['csscache'] == CSS_VERSION) {
		$styles .= '<link rel="stylesheet" href="' . get_template_directory_uri() . '/assets/css/style.min.css?ver=' . CSS_VERSION . '" />';

	} else {
		$styles .= '<style>' . file_get_contents('assets/css/critical/critical-' . $template . '.css', FILE_USE_INCLUDE_PATH) .'</style>';

        $styles .= '<link rel="preload" href="' . get_template_directory_uri() . '/assets/css/style.min.css?ver=' . CSS_VERSION . '" as="style" onload="this.onload=null;this.rel='stylesheet'"/>';

		$styles .= '<script>' . file_get_contents('assets/js/cssrelpreload.min.js', FILE_USE_INCLUDE_PATH) . '</script>';

		$styles .= '<script>'document.cookie=csscache="' . CSS_VERSION . '";expires="Tue, 19 Jan 2038 03:14:07 GMT";path="/"; '</script>';
    }
add_action('wp_head', 'critical_css');

In the end, implementing critical css into our workflow was well worth it, but like everything else in web development, every situation is unique and best practices evolve. As HTTP/2 becomes more widely adopted, it’s quite likely the preferred approach will change (check out what The Filament Group is doing with  HTTP/2 and Server Push).

  1. Google Data, Global, n=3,700 aggregated, anonymized Google Analytics data from a sample of mWeb sites opted into sharing benchmark data, Mar. 2016. https://www.thinkwithgoogle.com/data/mobile-site-abandonment-three-second-load/
  2. More on the fold here

Posted By Andy Knapp

Posted on May 31, 2019

dribbblefacebookinstagramtwittervimeoyoutube