Making WordPress fly on a single host

WordPress is an awesome tool for blogging. It is extremely feature rich out of the box, is complemented by a swath of plugins for any functionality it doesn’t have, and if that still isn’t enough it’s possible to edit and tweak down to the smallest detail to make it absolutely perfect for what you have in mind. Unfortunately, one of those tweaks is speed.

After optimisation, my landing page cold-loads in 130ms, of which only 50ms is spent in PHP

I say unfortunately, as I believe speed — like security — should not be an afterthought. A lot of the items on this list are items that only need a manual configuration and they’re done, but the manual configuration is a step on the way. It is a journey well worth it, though: after running through all the hoops, the PHP generation only needs 50ms to load a page, with the rest spent in networking. This can further be improved with HTTP/2 that I will discuss later.

Also, it is worth noting that while I am writing up this piece, it is essentially a collection of tips, guides and tricks that I have found around the web. Sadly I do not have proper notes on that (it was a LOT of googling) so I cannot credit them properly, but I am putting it together as an omnibus piece as it is the full guide I wish I had when I started this journey. So, without further ado, let’s start optimising for a faster WordPress!

Step 1: PHP

By far the easiest way to a faster WordPress is by using PHP 7. Without any other optimisations, this downright halved the loading time of my test blog. My webhost, Dreamhost, defaults to PHP 5.6, which works but is not nearly as fast. This should become the default soon enough. I did not encounter issues with WordPress itself when using PHP 7, but some plugins were not happy, especially the memcached wordpress object cache.

Overall, the site is running fine and is happy, so I would absolutely recommend PHP 7 as a WordPress baseline.

Step 2: Opcache

This is the first of two types of caches that we will deal with when it comes to WordPress. This cache contains the compiled php files. This is effectively the result of PHP reading the WordPress source, and then storing the result in memory. If we did not use an Opcache, every single page loaded would have to read, parse and compile every single php file needed for the page, a very CPU and disk-intensive process.

As this is such a basic item on the list, there have been many, many implementations over the years. eAccelerator, APC, Zend cache, XCache have all come, been used, and been overshadowed for whatever reason. For me, opcache was pre-built by Dreamhost, but not included by default. This was easily fixed by including it in my PHP config (phprc for dreamhost)

zend_extension=/usr/local/php70/lib/php/extensions/no-debug-non-zts-20151012/opcache.so

I did not need any further config on my moderately-little used website. This provided an additional, significant, speed boost over the Dreamhost-default XCache. We’re not talking much, but every bit counts. Note that opcache will, per default, check the filesystem for changes after two seconds. That is, after two seconds, before it executes a cached script, it will check for any changes of it on the filesystem. This essentially means the full speed boost (not touching disk) is only for any requests within those two seconds (configurable, of course) but for most websites that is more than plenty (i.e. it takes the edge off the highest of volume loads).

Step 3: Object Cache

This is where it gets tricky. At this level of caching, the usual recommendation of quick searching is something like WP Super Cache. This is an extremely popular and highly compatible plugin that works on a fairly simple premise: once a page has been rendered, save it as a static file, and then serve the static file to the next visitor. It is very fast when it works, cannot be beat if direct file access is used, and broadly compatible. It is not surprising that it’s so widespread, and if your web server cannot run persistent processes, this is what you should use. Feel free to go to the next step.

If you can run processes, there are places you can go in addition to WP Super Cache. These speed up page generation for logged in users as well, such as yourself, dear admin. The go-to here is memcached, simply put a memory cache that runs as a separate process and can be read and written from many processes. WordPress will store stuff here — whole webpages, intermediate results, etc — and retrieve it from blazingly fast local RAM rather than disk or SQL. There’s just one teensy tiny problem: the most common memcached solutions for WordPress won’t work in PHP 7, nor will they work with the php memcached extension; they require the older, and not updated, memcache extension (note the lack of d).

Unfortunately, the only plugin I found on this area was WP-FFPC, which admittedly works very well, but comes with a warning label that development is “on hold”. Notwithstanding that, it does an excellent job of caching pages and objects in RAM. Had I hopped over permanently to nginx, it allows nginx to directly interface with memcached as well, which should theoretically knock the socks off WP Super Cache.

Step 4: Image Resizing

Now, this might sound off, as Jetpack already provides an image resizer for you. However, as any free CDN, I’ve found that sometimes it’ll slow down for reasons I cannot explain. If I’m going to do it, I should control it myself, is what I’m saying. The choice here ended up being the oddly named EWWW Image Optimizer, which not only automatically cleared jpeg and pngs for me, it also created webp copies of all images. For Chrome users, this is a great improvement.

The major speedup here was on mobile (which stood for more than 50% of my traffic, so it was well worth doing!). Especially on 3g, this more than halved the time it took to load a page on my site, with no work needed from me. On Android, this went even further.

Step 5: CSS Minification

Jetpack, WP SuperCache and even WP-FFPC happily cache items for you, but they don’t really take the time to actually minify them for you. Minification is a very useful step: it takes HTML, CSS and JavaScript files and removes stuff the browser doesn’t need: comments, inline documentation, spaces, tabs, newlines and other non-syntactical characters. Not only does this mean less data to transfer, but it also means the web page needs less CPU to render on the target browser. Minified source files will be parsed quicker, as the receiving browser no longer needs to process useless text.

That’s great! For this, I used Fast Velicoty Minify, a plugin that does exactly the minification. The results are static CSS and JS files that are served up by the web server, skipping a lot of php work.

Step 6: Compression & Caching

Wait, caching again? Yes, but on the client side. WordPress is able to handle gzipping and caching for anything we load dynamically, but we’ve been naughty boys and pushed a lot of content out of PHP. Images, CSS, JS and, if used, WP Super Cache files are all served up by the plain web server. It’s time to crack open the .htaccess file and add some stuff.

If you’ve done what I did, the htaccess file will already contain stuff from WordPress (permanent URIs) and EWWWIO. We now add the following block:

SetOutputFilter DEFLATE
SetEnvIfNoCase Request_URI "\.(?:gif|jpe?g|png|webp|pdf)$" no-gzip

<FilesMatch "\.(ico|pdf|jpg|jpeg|png|gif|js|css)$">
Header set Cache-Control "max-age=604800, public"
</FilesMatch>

Essentially, we’re saying “DEFLATE” everything (a very, very simple compression). But don’t do it for images (there’s really no point to compress images). Oh, and if the file is an image, pdf, css or js file, add a cache header. This will allow the browser to keep a copy of that image/script so that it doesn’t have to download it all the time. On my blog, even minified CSS adds up to 377k data. Deflate takes that down to 104kb over the wire, but it’s still 100kb more than nothing, which is what the cache control allows us to keep it at.

Step 7: The part I didn’t manage

The sum of the above took my page load time down from 5 seconds to 1 second (cold) and less than 500ms (hot). That’s a lot. There is one, final, oomph that could have been done, however: nginx.

So far, everything I’ve discussed uses apache as the web server. It is a very capable web server, and highly compatible. Nginx, however, is faster. It’s also better at supporting HTTP/2, the newer, speedier version of HTTP. I managed to get the site going with nginx, but it was not stable. The biggest contention was between EWWW and nginx, as well as a few bugs in Dreamhost’s nginx management. I may yet get this done.

I measured a slight speed improvement with the plain nginx change, but not enough for me to continue down that path. For now.

Leave a Reply