The Quest For Speed

A little more than a month ago, I logged into my Google Webmaster Tools account and to check if Google had detected any crawl errors or any other problems with my site. Finding everything in good order, I was just about to close the browser tab when I noticed a sidebar link that had somehow escaped my attention in the past – Site performance (it’s under Labs). I clicked it and was appalled to discover that my site was “slower than 68% of sites”.

Naturally, I couldn’t leave it that way. Optimization problems are among my favourites. So I set out to speed up my site as much as possible, and after a month of off-and-on optimization efforts I managed to get the average load time down from 4.6 seconds to 1.5 seconds. In this post I’ll summarize all the performance tricks and techniques I used to achieve that. They are loosely ordered from most widely applicable to most narrow/esoteric.

Here’s how my site performance graph looks now (click to enlarge):

Site performance graph from Google Webmaster Tools

You will undoubtedly notice that after reaching 1.5 seconds it gradually returns to 2.3. This was a calculated decision on my part, and I’ll discuss it after at the end of this post.

Combine and compress CSS files

Technically there is a number of ways to do this automatically. However, I was too lazy to do it “right”. Instead, I combined the files by hand and edited my theme’s header.php to include the combined CSS file instead of the default style sheet.

Resources

Remove unused JavaScript

There was this one JS file that didn’t seem to actually do anything. After some investigation, I discovered that WP was including it because I had comment threading turned on. Since my theme doesn’t actually support comment threading, I simply disabled it in Settings -> Discussion. This got rid of the .js file.

Resources

Remove unused selectors

Small CSS =  faster downloads.

Resources

Set far expires headers for static content

Far-future “Expires” headers allow browsers keep static content – e.g. CSS, JS and images – in their cache and avoid re-downloading the same files unnecessarily. It’s as simple as adding a couple of lines to your .htaccess:
# Far-future "Expires" header for static files
<FilesMatch "\.(ico|jpg|jpeg|png|gif|js|css|swf)$">
ExpiresActive On
ExpiresDefault "access plus 8 weeks"
</FilesMatch>

Disable ETags

Again, just add this to your .htaccess:
# Disable ETags
FileETag none

Minify the HTML

Smaller page = faster page. I used the WPSCMin plugin for WP Super Cache to strip redundant whitespace from my pages and re-organize tags for better compressibility.

Resources

Use CloudFront CDN with origin pull

On the one hand, using a Content Delivery Network is a huge overkill for a small(ish) site like mine. On the other, it does improve performance for many visitors, and it helps with Reddit-proofing one’s site. It’s also ridiculously cheap – my CloudFront bill for December was around $1.30.

There are two ways to get your files into the CloudFront CDN. You can either upload them to Amazon S3 and give CloudFront the bucket name, or you can set up a “custom origin” distribution that makes the CDN automatically pull the required files from your server on an as-needed basis. I went with I went with the second option as the obviously easiest one.

Note, however, that the AWS CloudFront console lacks the ability to create custom origin distributions. You need to use a third-party tool like CloudBerry instead.

Resources

Enable preload mode in WP-SuperCache

By default, WP-SuperCache caches a post when someone first visits it. So if a post is visited only rarely, it’s cache entry is likely to expire before someone visits it again, and the next visitor will need to wait for the post to be re-generated. With preload on, WP-SuperCache automatically pre-caches all posts. This ensures that even less popular pages are will have a cached version available and load quickly.

Resources

Convert theme images to CSS sprites

This reduces the number of HTTP requests the browser has to make to render the page. Ideally, there should be only one image that the browser has to load to render all the decorative elements on the page.

Resources

Convert CSS sprites to data: URIs and embed them in the combined CSS file

And this reduces the number of HTTP requests even further. There are ways of automating data URI generation (see the “Resources” section), but I just put my sprites through an online converter and edited the style sheets by hand.

Note that data: URIs don’t work in IE 7 and IE 6, so be sure to provide a fall-back style sheet specifically for those despicable two.

Resources

Optimize popular content images

