powered by apache During a meeting at work today, we looked at my web site (as an example) to see if there are ways the performance could be improved. I was surprised to see how many things could be done to make it a big faster.

One of the most trivial changes involved adding an Expires: header which specifies a time far enough in the future so that clients (browsers) won't try to re-fetch images that haven't changed.

Since I'm running Apache 1.3, I dug up the old documentation on mod_expires and made a few simple additions to httpd.conf:

ExpiresActive On
ExpiresByType image/gif A2592000
ExpiresByType image/png A2592000
ExpiresByType image/jpg A2592000
ExpiresByType image/jpeg A2592000

That asks Apache to set an Expires header that's roughly one month from the moment at which the browser requested the image. Here's a quick test to show that it worked:

root@litterbox:~/w/i# telnet localhost 80
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
HEAD /i/mini-softie.jpg HTTP/1.0
Host: jeremy.zawodny.com
...
HTTP/1.1 200 OK
Date: Mon, 23 Jul 2007 22:03:35 GMT
Server: Apache/1.3.34 (Debian) mod_gzip/1.3.26.1a PHP/4.4.4-8
Cache-Control: max-age=2592000
Expires: Wed, 22 Aug 2007 22:03:35 GMT
Last-Modified: Tue, 29 May 2007 15:51:04 GMT
ETag: "95c67-5995-465c4be8"
Accept-Ranges: bytes
Content-Length: 22933
Connection: close
Content-Type: image/jpeg

Excellent. August 22nd is about a month from now.

In theory I could go quite a bit farther into the future since I almost never replace images on my site.

Posted by jzawodn at July 23, 2007 02:14 PM

Reader Comments
# Craig Hughes said:

curl -I is your friend

on July 23, 2007 03:26 PM
# Jeremy Zawodny said:

Bah.

And stop typing my GET and HEAD requests by hand?!?! Next thing you know someone will suggest that I ditch this fountain pen and stone tablets too...

on July 23, 2007 03:27 PM
# TjL said:


commandline function:

gethead () {
lynx -dump -head $@
}

web interface:

http://tntluoma.com/sidebars/head/

on July 23, 2007 03:57 PM
# Antony Sargent said:

I think most modern browsers these days respect the Cache-Control header as well. (http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9)

I usually use something like:

Cache-Control: public, max-age=SOME_NUMBER_OF_SECONDS

on July 23, 2007 04:14 PM
# Ask Bjørn Hansen said:

You can do much better than that (basically you can make it work when you do replace the images too). :-)

I do it slightly differently, but Cal wrote a good article of the general concept here:

http://www.thinkvitamin.com/features/webapps/serving-javascript-fast


- ask

on July 23, 2007 11:42 PM
# Ask Bjørn Hansen said:

Oh, as an example of it in action:

If you look at the html source at http://www.yellowbot.com/ you will see for example

http://css.st.ypbot.net/prod/css/master.v2559.css

if you fetch that file it'll expire in a long time. If you fetch

http://css.st.ypbot.net/prod/css/master.css

then you don't get any explicit expiration headers and your browser is expected to Do The Right Thing.

In our code we have some helpers to put in the appropriate "version number" (from subversion revisions).

- ask

on July 24, 2007 12:13 AM
# Jeremy Zawodny said:

I hadn't seen that article by Cal. Thanks for the pointer.

on July 24, 2007 09:16 AM
# Ryan said:

Jeremy, I'd love to see more tips like this from you. I can use them on some of my websites.

Also, where in the file would one put this? Does it matter?

on July 24, 2007 11:46 AM
# Paul Stamatiou said:

Wow, great timing. I just installed YSlow for Firebug and was wondering how to do this. However, many of my static images are on S3 - how can I get S3 to do the same?

on July 25, 2007 11:22 AM
# Antony Sargent said:

@Paul

From:
http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTObjectPUT.html
and
http://developer.amazonwebservices.com/connect/thread.jspa?messageID=59615

It looks like if you use the REST API to upload your objects, you can specify Cache-Control and Expires headers on the PUT that get sent back verbatim for any GET's. This probably means Cache-Control would be the way to go for S3, since a static Expires time doesn't accomplish the "set the Expires time to access plus n seconds" as Jeremy's Apache config snippet does.

(Disclaimer: I have not tried this so I may be wrong)

on July 25, 2007 12:28 PM
# Antony Sargent said:
on July 25, 2007 12:44 PM
# paul said:

This tool could/should put more weight on stuff that gets served within the domain the page comes from. I got what I think is an artificially low score on stuff I don't control (I can't gzip stuff at Amazon or Google, frinstance).

Think the performance engineering group would concur?

on July 25, 2007 07:38 PM
# Clayton O'Neill said:

I used to run a site that had a very large number of small static images for icons and such, and a lot of them would be loaded on nearly every page. My work around for this was to set the Expires header on all of them to never, and then I would always generate URLs for them of the form http://site.com/static-images/image.png?. Any time I actually changed the images, I would increment the serial, which would cause the URL to change, and people would retrieve them again.

on July 27, 2007 09:54 AM
# Gabriel said:

Jeremy,

Thanks for pointing out this good tip. I used it to help speed up (considerably) the loading of icon graphics in RSS2.com. Now I'm going through the other tips from the YSlow application to see what else I can do to optimize client-side load times.

Thanks again.

on August 5, 2007 09:11 PM
# Charles said:

Thanks Jeremy, great material. Here's what I used for my high-traffic web site:

# Cache static content for 6 months
ExpiresActive On
ExpiresByType image/png A15552000
ExpiresByType text/javascript A15552000
ExpiresByType text/css A15552000

on November 20, 2007 05:49 PM
# Shallysehgal said:

How we can use etags in our HTML

on March 3, 2008 10:52 PM
# Hanan Ali said:

Great sharing man, it works excellent. I applied it to my site and it improved the speed upto 25%

on February 14, 2009 12:02 AM
# ZXT said:

Sorry for the noob question but where do you put the code in a self hosted Wordpress blog?

on November 18, 2009 12:31 PM
# Zach said:

@ZXT -- for self hosted wordpress with a hosting company see if you have access to the .htaccess file. It is a config file for apache in the root of the web directory.

on January 8, 2010 09:03 AM
# eric said:

Hi, anytime I try to add a line about expiration or compression in my htaccess file, I get an Internet Server Error and need to remove this line, do you know why? thank you,
Eric

on April 15, 2010 06:04 AM
# Jack Fisher said:

does the .htaccess file conflict with httpd.conf

on May 27, 2010 10:24 AM
# Used Stationary Bikes said:

Wow.. Thanks for sharing these tips!

on July 30, 2010 11:50 PM
# JeeShen Lee said:

Simple implementation of static header on web.config.

http://jeeshenlee.wordpress.com/2010/07/31/how-to-add-expires-headers-in-asp-net/

on August 8, 2010 05:37 PM
Disclaimer: The opinions expressed here are mine and mine alone. My current, past, or previous employers are not responsible for what I write here, the comments left by others, or the photos I may share. If you have questions, please contact me. Also, I am not a journalist or reporter. Don't "pitch" me.

 

Privacy: I do not share or publish the email addresses or IP addresses of anyone posting a comment here without consent. However, I do reserve the right to remove comments that are spammy, off-topic, or otherwise unsuitable based on my comment policy. In a few cases, I may leave spammy comments but remove any URLs they contain.