As a follow-up to my post on compressing .php, .css and .js files without mod_gzip or mod_deflate, I’m documenting the changes I made to the .htaccess file on ardamis.com in order to speed up page load times for returning visitors and satisfy the Leverage browser caching recommendation of Google’s Page Speed Firefox/Firebug Add-on.
A great explanation of why browser caching helps the web deliver a better user experience is at betterexplained.com.
Two authoritative articles on the subject are Google’s Performance Best Practices | Optimize caching and Yahoo’s Best Practices for Speeding Up Your Web Site | Add an Expires or a Cache-Control Header.
I’d like to point out that in researching browser cashing, I came across a lot of information that contradicted the rather clear instructions from Google:
It is important to specify one of
Expires
orCache-Control max-age
, and one ofLast-Modified
orETag
, for all cacheable resources. It is redundant to specify bothExpires
andCache-Control: max-age
, or to specify bothLast-Modified
andETag
.
I’m not sure that this recommendation is entirely correct, as the W3C states that Expires and Cache-Control max-age are used in different situations, with Cache-Control max-age overriding Expires in the event of conflicts.
If a response includes both an Expires header and a max-age directive, the max-age directive overrides the Expires header, even if the Expires header is more restrictive. This rule allows an origin server to provide, for a given response, a longer expiration time to an HTTP/1.1 (or later) cache than to an HTTP/1.0 cache.
It would seem that Cache-Control is the preferred method of controlling browser caching going forward.
HTTP 1.1 clients will honour “Cache-Control” (which is easier to use and much more flexible).
HTTP 1.0 clients will ignore “Cache-Control” but honour “Expires”. With “Expires” you get thus at least a bit control for these old clients.
In any event, Page Speed won’t protest if you do end up sending both Expires and Cache-Control max-age, or if you remove both Last-Modified and ETag, but I was able to get the best results with just setting Cache-Control max-age and removing the ETag.
Setting the headers in .htaccess
On Apache, configuring the proper headers can be done in the .htaccess file, using the Header
directive. The Header
directive requires the mod_headers
module to be enabled.
I’m choosing to set a far future Expires header of one year on my images files, because I tweak the CSS and JavaScript pretty often, and don’t want those file types to be cached as long.
Add the following code to your .htaccess file to set your Cache-Control and Expires headers, adjusting the date to be one year from today.
# Set Cache-Control and Expires headers <filesMatch "\\.(ico|pdf|flv|jpg|jpeg|png|gif|swf|mp3|mp4)$"> Header set Cache-Control "max-age=2592000, private" Header set Expires "Sun, 17 July 2011 20:00:00 GMT" </filesMatch> <filesMatch "\\.(css|css.gz)$"> Header set Cache-Control "max-age=604800, private" </filesMatch> <filesMatch "\\.(js|js.gz)$"> Header set Cache-Control "max-age=604800, private" </filesMatch> <filesMatch "\\.(xml|txt)$"> Header set Cache-Control "max-age=216000, private, must-revalidate" </filesMatch> <filesMatch "\\.(html|htm)$"> Header set Cache-Control "max-age=7200, private, must-revalidate" </filesMatch>
Removing ETags in .htaccess
Most sources recommend simply removing ETags if they are not required.
Entity tags (ETags) are a mechanism that web servers and browsers use to determine whether the component in the browser’s cache matches the one on the origin server.
…
If you’re not taking advantage of the flexible validation model that ETags provide, it’s better to just remove the ETag altogether.
Add the following code to your .htaccess file to remove ETag headers.
# Turn off ETags FileETag None Header unset ETag
Set Expires headers with ExpiresByType (optional)
If your host has the mod_expires
module enabled, you can specify Expires headers by file type. Godaddy does not have this module enabled.
# Set Expires headers ExpiresActive On ExpiresDefault "access plus 1 year" ExpiresByType text/html "access plus 1 second" ExpiresByType image/gif "access plus 2592000 seconds" ExpiresByType image/jpeg "access plus 2592000 seconds" ExpiresByType image/png "access plus 2592000 seconds" ExpiresByType image/x-icon "access plus 2592000 seconds" ExpiresByType text/css "access plus 604800 seconds" ExpiresByType text/javascript "access plus 604800 seconds" ExpiresByType application/x-javascript "access plus 604800 seconds"
Removing the Last-Modified header in .htaccess (optional)
I’m following Google’s instructions and not removing the Last-Modified header, but if you wanted to do so, you could use:
# Remove Last-Modified header Header unset Last-Modified
Busting the cache when files change
What happens when you change files and need to force browsers to load the new files? Christian Johansen offers two methods in his post on Using a far future expires header.
Hi,
I’ve added mentioned code in .htaccess file, however I still get error report in google’s page speed report. Why is that so?
Kind regards
Cheers for the article. Was helpful among a sea of contradictions.