How I optimised my theme’s speed

This blog post was written by Nick McBurney and published 2nd April 2016

I worked hard to optimise my custom WordPress theme to load efficiently. This blog post will show you the services and code I used to do it. First off, I used three sites (Google PageSpeed, GTMetrix & Pingdom) to test the loading speed and get recommendations on how to optimise it.

Site scores after optimisation:

These scores are based on tests ran on my homepage: nickmcburney.co.uk

Google PageSpeed: 99/100 desktop & 98/100 mobile
GTMetrix: 93% PageSpeed score & 97% YSlow score
Pingdom: 98/100 performance score

Average Score: 97/100

My PageSpeed Score After Optimisation - 99/100

I compressed images

Photograph compression

I save photographs on my site as jpegs and compressed them in Adobe Photoshop before further compressing them using CompressJpeg.com and occasionally TinyJPG.com (I prefer CompressJpeg because you can control the compression amount).

Graphic compression

I save graphics on my site as either jpegs or pngs. Initially, I used the method above to compress my portfolio images. After getting the first few projects setup, I installed WP Smush to automatically compress images.

Icon compression

I use PNG and SVG formats for the icons on my website; displayed either as inline SVG code (which cuts down on requests and allows CSS animation) or as CSS background images.

I initially used a popular icon font for some of the small icons on my site but found the 84KB file size wasn’t worth the download size. I could simply save the few I needed as a .png sprite sheet, reducing the download size to 2.1KB! (This was after I minified it using TinyPNG.com, I also used SpriteCow.com to speed up my process)

I minified CSS, Javascript and HTML files

I tried a number of plugins which offered CSS, HTML and JS minification but I didn’t find one which actually minified the CSS, JS and HTML, Searching Google I found this article which provided a snippet to include my theme’s function.php file.

Expand this code block
<?php
// COMPRESS FUNCTION
class WP_HTML_Compression
{
	// Settings
	protected $compress_css = true;
	protected $compress_js = false;
	protected $info_comment = true;
	protected $remove_comments = true;

	// Variables
	protected $html;
	public function __construct($html)
	{
		if (!empty($html))
		{
			$this->parseHTML($html);
		}
	}
	public function __toString()
	{
		return $this->html;
	}
	protected function bottomComment($raw, $compressed)
	{
		$raw = strlen($raw);
		$compressed = strlen($compressed);
		
		$savings = ($raw-$compressed) / $raw * 100;
		
		$savings = round($savings, 2);
		
		return '';//'<!--HTML compressed, size saved '.$savings.'%. From '.$raw.' bytes, now '.$compressed.' bytes-->';
	}
	protected function minifyHTML($html)
	{
		$pattern = '/<(?<script>script).*?<\/script\s*>|<(?<style>style).*?<\/style\s*>|<!(?<comment>--).*?-->|<(?<tag>[\/\w.:-]*)(?:".*?"|\'.*?\'|[^\'">]+)*>|(?<text>((<[^!\/\w.:-])?[^<]*)+)|/si';
		preg_match_all($pattern, $html, $matches, PREG_SET_ORDER);
		$overriding = false;
		$raw_tag = false;
		// Variable reused for output
		$html = '';
		foreach ($matches as $token)
		{
			$tag = (isset($token['tag'])) ? strtolower($token['tag']) : null;
			
			$content = $token[0];
			
			if (is_null($tag))
			{
				if ( !empty($token['script']) )
				{
					$strip = $this->compress_js;
				}
				else if ( !empty($token['style']) )
				{
					$strip = $this->compress_css;
				}
				else if ($content == '<!--wp-html-compression no compression-->')
				{
					$overriding = !$overriding;
					
					// Don't print the comment
					continue;
				}
				else if ($this->remove_comments)
				{
					if (!$overriding && $raw_tag != 'textarea')
					{
						// Remove any HTML comments, except MSIE conditional comments
						$content = preg_replace('/<!--(?!\s*(?:\[if [^\]]+]|<!|>))(?:(?!-->).)*-->/s', '', $content);
					}
				}
			}
			else
			{
				if ($tag == 'pre' || $tag == 'textarea')
				{
					$raw_tag = $tag;
				}
				else if ($tag == '/pre' || $tag == '/textarea')
				{
					$raw_tag = false;
				}
				else
				{
					if ($raw_tag || $overriding)
					{
						$strip = false;
					}
					else
					{
						$strip = true;
						
						// Remove any empty attributes, except:
						// action, alt, content, src
						$content = preg_replace('/(\s+)(\w++(?<!\baction|\balt|\bcontent|\bsrc)="")/', '$1', $content);
						
						// Remove any space before the end of self-closing XHTML tags
						// JavaScript excluded
						$content = str_replace(' />', '/>', $content);
					}
				}
			}
			
			if ($strip)
			{
				$content = $this->removeWhiteSpace($content);
			}
			
			$html .= $content;
		}
		
		return $html;
	}
		
