Since last November I’ve been busy getting my website noticed by search engines. All the extra modules and tweaking resulted in an indexable, yet slow-loading website. Not a fun thought if you consider it all looks pretty simple. Google’s Webmaster Tools notified me that my site was steadily getting slower and slower.
Of course this is a challenge that needs to be crushed! The Webmaster Tools also alerted me to the existence of a Firefox addon called Page Speed, which allows me to test loading speeds of web pages. Later I also started using the YSlow addon. Both addons have Firebug as a dependency. Another tool I have been using is the website www.webpagetest.org. This sites shows you a waterfall chart of all the elements of a page that need to be downloaded, in which order they are downloaded, and how long the downloads take.
I used my résumé page as a reference, it’s got a lot of text and no weird stuff like images and whatnot. The first waterfall will show that none of the page’s elements are downloaded in parallel.
Be your own CDN
The first tip the Firefox addons gave me that I could actually do something about, was to spread the downloading of elements across multiple hostnames. By default, browsers have a setting that allows them to only open a maximum of four connections per hostname. An average simple page, contains about 30 to 40 downloadable elements. This means, that some time of the loading of the page is spent waiting to open more connections. You could alter this setting in your browser, but that doesn’t mean any of your visitors will be willing (or able) to do the same. Spreading downloads across multiple hostnames circumvent these limitations. The easiest and cheapest way to do this, is to make use of CNAMEs and ServerAliases.
The CNAMEs are purely for ease of management. If you ever need to move your website to another server, you will only need to change one IP address. You can of course choose to use A records. In this setting, the ServerAliases are handy, because all the files are already there and you don’t need to copy them anywhere else. You could call this setup a poor-man’s CDN (Content Delivery Network). Poor-man’s, because it’s not really a CDN.
Step 1: DNS
The DNS settings are best done, one day before you want to do any real work. DNS settings take a while to propagate across the Internet, which could result in some parts of your website becoming unavailable if you do things too fast. Create a set of CNAMEs pointing towards your site, I think any amount between four and six CNAMEs should do nicely. Most browsers only allow a maximum amount of 30 concurrent connections. With 6×4=24 connections, you leave a little bit of headroom for other pages to load in the background. You can of course experiment with these settings. I chose to use cdn01.eelcowesemann.nl to cdn06.eelcowesemann.nl. All of them being CNAMEs pointing towards www.eelcowesemann.nl.
Step 2: Webserver
This part can be done at the same time as the CNAMEs, as these settings don’t force anything yet. Add a set of ServerAliases (I use Apache 2.2, if you use something else, please read the documentation.) for the created CNAMEs:
ServerName eelcowesemann.nl
ServerAlias www.eelcowesemann.nl
ServerAlias cdn01.eelcowesemann.nl cdn02.eelcowesemann.nl
ServerAlias cdn03.eelcowesemann.nl cdn04.eelcowesemann.nl
ServerAlias cdn05.eelcowesemann.nl cdn06.eelcowesemann.nl
The ServerAliases don’t need to be split across multiple lines, I just like to do it this way because it improves readability. After saving these settings, you can (gracefully) restart Apache.
Step 3: WordPress Plugin
Of course we need WordPress to do some magic as well. We need a CDN plugin to do this, you can find one in the WordPress plugin directory. I chose to use My CDN, it does exactly what I need without any frills. There are also quite a few plugins which support commercial CDNs, you might want to check these out in case your site is a wee bit more commercial.
You can configure My CDN through the Settings menu in the WP-Admin dashboard.
I’ve set the Original Site URLs setting to the URLs that are normally used to access my site:
http://www.eelcowesemann.nl,http://eelcowesemann.nl
These are comma separated values.
I left the Excluding URL patterns as it was originally.
I set the Javascript file pre-URL setting to contain all my CDNs, also comma separated:
http://cdn01.eelcowesemann.nl,http://cdn02.eelcowesemann.nl,http://cdn03.eelcowesemann.nl,http://cdn04.eelcowesemann.nl,http://cdn05.eelcowesemann.nl,http://cdn06.eelcowesemann.nl
I copied this setting to CSS file pre-URL and Theme file pre-URL.
With these settings you should already see some results. This more recent waterfall contains a few tweaks that will be discussed at a later time, but it is already visible that the page’s elements are already being downloaded in parallel. Most site will benefit from this. Sites which contain a lot of small elements within one page will probably benefit the most.
Optimizing Apache
This next section covers the more complex and powerful features Apache provides for speeding up the loading of a site. These aren’t necessarily WordPress-specific.
Leveraging the browser cache: mod_expires
Browsers have the ability to temporarily save data to the hard drive: the Cache. By caching, files only need to be downloaded once. Each following time a file us needed, these can be loaded from the local cache, instead of being downloaded. To aid browsers with caching, you can load the mod_expires Apache module.
ExpiresActive On
Once enabled, the caching can be tuned with two directives:
ExpiresDefault "access plus 2 months 1 day 1 hour" # most files don't change often
ExpiresByType text/html "access plus 1 week" # html will change somewhat more often
ExpiresByType text/css "access plus 1 week" # css probably will too
- ExpiresDefault The default time items may reside in cache.
- ExpiresByType Allows you to tune the caching by mime-type. When ExpiresDefault is set, ExpiresByType can be used to define exceptions. If ExpiresDefault is not set, caching will only be used for the mime-types you configure with ExpiresByType.
The mime-types that Apache understands are usually loaded from a mime-types ‘database’ (a plain-text file). Apache can be told from which file to load the mime-types by using the TypesConfig directive. For Red Hat, the default location for the mime-types database is /etc/mime.types
, in FreeBSD the default location is /usr/local/etc/apache22/mime.types
.
Setting the expire time can be done in years, months, weeks, days, hours and seconds. As you can see in the above example for ExpiresDefault, these can be combined.
The starting time from when the expire time will be calculated can be set in three ways:
- access Start from the time the file was requested.
- now Same as access.
- modification Start from the moment the file was modified (or created, which is also a modification) on the server’s hard drive.
Using modification is only of use when the files are not dynamically created, but loaded directly from the servers hard drive. Dynamic files have no modification time, so a modification header will not be sent along with them.
Compress Apache output with mod_deflate
In the good old Apache 1.3 era, mod_deflate
was better known as mod_gzip
. Mod_deflate is a real time compression method which proves most useful when handling plain text files. By compressing the outgoing data, less data needs to be transmitted, which in turn leads to pages that load faster. It is good practice to not let mod_deflate compress files that are already compressed, like zip files or images. The extra compression is minimal and it basicly wastes CPU cycles.
Mod_deflate can be enabled by placing an outputfilter:
SetOutputFilter DEFLATE
The compression level can be set using DeflateCompressionLevel
:
DeflateCompressionLevel [1-9]
The compression is minimal with 1, and maximum with 9.
By now, the following setting will probably not be needed anymore, but it shows how exceptions can be made for browsers:
# only partial gzip support
BrowserMatch ^Mozilla/4 gzip-only-text/html
# or none at all
BrowserMatch ^Mozilla/4\.0[678] no-gzip
# MSIE does support it and this rule exempts it from previous rule
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
Because we don’t want to compress images, zips, pdf’s and other compressed files any further, we add the follwing rule:
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|pdf|zip|rar)$ no-gzip dont-vary
# Help proxies deliver the right content to their clients
Header append Vary User-Agent env=!dont-vary
The Vary header is added to help proxy servers serve their clients with content-encodings the clients actually understand.
The big picture
All settings put together gives us the follwing piece of configuration:
# mod_expires start
ExpiresActive On
ExpiresDefault "access plus 2 months 1 day 1 hour"
ExpiresByType text/html "access plus 1 week"
ExpiresByType text/css "access plus 1 week"
# mod_expires end
# mod_deflate start
SetOutputFilter DEFLATE
DeflateCompressionLevel 9
# only partial gzip support
BrowserMatch ^Mozilla/4 gzip-only-text/html
# or none at all
BrowserMatch ^Mozilla/4\.0[678] no-gzip
# MSIE does support it and this rule exempts it from previous rule
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
# Don't touch already compressed files
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|pdf|zip|rar)$ no-gzip dont-vary
# help proxies
Header append Vary User-Agent env=!dont-vary
# mod_deflate end
These settings can be appended to Apache’s main configuration.
Does it work?
Of course we want to know if all these settings actually do something. Apache can be told to log the level of compression:
DeflateFilterNote Input instream
DeflateFilterNote Output outstream
DeflateFilterNote Ratio ratio
SetEnvIf Request_URI \.gif image-request
SetEnvIf Request_URI \.jpg image-request
SetEnvIf Request_URI \.jpeg image-request
SetEnvIf Request_URI \.png image-request
SetEnvIf Request_URI \.zip image-request
SetEnvIf Request_URI \.rar image-request
SetEnvIf Request_URI \.pdf image-request
LogFormat '%V "%r" %{outstream}n/%{instream}n (%{ratio}n%%)' deflate
CustomLog /var/log/httpd/deflate.log deflate env=!image-request
The DeflateFilterNote settings tell Apache to append compression ratios to the logs. The SetEnvIf settings tell Apache not to log this for certain file extensions (we don’t compress these anyway). The results can be seen in the newly made logfile /var/log/httpd/deflate.log
.