Web Performance

Complete WordPress Performance Optimization Guide

In this day and age of short attention spans and demand for instant gratification, page speed is crucial. Not to mention that it’s one of the ranking factors on Google.

For me, as a web developer, website performance optimization is a vital part of the job. But more importantly, I enjoy it and starting to specialize in performance optimization.

For that reason, lately, I’m spending a lot of time studying all the new progressive enchantment delivery techniques.

So, I thought this was a great time to sort all things I learned. And it turned out into 3000+ word guide.

In this guide, you’ll find tested techniques I use to speed up WordPress sites for my clients.

Few things to note:

When it comes to WordPress optimization, there are a couple of ways to do it: using a plugin or manually editing theme files.

As always there is no one fit for all solution.

Plugins can make life easier, and there is no shame in using them. However, I never use a plugin until I understand what code it runs and what it does.

Most of the optimizations in this guide require modifying theme files so you should be comfortable doing that.

WordPress generates HTML pages dynamically – by employing PHP code coupled with MySQL database queries. Needless to say, it’s a lot slower than just serving HTML file right away.

Illustration how WordPress cache works
How WordPress generates dynamic page. By wpexplorer.com

Page caching will cache posts and pages as static files and then serve to users, reducing the processing load on the server.

Don’t mistake WordPress page caching with browser caching.

If you enable browser caching then browsers save static data in your computer’s hard drive, such as images, CSS and Javascript files. Usually, browser caching is enabled by modifying an .htaccess file.

You could put the following in an .htaccess file:

# One year for image files and CSS, JS
<filesMatch ".(css|js|jpg|jpeg|png|gif|ico)$">
Header set Cache-Control "max-age=31536000, public"
</filesMatch>

The above code sets images, CSS and JS to be cached for one year.

The easiest and fastest way to improve performance by utilizing both page caching and browser caching is to use WordPress plugin.

There are a lot of free caching plugins out there, however, I use the premium plugin – WP Rocket. There are few reasons why it’s worth the money.

First of all, this benchmarking test done by Swedish Marketing expert Philip Blomsterberg showed that WP Rocket was the fastest option on the market.

Also, it has some advanced functionalities like cache preloading, automatic database cleanup and easy integration with CloudFlare CDN.

For a free alternative, you could check out Cache Enabler which is created by the KeyCDN team.

It’s fairly new plugin but was created to utilize the new HTTP/2 protocol. During the benchmarks, Cache Enabler out-performed WP Super Cache and W3 Total Cache.

You can minify resources with my already mentioned WP Rocket, Cache Enabler or any other plugin, also with a free CloudFlare CDN account.

But if you’re building WordPress theme then you shouldn’t leave it for the server to do the work for you. CSS with JavaScript should be minified during front-end workflow using build tools like Webpack and Gulp.

First, let’s take a look how browser parses HTML to understand better render blocking.

This illustration explains very clearly how scripts are loaded by default and using async or defer attributes.

Illustration how async and defer works
Illustration by growingwiththeweb.com

So any scripts or styles which are not critical for page rendering should be deferred or loaded asynchronously.

For HTML sites deferring scripts is easy. Just add defer attribute next to src and you’re good to go.

But for WordPress, theme’s scripts are loaded from functions.php and each plugin could enqueue script from its source code as well. So we have to run the code which will retrieve all enqueued scripts.

You could add this to functions.php file.

/**
* Function to defer all scripts which are not excluded
*/
function crave_js_defer_attr($tag) {
	if (is_admin()) {
		return $tag;
	}
	// Do not add defer attribute to these scripts
	$scripts_to_exclude = array('jquery.js'); // add a string of js file e.g. script.js

	foreach($scripts_to_exclude as $exclude_script) {
		if (true == strpos($tag, $exclude_script ) )
			return $tag; 
	}
	// Defer all remaining scripts not excluded above
	return str_replace( ' src', ' defer src', $tag );
}
add_filter( 'script_loader_tag', 'crave_js_defer_attr', 10);

This code snippet iterates through all scripts, except the ones you specify in the array, and ads defer attribute. However, it’ll not run on your admin panel.

