on client-side Cache, Sinatra and Conditional GET’s

July 1st, 2011

After re-reading Google Code - Page Speed - Optimize Caching section, I began implementing it on the Sinatra app part of our eClinic.Pro system. Later, after having it working, I found out that Sinatra has this amazing helper:

# File 'lib/sinatra/base.rb', line 290

def last_modified(time)
  return unless time
  time = time_for time
  response[’Last-Modified’] = time.httpdate
  # compare based on seconds since epoch
  halt 304 if Time.httpdate(env[’HTTP_IF_MODIFIED_SINCE’]).to_i >= time.to_i
rescue ArgumentError

Commited the code with a farewell git commit -am “Awesome handmade conditional GET to leverage client caching”, pushed it and refactored it to use the instance method last_modified(timestamp), just to commit it again.

Combine this with a before filter, like:

before do
  # set to rfc maximum because db_version will handle expiry date
  cache_control :public, :max_age => 31536000
  # get the database version time stamp
  db_version = Admin.first.created_at
  # now if db_version is more recent then last_modified will send a :halt 304 Not Modified as response.
  last_modified db_version
  # ... and if not, proceed here to the request route...

In short, the client (browser) issues a request for some content via a URI to the server. The server receives the request and checks the Request Headers to see if a If-Modified-Since directive is defined. If not, a response['Last-Modified'] header is set to the current time and the execution flow will proceed to the Sinatra app request route to deliver a response [no client local caching scenario]. Yet, this timestamp will be used by the client (browser) cache manager to store it locally, together with the response payload, building a client-side local cache for this resource. Now, if there is another client request, to the same URI, this time a If-Modified-Since Request Header will be sent along the request. The server will catch it and compare to an expiry date, in this case, the db_version. Since it is lower than, the server will stop the execution flow and send a 304 Not Modified status response. The “Conditional GET” technique name is because the GET request had an “interruption” based on conditions defined by HTTP Headers. Now, the client receiving this response will get the local storage cached content and deliver it as response. For the Sinatra app, the router was not even touched.

Acording to Google, using both Cache-Control: max-age and Last-Modified headers, or even Etag if necessary, instructs the browser to load previously downloaded resources from local disk rather than over the network. This is client (browser) caching.

Based on the documentation it is redundant to specify both Expires and Cache-Control: max-age, or to specify both Last-Modified and ETag. It makes note to use the Cache control: public directive to enable HTTPS caching for Firefox.

This allows the browser to efficiently update its cached resources by issuing conditional GET requests when the user explicitly reloads the page. Conditional GETs don’t return the full response unless the resource has changed at the server, and thus have lower latency than full GETs.

For this, I set the server to dispatch a 304 Not Modified response to the browser so that is gets the local cached content and presents it as the result of the request, with a:

Request Method: GET
Status Code: 304 Not Modified

This is quite important since I’m requesting readonly JSON content that takes about 3 seconds
to be fetched, and the result of this is 13 miliseconds response time. Local content. The payload is considerable, about 136KB of JSON response for one of the areas. Adding up that for one of the pages I’m loading 3 payloads of these via jQuery, the main server Merb app is now feeling quite lighter due to this recent decoupling. This means lower CPU utilization, database access, bandwith and time to respond.

Combining this with application server caching, response times get quite improved.

Before client (browser) local caching:
before Conditional GET

After client (browser) local caching:
after Conditional GET

Final Notes:
- Github SinatraRb documentation for (Object) last_modified(time);
- Google Code Optimize Caching page;
- HTTP Headers on Wikipedia;
- Google Chrome is an amazing browser to develop on, using the Developer Tools;
- still got to detect a “typo” on Google Code documentation.