Over the past few weeks I’ve been looking at different solutions to improve the speed of my WordPress websites. The first step was to mirror and redirect the static content to another server (aka Content Delivery Network or CDN). I’m currently using a DreamHost VM, but I may look into using Amazon S3 as well. This is an easy way to save bandwidth, and off-load a web server that is configured for dynamic content (larger and slower). In the case of PHP and WordPress, there are several additional options available to improve local web server performance. I’ll describe the ones I’m currently using, including their expected impact to performance and short-comings. This article deals mainly with the local Apache Httpd and PHP configuration. There are additional infrastructure solutions that can improve performance, like using Nginx servers on the front, Varnish cache servers in the middle, and Apache Httpd on the back-end (for PHP and WordPress). This post is only about optimizing the Apache Httpd back-end. If you’re considering adding a Varnish server in front of Apache Httpd, you should probably avoid caching whole pages in Apache Httpd and leave that to the Varnish servers.

Before making any change, you should run a speed and load test against your web site(s). A good baseline will allow you to compare the performance gain of each individual change. The delivery speed of a single page for a single client is important for Google Ranking, and a load test will tell you how many simultaneous connections your web server can handle.

Static Content

Content Delivery Networks (CDNs)

You can use rewrite-rules with Apache Httpd, for example, to redirect (all or some) static content to other servers. This is the technique I’ve described here to redirect static content (not modified in the last 1800 seconds). The downside to redirects is doubling the HTTP connections for that content. When you’re working to shave off 0.5 seconds here and there, those extra HTTP connections start to add up. You can also mix-and-match — use correct URLs in your web page for most static content, but leave others for Apache Httpd to redirect (or not) based on their modification time. For example, you could send all static content to a CDN (see Content Delivery Network on Wikipedia), except for the media gallery content, which would be redirected by Apache Httpd.

To send all static content directly to other servers, you’ll need a plugin to change the URLs within your PHP generated web pages. You can use the (unsupported) CDN Linker lite from the WordPress.org website, or the (supported) CDN Linker from GitHub.

You can send content to multiple CDN hostnames (cdn1.domain.com, cdn2.domain.com, etc.) and exclude certain directories and pages. For example, you could exclude the wp-content/uploads/ and wp-content/gallery/ folders, letting Apache Httpd redirect those based on the modification date of each file.

The downside to using CDN Linker, or any similar plugin, is that it redirects all content, regardless of it’s existence on the CDN servers or not. If you sync your content with the CDN at regular intervals, any new content added between those intervals will show up as a broken link.

The more static content you have on a page, the more benefit you will see from using a plugin like CDN Linker.

Javascript and Stylesheets

A WordPress page can have several links to javascript and stylesheet files, and those files can usually be combined and compressed. This reduces the number of HTTP connections, and makes the content itself smaller and faster to deliver.

The Better WordPress Minify plugin minifies and combines javascript and stylesheet files. It uses the Minify PHP library, and relies on WordPress’s enqueueing system rather than the output buffer (will not break your website in most cases). The plugin modifies the javascript and stylesheet URLs in web pages to call the minify PHP script instead, which then creates a new cache file or delivers an existing file from it’s cache.

Using Better WordPress Minify (aka BWP Minify) with the CDN Linker plugin requires some forethought. CDN Linker, by default, will rewrite the javascript and stylesheet URLs to your chosen CDN. This means that the minify PHP script will be executed on the CDN side, and any cache files created will be located on the CDN server(s). If the remote server is also able to execute PHP code, this may be a desirable feature — just make sure you don’t delete or overwrite those cached files on the CDN if / when you sync static content. ;-)

If you’d rather keep the cached & minified javascript and stylesheet files local, you’ll have to exclude /bwp-minify/ in the CDN Linker options. And if you’re setting up a new web server, or tweaking an old one, you’ll probably want to disable BWP Minify or exclude the stylesheets you’re working on.

Some stylesheet plugins can have a lower priority than BWP Minify, like Lazyest Stylesheet for example, so you’ll have to get creative. The easiest and simplest solution might be to redirect those CSS URLs to BWP Minify, as explained in their FAQ section.

Pictures and Images

Although JPG and PNG images are compressed, they’re usually not as small as they could be. There exists several utilities that specialize in compressing image files even more, and many WordPress plugins can use these to further shrink the images in your media library.

The EWWW Image Optimizer is a WordPress plugin that automatically and losslessly shrinks images as they are uploaded, or re-optimizes those that are already in your media library. The plugin also has a Bulk Optimize feature for images and thumbnails in NextGEN Galleries. It doesn’t optimize NextGEN Gallery thumbnails on upload, but these can be re-optimized individually or in bulk afterwards.

Dynamic Content

PHP Opcode Cache

Before a PHP script can be executed, it must be compiled into operational code (aka opcode), which is discarded after execution. A typical WordPress page might have dozens of PHP scripts to execute, so the compilation process can be a significant part of the total web page load time. An opcode cache keeps the compiled opcode in memory, so additional executions of the same script don’t have to be re-compiled.

The are many opcode cache applications available, including PHP’s own Alternative PHP Cache (that I’ve discussed previously), memcached, and several others. The easiest and quickest to install is probably APC, but support for APC by WordPress plugins is somewhat limited, where-as support for memcached is much broader. For example, there are some PHP web page caches — that save a copy of the fully rendered page — which use memcached, but not APC. Memcached is also a stand-alone process that can cache a variety of information from different applications, not just from PHP. Depending on your needs, memcached might be a better choice (for example, if you have several front-end servers and want to use a centralized cache).