Deferring jQuery usually is a bad idea. I almost never do it for my clients because I have to be sure that I’ll retain control of a website or there won’t be any changes to a site later on.

Loading CSS asynchronously in WordPress is tricky because there could be a few stylesheets loaded by a theme and plugins could load their styles.

Most available plugins do a poor job by inlining all CSS or just breaking a theme.

If a site is not loading a ton of stylesheets, I prefer to async CSS manually with code. Unfortunately often that’s not the case.

So when CSS is scattered all over a place, we need to collect all stylesheets. In this case, to not go crazy we still need to use a plugin.

There is one well-coded option that I trust at the moment, but it comes at a price. But more about it later.

We can load CSS asynchronously using loadCSS script by Fillament Group. It’s a proven technique that even Google recommends using.

For a simple WordPress site we could just load stylesheets by inlining <link> element in the header.php.

I know it’s not according to the best WordPress practice, but if you just have a few stylesheets and not using plugins that load CSS on the front-end, then it’s not necessary to make this more complicated than it should be.

For each CSS file you’d want to load asynchronously, use a link element like this:

<link rel="preload" href="path/to/mystylesheet.css" as="style" onload="this.rel='stylesheet'">

Here is how the authors explain it:

In browsers that support it, the rel=preload attribute will cause the browser to fetch the stylesheet, but it will not apply the CSS once it is loaded (it merely fetches it). To address this, we recommend using an onload attribute on the link that will do that for us as soon as the CSS finishes loading.

This step requires JavaScript to be enabled, so for browsers that dont support it you could use a fallback like this:

<link rel="preload" href="path/to/mystylesheet.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="path/to/mystylesheet.css"></noscript>

After linking to your stylesheets, inline the loadCSS script and the rel=preload polyfill script in your page.

Here’s how your code should look at this point:

<link rel="preload" href="path/to/mystylesheet.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="path/to/mystylesheet.css"></noscript>
<script>
/*! loadCSS. [c]2017 Filament Group, Inc. MIT License */
(function(){ ... }());
/*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */
(function(){ ... }());
</script>

These scripts will detect if a browser supports rel=preload. In browsers that support it, these scripts will do nothing, allowing the browser to load and apply the asynchronous CSS.

In browsers that do not support rel=preload, they will find CSS files referenced this way in the DOM and load and apply them asynchronously using the loadCSS function.

Now, there is one more thing left to do.

Because we load CSS asynchronously, our page will load HTML first and then apply stylesheets when they’re finished loading.

This produces undesirable Flash of Unstyled Content (FOUC). FOUC makes your website look like it brakes for a moment and doesn’t signal authority or competence. So obviously, we would like to avoid that.

Demonstration how flash of unstyled content looks like
Flash of Unstyled Content

We can fix it by inlining a critical portion of CSS, that’s required to style above the fold content.

To generate critical CSS of your page you can use this free critical CSS generator.

But if you’re optimizing a lot of pages than it could be worth to pay 2 GBP/month for criticalcss.com service. It lets you save all pages and regenerate CSS if you made changes.

Use one of those websites to generate and inline CSS inside header.php before <link> elements.

If you’re using free critical CSS generator you may have to edit any relative URLs in the code manually (e.g., for fonts and background images) and turn them into absolute URLs.

For example, if the generated CSS contains a relative path to a font, like this:

