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.

#WordPress Performance Optimization

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.

#Use caching

#Server-side caching

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

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

#Client-side caching

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"

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

#Caching plugins

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

#WP Rocket

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.

Updated November 12, 2018: I no longer use WP Rocket since I switched to Trellis set up with NGINX FastCGI caching and I’ve automated async CSS loading as well, so for me there is simply not much use for it. However, I think WP Rocket is still not a bad choice.

#Cache Enabler

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.

#Minify HTML, CSS and JavaScript

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.

#Eliminate render-blocking JavaScript and CSS

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

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

#Defer JavaScript

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.

#Load CSS Asynchronously

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.

#Use LoadCSS

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>
/*! loadCSS. [c]2017 Filament Group, Inc. MIT License */
(function(){ ... }());
/*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */
(function(){ ... }());

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.

#Generate critical CSS

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 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:


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:


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

#Automate with build tools

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.

If you care about performance I highly recommend for any custom theme build to use Sage starter theme. I wrote I guide on how to automate asynchronous CSS with Sage.

#Automate with a plugin

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.

#Switch to PHP 7

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.

#Optimize Images

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.

#Remove junk from head

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

#Remove query strings

WordPress adds a file version to the end of URLs for CSS and JS files that are loaded. It looks something like this:

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 );

#Disable unnecessary scripts

#Disable Emojis

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', '' );
		$urls = array_diff( $urls, array( $emoji_svg_url ) );
	return $urls;

Source: code is extracted from Disable Emojis plugin.

#Disable Embeds

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')) {
	return $rules;

Source: code is extracted from Disable Embeds plugin.

#Disable jQuery migrate

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->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.

#Lazy-load comments


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.

#DNS Prefetching

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

<link rel="dns-prefetch" href="//">

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( 
      foreach ($dns_domains as $domain) {
        if (!empty($domain)) echo '<link rel="dns-prefetch" href="'.$domain.'" />'."n";
    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.

#Wrapping It Up

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!

7 thoughts on “Complete WordPress Performance Optimization Guide

  1. Hey and thanks for this guide,

    I’ve got several questions:

    I am wondering; Many of the scripts that seem to slow down my site are external scripts.

    Would you suggest hosting the scripts locally and add a function to auto update it when a new version is available on the original site, or to keep those functions at their external resources? (Google analytics, webfonts, social sharing plugin files etc.)

    2. Is there a way to see what loaded resources are actually used on the page, so that you know what scripts you can safely disable on that specific page?

    3. is there a way to decide what is preloaded and what custom links in the content are prefetched?

    Thank you,

    1. Hey Bas, I would say optimization should be a balance between performance and maintainability/usability.

      Some files are better to be left hosted externally. Even though you’ll get a slight performance boost hosting fonts on CDN where you host the rest of the assets I wouldn’t go out of my way to do that for Google fonts or premium fonts hosted on their own CDN.

      Instead I use library which provides a seamless transition between system fonts and custom fonts while they are loaded without FOIT (Flash of Invisible Text).

  2. Greetings! I know this is kind of off topic but I was wondering which blog platform are you using for this website? I’m getting sick and tired of WordPress because I’ve had issues with hackers and I’m looking at options for another platform. I would be awesome if you could point me in the direction of a good platform.

    1. I’m using WordPress. I would recommend looking into headless WordPress with Gatsby. However, you’ll need hire a developer to set it up for you but then you’d be able to still use WordPress to create posts but your website would be separated from WP and just get the data from WP backend. This way you eliminate all secutiry issues with WP because your website’s frontend is static and there is nothing to hack.

  3. Dear Jason,
    thank you so much for your post! That helped me a lo.
    I hope you’re reading my message.

    I have the following problem –
    The type attribute for the style element is not needed and should be omitted.
    From line 1, column 4617; to line 1, column 4639
    6db8b93″> img.wp

    wprocket is aktive my site is

    Do you have any idea how I could solve these warnings?

    many thx for your ideas

    1. WordPress inserts type attribute automatically for styles and scripts. Yes it’s an old practice and not needed for modern browsers but it doesn’t do any harm either. I’d just ignore it and focus your effort elsewhere.

Leave a Reply

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