PHP’s Alternative PHP Cache (APC) should be fairly quick and easy to install for any UNIX power-user or SysAdmin. You may only have to execute a yum install command (as root) like this one.

$ yum install php-pecl-apc 1 2 3 $ yum install php - pecl - apc

Then edit the /etc/php.ini file and add the following. You should also copy the apc.php script that comes with APC into your web server’s document root. You can use apc.php to monitor the size and fragmentation of your APC cache, and adjust it accordingly.

php.ini [apc] apc.enabled=1 apc.enable_cli=0 apc.filters="apc\.php$" apc.max_file_size=2M apc.mmap_file_mask="/var/tmp/apc.XXXXXX" apc.num_files_hint=2048 apc.shm_segments=1 apc.shm_size=256M apc.stat=1 apc.ttl=7200 apc.user_ttl=7200 1 2 3 4 5 6 7 8 9 10 11 12 13 14 [ apc ] apc . enabled = 1 apc . enable_cli = 0 apc . filters = "apc\.php$" apc . max_file_size = 2M apc . mmap_file_mask = "/var/tmp/apc.XXXXXX" apc . num_files_hint = 2048 apc . shm_segments = 1 apc . shm_size = 256M apc . stat = 1 apc . ttl = 7200 apc . user_ttl = 7200

As I mentioned in an earlier post, APC can actually save you memory, even if Apache Httpd’s processes appear to have increased in size after enabling APC. The percentage of shared memory used by those larger processes is much greater, so the actual memory used by each process is less. The script I wrote to check Apache Httpd memory usage and configured limits can be useful to compare memory use before and after enabling APC. The APC cache is easily enabled and disabled using the apc.enabled option in /etc/php.ini.

WP Object Cache

The WP_Object_Cache is WordPress’ class for caching data that may be computationally expensive to regenerate, such as the result of complex database queries. By default, the object cache is non-persistent. This means that data stored in the cache resides in memory and only for the duration of the request. Cached data will not be stored persistently across page loads, unless you install a persistent caching plugin.

The Tribe Object Cache is a persistent object cache plugin for WordPress that supports Memcached, APC, Xcache or WinCache. All you have to do is activate it. Unlike opcode caches, the Object Cache could fragment your APC cache over time, and degrade performance below optimum. Use the apc.php script to keep an eye on the fragmentation level, and restart Apache Httpd if fragmentation gets too high.

The APC Object Cache Backend is another plugin that offers persistent object caching, but it must be installed manually, where-as the Tribe Object Cache can be activated / deactivated like any other plugin.

Database Query Cache

The DB Cache Reloaded Fix caches the result of every database query into individual files on disk. This will off-load a busy MySQL database, and depending on your web server’s I/O throughput and available memory, may also improve your web page load time. Checking I/O wait and disk cache is beyond the scope of this post, but generally, if a vmstat 5 command (for example) continuously shows 0 in the b column, and free -m shows a good amount of memory under the cached column, you should be good.

2013/10/30 : Note that a local database query cache will only help if your database is remote. If your database is local, especially if you’re accessing it through a socket, for example, a query cache like this will probably degrade the performance, not improve it.

PHP Page Caching

Caching complete, pre-built PHP web pages is where you’re likely to see the biggest performance increase. Complete page caching will get the HTML page out fastest, improve your “Time To First Byte”, and possible increase your Google PageRank score.

There are several very good, and well known page caching plugins available for WordPress. Although I’ve listed PHP Page Caching last in this post — taking more of a layered presentation approach to the subject — the choice of which page cache to use should probably be your first decision. Some of the larger and more complete caching solutions offer many of the features provided by the individual plugins I’ve described here. I chose to install several plugins, instead of just one monolithic plugin, to have more control over each feature.

The Quick Cache plugin saves rendered PHP pages on disk. And like the DB Cache Reloaded Fix plugin, it assumes you don’t have any I/O bottlenecks and have enough memory available for the OS disk cache. The OS disk cache will keep a copy of frequently accessed files in memory. Relying on the OS disk cache is probably not as reliable as using memcached, for example, but it does have the advantage of offering much better performance.

2012/12/04 : For more information on the advantages of Disk Caching over Memcached (in most circumstances), see my follow-up post about Memcached vs Disk Cache.

If your WordPress theme generates different content for mobile and desktop users (or even different languages) without modifying the hostname or URL, you will have to set Quick Cache’s MD5 Version Salt. By default, Quick Cache uses $_SERVER["HTTP_HOST"] and $_SERVER["REQUEST_URI"] to encode a reference to the cached page. This means there can only be one cached version of a web page for one URL. On Surnia Ulula I’ve added $_COOKIE["platform"] to Quick Cache’s MD5 Version Salt, which allows me to have different versions of the same page, with the same URL, for mobile and non-mobile users.

Quick Cache doesn’t provide cached web pages (by default) for users that are logged in or have left a comment, and cached web pages are removed when pages or posts are updated, or the cached web page has expired. As an additional way to improve performance when the web page is not cached, you can use a plugin to cache individual widgets. The WP Widget Cache adds caching options to the settings of each individual widget. You can decide how long each widget’s content is cached for, and what conditions will remove the cached content earlier (like leaving a comment, updating a post, etc.).

Conclusion

This post has described a somewhat progressive and layered approach to addressing performance issues with WordPress. As you may have surmised from my comments, this is not a solution for everyone. It assumes you have a fairly healthy web server and use mostly off-the-shelf code. If you are developing complex, transactional and distributed web sites, I would definitely look at using memcached instead of APC. You might also skip the page caching features if you use Apache Httpd behind Nginx or Varnish.