On this site, there are a couple of pages that get a lot more traffic than the rest.  By fine-tuning the images used on those pages to load as quickly as possible, I (hopefully) improved the first-visit experience for a significant fraction of this site’s visitors.

In practice, I uploaded the images to Smush-it, then ran the results through PNGCRUSH. For the trickier ones, I spent an hour or so playing with lossy compression options in Photoshop.

Resources

Load AdSense asynchronously

AdSense ads are loaded synchronously. This means that when the browser encounters an AdSense ad, it will wait until the ad has completely finished downloading before continuing to load the rest of the page. This can slow down your site considerably.

There is, however, a hack that lets one load AdSense asynchronously – in parallel with other content. The details are extremely involved, so I’ll just give you an implementation example:

<div id="adsense_ad" style="width:336px;height:280px;"> </div>
<script type="text/javascript" src="path-to/writeCapture-1.0.5-nolib-min.js"></script>
<script type="text/javascript"><!--
	google_ad_client = "ca-pub-1234567890";
	google_ad_slot = "987654321";
	google_ad_width = 336;
	google_ad_height = 280;
	//Async AdSense
	writeCapture.html('#adsense_ad', '<scr'+'ipt type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"><'+'/script>');
//-->
</script>

Dire warning: While I believe that the AdSense ToS doesn’t explicitly forbid this technique, I also cannot claim that they allow it. Use at your own risk!

Resources

Remove unnecessary HTML

Lets face it, WordPress isn’t exactly thrifty with its markup. For example, it adds a whole slew of META and LINK tags to each page – a Windows Live Writer manifest, a shortlink, a “generator” tag displaying the WordPress version, three different types of feeds, and so on. Each of those tags has a purpose, but in practice only a few will be relevant for your site. You can save a Kb or two by removing the useless ones.

I removed 7(!) useless tags by adding this code to my theme’s functions.php file:

function removeUselessMeta() {
	remove_action('wp_head', 'rsd_link');             //EditURI
	remove_action('wp_head', 'wlwmanifest_link');     //WLW Manifest
	remove_action('wp_head', 'wp_shortlink_wp_head'); //Shortlink
	
	//My theme already outputs a general feed autodiscovery tag, so no need for more.
	//Also disables comments feed autodiscovery (I doubt anyone really uses it).
	remove_action('wp_head', 'feed_links', 2);
	remove_action('wp_head', 'feed_links_extra', 3);
	
	//No, don't tell the whole world what version of WP I'm running.
	remove_action('wp_head', 'wp_generator');
	
	//Disable rel='start' and rel='index' link tags. They make no sense on a site that
	//doesn't contain multi-page/series posts.
	remove_action('wp_head', 'start_post_rel_link');
	remove_action('wp_head', 'index_rel_link');
}
add_action('init', 'removeUselessMeta');

You can trim away some more bytes by removing optional tags and optimizing your layout and CSS so that, for example, common class names are not repeated more often than necessary.

Resources

Cache the FeedBurner counter and convert it to PNG

A minor optimization to be sure, but it saves one DNS query and the PNG version of the feed counter is about half the size of the original GIF. Here’s the PHP script I use to create and display the cached image:

<?php
//Configuration
$feed_id = 'wshadowcom';
$bg_color = 'B9CFFF';
$fg_color = '330000';
$cache_duration = 3600*12;
$cache_filename = $feed_id . '-cached.png';

if ( !file_exists($cache_filename) || (time() - filemtime($cache_filename) > $cache_duration) ){
	//Retrieve the original badge
	$image_url = sprintf('http://feeds.feedburner.com/~fc/%s?bg=%s&fg=%s&anim=0', $feed_id, $bg_color, $fg_color);
	$image = imagecreatefromgif($image_url);
	
	//Save as PNG
	imagepng($image, $cache_filename, 9, PNG_NO_FILTER);
	
	//Cleanup
	imagedestroy($image);
}