	public function parseHTML($html)
	{
		$this->html = $this->minifyHTML($html);
		
		if ($this->info_comment)
		{
			$this->html .= "\n" . $this->bottomComment($html, $this->html);
		}
	}
	
	protected function removeWhiteSpace($str)
	{
		$str = str_replace("\t", ' ', $str);
		$str = str_replace("\n", '', $str);
		$str = str_replace("\r", '', $str);
		
		while (stristr($str, ' '))
		{
			$str = str_replace(' ', ' ', $str);
		}
		
		return $str;
	}
}

function wp_html_compression_finish($html)
{
	return new WP_HTML_Compression($html);
}

function wp_html_compression_start()
{
	ob_start('wp_html_compression_finish');
}
add_action('get_header', 'wp_html_compression_start');
?>

Note: this snippet minified the HTML perfectly but the JavaScript no longer worked – I decided the code was good enough for the HTML minification and simply changed protected $compress_js = true; to protected $compress_js = false;.

This stopped the JS from being minified. I then used jscompress.com to manually minify the JavaScript.

I inlined styles and javascript

I used a simple bit of PHP code to inline the minified CSS into the <head> section of my site and my minified JS just above the closing <body> tag.

<!-- STYLESHEETS -->
<style>
  <?php 
    echo file_get_contents('http://mysite.co.uk/wp-content/themes/mytheme/style.min.css', true);
  ?> 
</style>

<!-- JAVASCRIPT -->
<script>
  <?php 
    echo file_get_contents('http://mysite.co.uk/wp-content/themes/mytheme/js.min.js', true);
  ?> 
</script>

I created a couple of different footer.php templates to include addition javascript required for specific pages.

If you have large stylesheets, separate above-the-fold styles and only inject these into the head of your website. You can then load the remaining styles using the script in the next section.

I delayed the loading of scripts and stylesheets

I delayed the loading of non critical and blocking CSS / JavaScript files until my website had finished loading. I modified code suggested by Google to also delay the loading of JavaScript files (it was written to delay CSS originally).

// DELAY SCRIPTS AND CSS FILES
var cb = function() {
	// defer scripts
        var l1 = document.createElement('script'); l1.type = 'text/javascript';
	l1.src = '/wp-content/themes/nickmcburney/includes/js/non-critical-js.js';
	var h1 = document.getElementsByTagName('head')[0]; h1.parentNode.insertBefore(l1, h1);
	var h1 = document.getElementsByTagName('head')[0]; h2.parentNode.insertBefore(l1, h1);

        // defer css	
	var l2 = document.createElement('link'); l2.rel = 'stylesheet';
	l2.href = '/wp-content/themes/mytheme/includes/css/non-critical-css.css';
	var h2 = document.getElementsByTagName('head')[0]; h2.parentNode.insertBefore(l2, h2);
};

var raf = requestAnimationFrame || mozRequestAnimationFrame || webkitRequestAnimationFrame || msRequestAnimationFrame;
if (raf) raf(cb);
else window.addEventListener('load', cb);

See more on delaying CSS here and JavaScript here

I removed unneeded plugins

I automatically installed Contact Form 7 ( an extremely popular WordPress plugin which I use on nearly all my other websites) when I started builing this theme. It’s a great plugin but one of the downsides is that it loads various CSS/JS files on every page of the site, contact form or no contact form.

After building my website quote calculator I decided to have a go at writing my own contact form PHP script, and setup a custom template with all the code needed to collect data (from forms send HTML formatted emails out).

Im hoping to do a full write up on this in the near future

I removed jQuery and converted functions to native JavaScript