@font-face{font-family:proxima-nova;src:url(../proxima-nova.eot);

The relative path (indicated by ../ in the URL) will not be correct when you inline CSS in header.php. So you need to replace it with the absolute path, for example:

@font-face{font-family:proxima-nova;src:url(http://example.com/wp-content/themes/crave/font/proxima-nova.eot);

Next, I’ll show how to automate asynchronous CSS loading process.

If you’re building a theme from scratch then you could automate the whole process – critical CSS generation and insertion, inserting link elements and inlining loadCSS script – during front-end workflow with Webpack’s plugin HTML Critical Webpack Plugin.

Webpack’s plugin is based on Critical which is npm package for Gulp.

As I hinted at the beginning of this step, there is one good plugin I trust that handles loading CSS asynchronously. It’s actually WP Rocket caching plugin I mentioned in this article already.

WP Rocket grabs all <link> elements and adds rel="preload" and onload attributes to them as well as uses fallbacks and the loadCSS script by the Filament Group.

You can also inline critical CSS in WP Rocket settings.

WP Rocket critical CSS

Updated April 26, 2018: As of version 2.11 released on December you can now generate and include critical CSS automatically from WP Rocket settings. It will generate a different critical CSS for your homepage, your blog page and every type of pages/taxonomies you have.

PHP 7 is twice as fast as an old PHP 5.6. WordPress already supports it and even officialy recommends PHP 7.

However, as of October 2017, according to WordPress stats only a 13.7% of users use PHP 7. And only 4.1% are using PHP 7.1.

Pie chart of PHP versions used by WordPress users
PHP versions used by WordPress users

I think most are just unaware of PHP 7 and its benefits. Others may be reluctant to switch because of plugin or theme support.

But performance gains are too big to ignore. Therefore, I recommend to test it on your site and revert to PHP 5.6 if you encounter problems.

So how to do it?

First of all, your hosting has to support PHP 7. Ask them if they do and if they can enable it for you.

If you’re using SiteGround hosting here is the guide how to enable PHP 7 on their servers.

SiteGround is already running PHP 7.2 alpha which from preliminary test results is promising to be even faster version.

Right now SiteGround’s 7.2 version is throwing an internal error for my blog, but PHP 7.1 works perfectly.

I could write an entirely separate post talking just about image optimization. And probably I’ll write it in the future because this step is crucial towards an increased performance of any website.

Images are the main cause of bloat on the web.

According to the HTTP Archive, as of October 2017, 54% of the data transferred to load a web page comprised of images.

So it’s important not to skip this step and invest in efficient image optimization strategy to reduce page load time.

At the bare minimum, you should compress images with a lossy compression.

Lossy compression can sometimes reduce up to 70% of initial file size while retaining most of the quality. In most cases, you won’t be able to tell the difference even upon close inspection.

With WordPress, it’s easy to automate image compression with a plugin.

There are a bunch of options out there, although most are paid, some have a free limited usage. I use TinyPNG and with a free account, you can optimize 500 images each month.

However, remember that WordPress resizes each image and saves five or more sizes and each count as a separate image.

If you want to learn more about image optimization, Addy Osmani, an engineer at Google wrote a free eBook called Essential Image Optimization.

WordPress adds a lot of unnecessary code in the <head>. In most cases, it’s useless and in some even makes a site less secure.

It may be a tiny performance gain, but every little bit helps, especially when some things do not need to be there in the first place.

Add the following code to your function.php file to clean <head>.

/**
* Remove junk from head
*/
// remove WordPress version number
function crave_remove_version() {
	return '';
}
add_filter('the_generator', 'crave_remove_version');
remove_action('wp_head', 'wp_generator');

remove_action('wp_head', 'rsd_link'); // remove really simple discovery (RSD) link
remove_action('wp_head', 'wlwmanifest_link'); // remove wlwmanifest.xml (needed to support windows live writer)

remove_action('wp_head', 'feed_links', 2); // remove rss feed links (if you don't use rss)
remove_action('wp_head', 'feed_links_extra', 3); // removes all extra rss feed links

remove_action('wp_head', 'index_rel_link'); // remove link to index page

remove_action('wp_head', 'start_post_rel_link', 10, 0); // remove random post link
remove_action('wp_head', 'parent_post_rel_link', 10, 0); // remove parent post link
remove_action('wp_head', 'adjacent_posts_rel_link', 10, 0); // remove the next and previous post links
remove_action('wp_head', 'adjacent_posts_rel_link_wp_head', 10, 0 );

remove_action('wp_head', 'wp_shortlink_wp_head', 10, 0 ); // remove shortlink

WordPress adds a file version to the end of URLs for CSS and JS files that are loaded. It looks something like this: domain.com/script.min.js?ver=1.14.2.

The problem is some servers are unable to cache resources with query strings, even if a Cache-Control: public header is present. So by removing query strings, you can ensure resource caching.

/**
* Remove query strings
*/
function crave_remove_script_version( $src ) {
	$parts = explode( '?ver', $src );
	return $parts[0]; 
} 
add_filter( 'script_loader_src', 'crave_remove_script_version', 15, 1 );
add_filter( 'style_loader_src', 'crave_remove_script_version', 15, 1 );

WordPress insists on loading wp-emoji-release.min.js every time. Whether you use emoticons or not. Most don’t so it’s best to get rid of it and have one less HTTP request.

Add the following code to your WordPress theme’s functions.php file.

/**
* Disable the emoji's
*/
function crave_disable_emojis() {
	remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
	remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
	remove_action( 'wp_print_styles', 'print_emoji_styles' );
	remove_action( 'admin_print_styles', 'print_emoji_styles' ); 
	remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
	remove_filter( 'comment_text_rss', 'wp_staticize_emoji' ); 
	remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
	add_filter( 'tiny_mce_plugins', 'crave_disable_emojis_tinymce' );
	add_filter( 'wp_resource_hints', 'crave_disable_emojis_remove_dns_prefetch', 10, 2 );
}
add_action( 'init', 'crave_disable_emojis' );

/**
* Filter function used to remove the tinymce emoji plugin.
* 
* @param array $plugins 
* @return array Difference betwen the two arrays
*/
function crave_disable_emojis_tinymce( $plugins ) {
	if ( is_array( $plugins ) ) {
		return array_diff( $plugins, array( 'wpemoji' ) );
	} else {
		return array();
	}
}

/**
* Remove emoji CDN hostname from DNS prefetching hints.
*
* @param array $urls URLs to print for resource hints.
* @param string $relation_type The relation type the URLs are printed for.
* @return array Difference betwen the two arrays.
*/
function crave_disable_emojis_remove_dns_prefetch( $urls, $relation_type ) {
	if ( 'dns-prefetch' == $relation_type ) {
		/** This filter is documented in wp-includes/formatting.php */
		$emoji_svg_url = apply_filters( 'emoji_svg_url', 'https://s.w.org/images/core/emoji/2/svg/' );
		$urls = array_diff( $urls, array( $emoji_svg_url ) );
	}
	return $urls;
}

Source: code is extracted from Disable Emojis plugin.

Since 4.4 release, WordPress loads a new script called wp-embed.min.js. It allows embedding blog post, videos, etc. more easily. The problem is that WordPress loads this script on every page.

You can read more about it on WordPress official page and decide if you need to keep it.

Here is how to disable it. Paste this code into your theme’s functions.php file.

/**
 * Disable embeds
 */
function crave_disable_embeds() {
	
	// Remove the REST API endpoint.
	remove_action( 'rest_api_init', 'wp_oembed_register_route' );
	
	// Turn off oEmbed auto discovery.
	add_filter( 'embed_oembed_discover', '__return_false' );
	
	// Don't filter oEmbed results.
	remove_filter( 'oembed_dataparse', 'wp_filter_oembed_result', 10 );
	
	// Remove oEmbed discovery links.
	remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
	
	// Remove oEmbed-specific JavaScript from the front-end and back-end.
	remove_action( 'wp_head', 'wp_oembed_add_host_js' );
	add_filter( 'tiny_mce_plugins', 'crave_disable_embeds_tiny_mce_plugin' );
	
	// Remove all embeds rewrite rules.
	add_filter( 'rewrite_rules_array', 'crave_disable_embeds_rewrites' );
	
	// Remove filter of the oEmbed result before any HTTP requests are made.
	remove_filter( 'pre_oembed_result', 'wp_filter_pre_oembed_result', 10 );
}
add_action( 'init', 'crave_disable_embeds', 9999 );
	
function crave_disable_embeds_tiny_mce_plugin($plugins) {
	return array_diff($plugins, array('wpembed'));
}
	
function crave_disable_embeds_rewrites($rules) {
	foreach($rules as $rule => $rewrite) {
		if(false !== strpos($rewrite, 'embed=true')) {
			unset($rules[$rule]);
		}
	}
	return $rules;
}

Source: code is extracted from Disable Embeds plugin.

Most up-to-date code and plugins don’t require jquery-migrate.min.js. So in most cases, this simply adds unnecessary load.

jQuery migrate in WordPress is loaded as a bundle with a main jQuery library. The following code snippet removes the bundle on the frontend, thus removing jquery-migrate.js, then re-loads ‘jquery-core’ by itself.

function crave_remove_jquery_migrate( &$scripts) {
	if(!is_admin()) {
		$scripts->remove('jquery');
		$scripts->add('jquery', false, array( 'jquery-core' ), '1.12.4');
	}
}
add_action( 'wp_default_scripts', 'crave_remove_jquery_migrate' );

#Disable scripts on a per post/page basis

WordPress plugins load scripts across an entire website even when they’re only used for a single page or only for posts. Disabling them can significantly increase the performance of a website, especially a homepage. Here are a few examples:

  • The Contact Form 7 plugin loads itself on every page and post. You should dequeue it everywhere and enqueue only on a contact page.
  • Social media sharing plugin should only be loaded on posts.
  • Table of contents plugin is also used only for posts.

This is just a few examples. In reality, there could be a bunch of plugins loading, and it could be a tedious job to sort through each of them.

That’s why when it comes to disabling WordPress enqueued scripts I use a premium perfmatters plugin. It’s developed by a team member at Kinsta – high performance WordPress hosting.

It has a panel activated through admin toolbar for a page you’re currently at. After activating it, you’ll be presented with all the scripts and styles, which are loading on that page. Then you can easily disable them on a current URL, everywhere, or everywhere except your selected posts types.

perfmatters plugin script manager panel
perfmatters script manager panel

By the way, with perfmatters you can also easily disable emojis, embeds, jQuery migrate, clean <head> and perform other optimizations.

Disqus is a great option for comments, eliminating almost all spam. However, its default plugin creates additional HTTP requests which can significantly slow down a site.

However, James Joel developed a plugin which cuts out those HTTP requests on initial load by using lazy loading. It’s called “Disqus Conditional Load”. It’s free and even doesn’t require jQuery. Also, it’s SEO friendly, so Google will still crawl all comments.

If you’re already using Disqus don’t forget to disable the official Disqus plugin to avoid conflict.

Let’s say you need to request a file from ajax.googleapis.com. Then you can prefetch that hostname’s DNS by adding this line in the <head> of the document:

<link rel="dns-prefetch" href="//ajax.googleapis.com">

In his front-end performance article, Harry Roberts explains it very clearly:

That simple line will tell supportive browsers to start prefetching the DNS for that domain a fraction before it’s actually needed. This means that the DNS lookup process will already be underway by the time the browser hits the script element that actually requests the file. It just gives the browser a small head start.

In WordPress, you can add this code to functions.php to activate DNS prefetch lookup.

function dns_prefetch() {
$prefetch = 'on';
    echo "n  n";
    echo '<meta http-equiv="x-dns-prefetch-control" content="'.$prefetch.'">'."n";
    
    if ($prefetch != 'on') {
      $dns_domains = array( 
          "//use.typekit.net",
          "//netdna.bootstrapcdn.com", 
          "//cdnjs.cloudflare.com",
          "//ajax.googleapis.com", 
          "//s0.wp.com",
          "//s.gravatar.com",
          "//stats.wordpress.com",
          "//www.google-analytics.com"
      );
      foreach ($dns_domains as $domain) {
        if (!empty($domain)) echo '<link rel="dns-prefetch" href="'.$domain.'" />'."n";
      }
      unset($domain);
    }
    echo " n";
}
add_action( 'wp_head', 'dns_prefetch', 0 );

Source: GitHub by Leo Gopal

This code snippet will turn on X-DNS-Prefetch-Control, a feature that makes browsers proactively look for domain names to prefetch.

If you set $prefetch variable to 'off', then it will print DNS lookups you provide in an array.

Following above techniques, you could dramatically reduce the load time of any WordPress site.

However, performance optimization is an ongoing process. Over time web designs change, and new technologies emerge which allow for new methods to be created.

So, even though this guide is called “complete” I still have a few techniques in mind that I’d like to test and share with you.

I’d love to hear any feedback so share it in the comments!

Leave a Reply

Your email address will not be published. Required fields are marked *