//Output the resulting image
header('Content-type: image/png');
header('Content-Length: ' . filesize($cache_filename));
header('Cache-Control: max-age=' . $cache_duration);
readfile($cache_filename);
?>

Lazy-load avatars

The overwhelming majority of visitors never leave a comment. Chances are, most don’t even scroll down to the comments section. So why waste their time and bandwidth by loading avatars that they’ll never see? A better option is to delay the loading of avatars until the user has scrolled far enough to actually see them.

My implementation of this trick is too large to fit on this page, so I will leave it for a future post.

Resources

Disable AdSense (if you can afford it)

Finally, I disabled my AdSense ads and left them off for several days to test the impact they have on the average page load time. Surprise, surprise: the load time went down almost by a whole second. This is the steep 800 ms dip you saw on the performance graph – from ~2.3 seconds to ~1.5. The gradual increase back to 2.3 is me deciding that $XX/day from AdSense is worth the performance hit and re-enabling ads.

I must admit I’m a bit miffed about these results. Consider: a single block of AdSense adds around 800 milliseconds to the page load time. By looking at the graph from Google Webmasters Tools, we can infer that Google sees a site as “fast” if it loads in around 1.5 seconds. So if you use AdSense and want your site to meet that speed criteria, everything else on your page must load in 700 milliseconds or less.

You’d have to do better a job at performance optimization than frickin’ Google did with AdSense.

Further Considerations

Here are a few things I considered but didn’t attempt either because of their tediousness or high costs:

  • Clean up the theme even further. There’s still a fair bit of redundancy in the markup and CSS.
  • Get a DNS anycast provider for my domain name.
  • Switch from Google Analytics to a something that doesn’t use JavaScript (e.g. AWStats).
  • Experiment with different CDNs.
  • Set up a Varnish cache for WordPress.
  • Get more points-of-presence around the world. I’ve noticed that with the current setup – one server in USA – the time it takes to download the page HTML alone varies greatly depending on where the client is located.
Related posts :

