Skip to content

Instantly share code, notes, and snippets.

@szimek
Created January 24, 2012 09:51
Show Gist options
  • Star 26 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save szimek/1669330 to your computer and use it in GitHub Desktop.
Save szimek/1669330 to your computer and use it in GitHub Desktop.
HTTP caching in Rails

HTTP caching

Kinds of caches

  • Browser
  • Proxy
  • Gateway

TODO Difference between proxy and gateway caches.

HTML meta tags and HTTP headers

  • HTML meta tags (Pragma: no-cache) - they’re only honored by a few browser caches, not proxy caches (which almost never read the HTML in the document).
  • HTTP headers - much better

Pragma headers

TODO Describe details and why they don't work.

Expiration headers: Expires (HTTP 1.0)/Cache-Control:max-age (HTTP 1.1)

Expiration

Cache-Control

  • max-age (most important here)
  • no-store
  • public/private
  • others

TODO Move other options to some later topic about disabling caching/SSL/etc. ?

From RFC:

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.

Or, when both Cache-Control and Expires are present, Cache-Control takes precedence.

Validation headers: Last-Modified (HTTP 1.0)/ETag (HTTP 1.1)

By using it, caches avoid having to download the entire representation when they already have a copy locally, but they’re not sure if it’s still fresh.

Last-Modified -> If-Modified-Since request

ETag -> If-None-Match request

Almost all caches use Last-Modified times as validators; ETag validation is also becoming prevalent.

Most modern Web servers will generate both ETag and Last-Modified headers to use as validators for static content (i.e., files) automatically; you won’t have to do anything.

From RFC:

The preferred behavior for an HTTP/1.1 origin server is to send both a strong entity tag and a Last-Modified value.

TODO What if expiration and validation headers are set?

HTTP caching in Rack/Rails apps

Rack::Cache, Rack::ETag, Rack::ConditionalGet etc.

fresh_when, stale?

tips for generating (current_user, @collection.max(:updated_at))

Example: Asset caching

CDN

Issues with asset versioning e.g. mylib-1.0.js etc.

Rails asset pipeline

Provides automatic versioning

Issues

  • caching SSL Cache-Control: public

Tips

  • cookies

Examples

Basecamp

Fragment caching - examples on using cache-keys + manual versioning for HTML/translation changes

Links

Etag

# lib/rack/etag.rb
def call(env)
  status, headers, body = @app.call(env)
  parts = []
  body.each { |part| parts << part.to_s }
  headers['ETag'] = %("#{Digest::MD5.hexdigest(parts.join(""))}")
  [status, headers, parts]
end
# actionpack/lib/action_dispatch/http/cache.rb
module ActionDispatch::Http::Cache::Response
  def etag=(etag)
    key = ActiveSupport::Cache.expand_cache_key(etag)
    @etag = self[ETAG] = %("#{Digest::MD5.hexdigest(key)}")
  end
end

DONE:

  1. ETag is associated with URL - same ETag for different URLs won't cause the same data to be loaded by browser for every URL
  2. Setting ETag manually using #fresh_when and stale? vs setting it automatically via Rack::ETag middleware - when setting it manually, Rails doesn't have to render any view

TODO:

  1. Describe usage of #fresh_when and #stale?
@questions = Question.scoped
fresh_when(:etag => @questions) # fetches all questions and calls #cache_key on every one of them
@questions = Question.scoped
fresh_when(:etag => [current_user, @questions].flatten.compact)
@questions = Question.scoped
fresh_when(:last_modified => @questions.maximum(:updated_at)) # doesn't have to fetch all questions
@questions = Question.scoped
fresh_when(:last_modified => [current_user.try(:updated_at), @questions.maximum(:updated_at)].compact.max)
@questions = Question.all
render @questions, :layout => false if stale?(:etag => @questions)
  1. Control-cache: max-age vs ETag/Last-Modified-At - clicking a link to a cached page (browser reads from cache if present) vs hitting reload button in browser (hitting server, optionally 304 response)

  2. Action caching + HTTP caching - action code is not executed, so #fresh_when and #stale? won't work, but Rack::ETag is used automatically, so Rails reads the body from the cache, Rack::ETag generates ETag and empty response it returned with 304 if ETag matches.

  3. Optimizing assets

  • assets pipeline
    • adds timestamp to filenames, compiles files (image-url in CSS etc.), minifies JS files, helpers etc.
  • CDN
    • CNAME to your domain e.g. cdn.domain.com
      • makes it possible to fetch assets faster (workaround for max connections limit)
      • still every user who visits your app for the first time hits Rails app to fetch assets
    • use e.g. cache proxy (e.g. Cloudfront) CNAME to your domain
      • makes it possible to fetch assets faster (workaround for max connections limit)
      • first request caches assets, next requests from any user don't touch Rails at all, but fetch assets from proxy server
  • max-age header
    • caches assets in the browser, so that it doesn't have to make any request
  config.serve_static_assets = false
  config.static_cache_control = "public, max-age=31536000" # 1 year
  1. Cache-control - "private" vs "public"

  2. fragment caching, default #cache_key

  3. Rack::Cache, Rack::Etag, Rack::ConditionalGet. How it all works in Rails. https://gist.github.com/9395

  4. Issues

  • HTML, translations changes and client side cache - need to modify manual ETags after deploy - e.g. use deploy SHA or timestamp (use env var on Heroku - maybe soon it will be set automatically by Heroku)
  • CSRF token on pages with forms
    • is not a big issue for signed out users
    • may be an issue if user signs in, the page is cached, signs out, signs in again, receives new CSRF token, visits the same page and gets cached version with old CSRF token and tries to post/put/delete a form. One can either update user object on every sign in and thus modify generated ETag, or use form_authenticity_token when generating ETag.
def handle_unverified_request
  reset_session # doesn't raise ActionController::InvalidAuthenticityToken anymore
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment