Sometimes your data doesn't change that frequently so instead of rendering/serving that data when a client makes a request to a webserver, just tell it to use what it already has. I've been hacking away at writing live search/autocomplete from scratch. My first implementation would hit the server every time a key was inputted into the search field. For example, if someone entered "Banana", the server would get a post for "B", "Ba", "Ban", up to "Banana" and return an array of matching fruit each time. I'm sure you can imagine if a site has a solid user base, making six round trips to the server in addition to six queries to the database is not optimal. Well if my list of fruit isn't that big, why not just give the client the list and have it all happen client side? This is where page caching comes in handy.
This guide is great: http://www.mnot.net/cache_docs/, but if you aren't up to read that let me give a quick summary.
So what is a web cache? Caches sit somewhere between the server and client and saves copies of the responses to the client. When the client asks for the same data, instead of hitting the server, the cache will use the copy and send that to the client to use instead of having the server generate it again. Caches are handy for:
Reducing latency - Since the cache is closer to the client than the server, the site seems more responsive.
Reducing network traffic - Since data is being reused, bandwidth can be saved by both the server and client. Especially helpful for larger datasets.
So there are multiple kinds of caches but lets talk about Browser caches. Most modern browsers allow developers to store data on the client's hardrive. The browser cache will grab a page from the server and save it. If another request is made, it will check to see if the page it already has is fresh before it tries to ask the server for the page again.
The way a browser checks with the server is through HTTP headers. Whenever a browser makes a HTTP request to a server, the server sends back an HTTP Header which is not rendered by the browser. The header contains some cool information we can use to keep track of how 'fresh' our data is. Rails ships with some magical caching so we can start taking a look at the HTTP header right away. To do this open up a terminal and type and examine the output:
>> curl -I http://localhost:3000
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-UA-Compatible: IE=Edge
ETag: "53d2b88b7648b6a7020cffb937dd4354"
Cache-Control: max-age=0, private, must-revalidate
First lets look at the ETag value. An ETag is a unique identifier that is generated by the server based on the representation of the data it is serving. If you run the curl command again, you will notice that ETag value is different.
>> curl -I http://localhost:3000
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-UA-Compatible: IE=Edge
ETag: "f8098de645bb2635d2d7804b51b9d899"
Cache-Control: max-age=0, private, must-revalidate
This is because the server is generating a new page for each request. Well, if my list of fruits hasn't changed then I want the browser to use the data it already has to render the page. Somehow we have to generate the ETag based on the state of my list of fruit. Rails has a very handy method called fresh_when that can be used in controllers. Check this line of code out in my FruitsController:
def index
@fruits = Fruit.all
fresh_when :etag => Fruit.maximum(:updated_at)
end
What this will do is generate an ETag for the index action in my FruitsController based on the last time a fruit was updated/created. So if I haven't changed or added any fruit the data should be the same and the etag shouldn't change. Let's curl again!
>> curl -I http://localhost:3000
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-UA-Compatible: IE=Edge
ETag: "809568a0768e4fd6cd61fb15583299d5"
Cache-Control: max-age=0, private, must-revalidate
>> curl -I http://localhost:3000
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-UA-Compatible: IE=Edge
ETag: "809568a0768e4fd6cd61fb15583299d5"
Cache-Control: max-age=0, private, must-revalidate
Something to note is that when your browser makes the request, it adds something to the HTTP header, a validator called 'If-None-Match'. This is actually really interesting to check out so let's curl one more time but this time add the 'If-None-Match' header with the ETag.
curl -I http://localhost:3000 --header 'If-None-Match: "809568a0768e4fd6cd61fb15583299d5"'
HTTP/1.1 304 Not Modified
ETag: "809568a0768e4fd6cd61fb15583299d5"
Cache-Control: max-age=0, private, must-revalidate
Can you spot the subtle difference in the header this time?
If you noticed that the header says "HTTP/1.1 304 Not Modfied" instead of "HTTP/1.1 200 OK" give yourself a pat on the back. If you also take a look at your rails server log it might look something like this:
#before
Started GET "/" for 127.0.0.1 at 2012-11-17 22:44:08 -0800
Processing by FruitsController#index as */*
Item Load (7.2ms) SELECT "fruits".* FROM "fruits"
Rendered fruits/index.html.erb within layouts/application (10.7ms)
Completed 200 OK in 86ms (Views: 19.5ms | ActiveRecord: 7.2ms)
#after
Started GET "/" for 127.0.0.1 at 2012-11-17 23:22:27 -0800
Processing by FruitsController#index as */*
Item Load (7.9ms) SELECT "fruits".* FROM "fruits"
(0.5ms) SELECT MAX("fruits"."updated_at") AS max_id FROM "fruits"
Completed 304 Not Modified in 68ms (ActiveRecord: 8.4ms)
Notice how before using the fresh_when method, when a client made a request, the database was queried to grab all the fruits. In the second time, the etag was generated by only querying the db for the last updated fruit. The server doesn't grab all the fruits again and just responds with 304 Not Modified (It actually doesn't execute any of the code in the index action except for the fresh_when method). Your database will be thanking you for giving it a break. Wow that was a long post and I haven't even touched, page/fragment caching, using memcache as a replacement for your Rails cache or using the Rails cache to CACHE db queries. I'm cached out.
IN THE NEXT EPISODE: MEMCACHE
IN THE NEXT NEXT EPISODE: more on HTTP headers, Cache-Control, page/fragment caching in Rails