18 Responses to “The Quest For Speed”

  1. Slushman says:

    What about using W3 Total Cache instead of Super Cache? It will minify the CSS & Javascript for you and work with a CDN for images.

  2. White Shadow says:

    The reason I’m not using W3 Total Cache is inertia, mostly. WP Super Cache works well enough, so I don’t feel a pressing need to switch to an alternative. For a new site, I’d probably give W3TC a try.

  3. Looking at the load times, I’m guessing you’re also using Apache. If so, you should try Nginx. It’s amazing the speed & resource efficiency improvements it can deliver.

  4. White Shadow says:

    Yep, Apache it is.

    I’ve considered giving Nginx a try, but, while it would probably help with server load, I doubt it would greatly impact page load times. From the benchmarks that I’ve run, bandwidth and network latency seem to be the main bottlenecks for this site.

  5. Nginx is not compatible with Apache rewrite rules, so while I would love to switch to it, I’m always stopped by the myriad of conversions that would need to take place.

    Great analysis otherwise, WS. I tried W3TC the other week and found it to be powerful but still buggy, so I’m still with WPSC (especially after Donncha fixed the bug I reported on feeds not getting cached). WPSC now also has cdn support built-in which is really nice.

    Finally, W3TC’s minify implementation just sucks – it’s broken and inefficient. WP-Minify does a superb job instead.

    Cheers – I’d love to see your lazy load of avatar images next.

  6. MK Safi says:

    What about switching to an expensive hosting service, if you can afford it? How much faster will a website become — compared to shared hosting with Bluehost, HostGator, Dreamhost, and the rest?

  7. White Shadow says:

    I switched this site to ServInt VPS a couple years ago. I wasn’t tracking performance very closely at that time, but I think it didn’t make a noticeable difference at my traffic levels (<100K pageviews/month).

  8. […] some magic)HomeBlogWordPress PluginsPopular PostsContact Lazy-Load AvatarsIn my previous post, The Quest For Speed, I mentioned that lazy-loading avatars is a good way to improve your site performance:The […]

  9. Milan says:

    There is, however, a hack that lets one load AdSense asynchronously

    This have sense if visitor is regular visitor or will visit several pages. Reason for this is because in order to load asynchronously it needs to first download a library which enables that so with empty cache it is not that much useful.

    Switch from Google Analytics to a something that doesn’t use JavaScript (e.g. AWStats).

    How would that work with caching turned on?

    Get more points-of-presence around the world. I’ve noticed that with the current setup – one server in USA – the time it takes to download the page HTML alone varies greatly depending on where the client is located.

    How will that work with WordPress and dynamic content?

    The gradual increase back to 2.3 is me deciding that $XX/day from AdSense is worth the performance hit and re-enabling ads.

    Congratulation on that, its great that you can get that much here.

  10. White Shadow says:

    How would that work with caching turned on?

    AWStats and friends work by analysing Apache logs, so they do fine even if you have caching turned on. There are, however, some highly advanced caching mechanisms like Varnish that might confuse even AWStats. In that case Google Analytics is probably your best bet.

    How will that work with WordPress and dynamic content?

    Trade-offs would have to be made. There are two general approaches that I can see – either port everything that requires dynamism to JavaScript (e.g. use Disqus for comments), or just accept that the some of your visitors will sometimes see slightly out-of-date content. The route you choose will depend on your specific goals and requirements. You can also do a little bit of both, of course.

  11. MK Safi says:

    In removing unnecessary WordPress head HTML, why are you hooking to “init”? I placed the remove_action() commands directly in functions.php (without hooking to anything) and they worked fine.

    And since I use WordPress as a CMS, I didn’t need the rel=”previous”, rel=”next” links in the header, so I added this to remove them:

    remove_action(‘wp_head’, ‘parent_post_rel_link’);
    remove_action(‘wp_head’, ‘adjacent_posts_rel_link’, 10, 0);
    remove_action(‘wp_head’, ‘adjacent_posts_rel_link_wp_head’, 10, 0);

  12. White Shadow says:

    Err, probably force of habit, doing everything in callbacks. I suppose you’re right – hooking them to “init” was unnecessary.

  13. Jan Husdal says:

    Nice post. I learned a lot from this.

    Just a word of caution regarding the Site Performance graph in GWT. It is based on data collected from users having the Google Toolbar installed when browsing your site, and it is thus a rather biased measure. However, the Crawl Stats graph in the Diagnostics section of GWT showing the time spent downloading a page is probably more accurate.

    What I did find interesting though was your AdSense experiment. I made the same discovery, which in the end prompted me to remove them since the xx$ I was making from it really wasn’t worth the lag.

    As to CDN, my experiences (from using Amazon Cloudfront) is that when it’s fast it really is superfast. However, it varies a lot, and my download speed monitor looks like a rollercoaster track, with anything from 100ms to 2000ms, depending on the location.

  14. White Shadow says:

    Yes, I’m aware that the graph is generated from Google Toolbar data. In my opinion, this is a good thing. It shows how fast the site loads for real users with normal Internet connections. Most other performance testing tools are synthetic benchmarks which, while useful for certain tasks, generally don’t reflect real-world performance.

    Speaking of CDNs, I recently stumbled upon CloudFlare. They appear to be offering an easy way to set up CDN + Anycast DNS + reverse caching proxy + a bunch of other interesting optimizations. Might give them a try in the future.

  15. […] to page speed in WordPress, that was all of it. If you want speed beyond caching, check out the Quest for Speed guide.Leave a comment Cancel commentYour email address will not be published. Required fields are […]

  16. […] – a month at most for website data. $1.22 – in a month for website images. $1.30 – to make site go faster . $1.50 – to host nutritional leaflet each month. $1.69 – for a months image hosting to make […]

  17. […] the last two months, I optimized this site to load almost 2x faster. After the optimization was complete, the amount of Google search traffic […]

  18. Ashton says:

    Hi everyone, it’s my first go to see at this web site, and article is truly fruitful in support of me, keep up posting these articles or reviews.

Leave a Reply