jQuery - Eliminate render-blocking JavaScript and CSS in above-the-fold content

By default, jQuery is loaded as part of the WordPress core, and its needed if your site uses front-end plugins because they will likely use jQuery to function.

I knew that my site wasn’t going to use plugins requiring jQuery and it was the last remaining blocking file, slowing the load time of the site. I had 4 jQuery functions running in the footer of my site; considering they weren’t doing anything to complicated I decided to convert these to native JavaScript and then ditch jQuery.

After a struggle I managed to convert the jQuery code to native JS and then I could stop WordPress from loading jQuery. This had a huge affect on my site; removing another blocking resource, reducing overall page size (removing jQuery reduced page size by approximately 82KB) and giving me a reason to improve my native JavaScript skills – which was needed after becoming lazy with jQuery.

I delayed the loading of below-the-fold images

I have 4 project images on the homepage of my site that were slowing the loading of the website and reducing my PageSpeed score. I decided to lazy load the images and chose to write my own simple script to do this for me (there are lots of scripts out there for handling lazy loading).

I wrote the php script producing the ‘recent projects’ to not add the img src but instead write the source to a custom data type, my script waits for the page to load and then uses the URL in the data type to populate the image source, which then loads the images.

// LAZY LOAD IMAGES
function lazyLoadImages(){
	// get all images within lazy load section
	var $images = document.querySelectorAll(".lazy-load img");
	
	// run through each image and update src
	if($images.length > 0) {
		for (var i = 0, len = $images.length; i < len; i++) {
			
			// get url from each image
			var image_url = $images[i].getAttribute("data-delaysrc");
			// update image url with data-delaysrc
			$images[i].src = image_url;
		}
	}
}
lazyLoadImages()

I modified the .htaccess file

Adding the code below enabled gzip compression, added header expires and enabled keep alive. These improvements are simple to implement and can really help improve your loading speed.

Expand this code block
## ENABLE KEEP-ALIVE ##
<IfModule mod_headers.c> 
  Header set Connection keep-alive
</IfModule>

## ENABLE GZPI COMPRESSION ##
<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE text/html
  AddOutputFilterByType DEFLATE text/css
  AddOutputFilterByType DEFLATE text/javascript
  AddOutputFilterByType DEFLATE text/xml
  AddOutputFilterByType DEFLATE text/plain
  AddOutputFilterByType DEFLATE image/x-icon
  AddOutputFilterByType DEFLATE image/svg+xml
  AddOutputFilterByType DEFLATE application/rss+xml
  AddOutputFilterByType DEFLATE application/javascript
  AddOutputFilterByType DEFLATE application/x-javascript
  AddOutputFilterByType DEFLATE application/xml
  AddOutputFilterByType DEFLATE application/xhtml+xml
  AddOutputFilterByType DEFLATE application/x-font
  AddOutputFilterByType DEFLATE application/x-font-truetype
  AddOutputFilterByType DEFLATE application/x-font-ttf
  AddOutputFilterByType DEFLATE application/x-font-otf
  AddOutputFilterByType DEFLATE application/x-font-opentype
  AddOutputFilterByType DEFLATE application/vnd.ms-fontobject
  AddOutputFilterByType DEFLATE font/ttf
  AddOutputFilterByType DEFLATE font/otf
  AddOutputFilterByType DEFLATE font/opentype

# For Olders Browsers Which Can't Handle Compression
  BrowserMatch ^Mozilla/4 gzip-only-text/html
  BrowserMatch ^Mozilla/4\.0[678] no-gzip
  BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
</IfModule>

## SET HEADER EXPIRES ##
<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType image/jpg "access plus 1 year"
  ExpiresByType image/jpeg "access plus 1 year"
  ExpiresByType image/gif "access plus 1 year"
  ExpiresByType image/png "access plus 1 year"
  ExpiresByType image/svg+xml "access plus 1 year"
  ExpiresByType text/css "access plus 1 month"
  ExpiresByType application/pdf "access plus 1 month"
  ExpiresByType text/x-javascript "access plus 1 month"
  ExpiresByType application/x-shockwave-flash "access plus 1 month"
  ExpiresByType image/x-icon "access plus 1 year"
  ExpiresDefault "access plus 2 days"
</IfModule>
## EXPIRES CACHING ##

Let me know what you think

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