when to tell your kids about client caching
DESCRIPTION
RailsConf 2009 Presentation by Matthew DeitersTRANSCRIPT
When to tell your kids about presentation
caching
Matthew Deiterswww.theAgileDeveloper.com
A practical guide to stuffing your app’s bits
into someone else’s browser
Questions: @mdeiters
Rapid Feature Development
Rapid Feature Development
Adoption & Growth
Client Caching
client.is_a?(Browser) == true
Browsers &Leveraging HTTP 1.1
Fewer Requests
Smaller Responses
80/20 Rule(Pareto Principle)
80% of the wealth owned by 20% of people
80% of your time is with 20% of your acquaintances
80% of the time you wear 20% of your clothing
80% of a request is spent on the wire
Today
Last-Modified Header
ETag Header
max-age Header
Expires Header
Reducing Network Traffic
GZip
Minification
Cookies
Today
ME
To illustrate: Scalability
Applicable for?
Applicable for?
Enterprises
Applicable for?
Enterprises
High Traffic Web Sites
Applicable for?
Enterprises
High Traffic Web Sites
Startups
Enterprise
High Traffic Sites
Reduce network trafficReduce response timesReduce load
Facebook: Bumpersticker
1.4 Million Average Users
1.4 Million Average Users
Average 20 page views
“Push everything you possibly can to the client to reduce the amount of traffic going over the network...”
Startups
HTTP 1.1 Enity Tags
\puppies\43
HTTP/1.x 200 OKEtag: "8b2242293d5e5b02e99b3be73fc0c9fa"
If-None-Match: "8b2242293d5e5b02e99b3be73fc0c9fa"
\puppies\43
HTTP/1.x 304 Not Modified
Last-Modified: Tue, 12 Dec 2006 03:03:59 GMTETag: "10c24bc-4ab-457e1c1f"
Last-Modified: Tue, 12 Dec 2006 03:03:59 GMTETag: "10c24bc-4ab-457e1c1f"
If-Modified-Since: Tue, 12 Dec 2006 03:03:59 GMTIf-None-Match: "10c24bc-4ab-457e1c1f"
+ Conditional Get
class PeopleController < ApplicationController def show @person = Person.find(params[:id])
respond_to do |wants| #... end end end
#response.rb
def last_modified=(utc_time)def etag=(etag)
#request.rb
def fresh?(response)def not_modified?(modified_at)def etag_matches?(etag)
class PeopleController < ApplicationController def show @person = Person.find(params[:id])
respond_to do |wants| #... end end end
class PeopleController < ApplicationController def show @person = Person.find(params[:id])
respond_to do |wants| #... end end end
response.last_modified = @person.updated_at.utc
class PeopleController < ApplicationController def show @person = Person.find(params[:id])
respond_to do |wants| #... end end end
response.last_modified = @person.updated_at.utc response.etag = @person
class PeopleController < ApplicationController def show @person = Person.find(params[:id])
respond_to do |wants| #... end end end
return head(:not_modified) if request.fresh?(response)
response.last_modified = @person.updated_at.utc response.etag = @person
response.etag = @person # => “5cb44721b6ce18857ff6900486dc4aba”
@person.cache_key # => "people/5-20071224150000"
def fresh_when(options)def stale?(options)
class PeopleController < ApplicationController def show @person = Person.find(params[:id])
response.last_modified = @person.updated_at.utc response.etag = @person return head(:not_modified) if request.fresh?(response) respond_to do |wants| #... end end end
class PeopleController < ApplicationController def show @person = Person.find(params[:id])
if stale?(:etag => @person, :last_modified => @person.updated_at.utc) respond_to do |wants| #... end end end end
Last-Modified vs ETag
response.etag = [@admin, @person, flash]
Later
def handle_conditional_get! if nonempty_ok_response? self.etag ||= body if request && request.etag_matches?(etag) self.status = '304 Not Modified' self.body = '' end end
set_conditional_cache_control! if etag? || last_modified?end
self.etag ||= body
SInce Feb 2007
http://localhost:3000/peopleGET /people HTTP/1.1
http://localhost:3000/peopleGET /people HTTP/1.1
HTTP/1.x 200 OK...Etag: "94785662c6f60cb96681ed1b09a44783"
http://localhost:3000/peopleGET /people HTTP/1.1
HTTP/1.x 200 OK...Etag: "94785662c6f60cb96681ed1b09a44783"
http://localhost:3000/peopleGET /people HTTP/1.1If-None-Match: "94785662c6f60cb96681ed1b09a44783"
http://localhost:3000/peopleGET /people HTTP/1.1
HTTP/1.x 200 OK...Etag: "94785662c6f60cb96681ed1b09a44783"
http://localhost:3000/peopleGET /people HTTP/1.1If-None-Match: "94785662c6f60cb96681ed1b09a44783"
HTTP/1.x 304 Not ModifiedEtag: "94785662c6f60cb96681ed1b09a44783"
send_file
Assets
Assets
Assets
INODE
/intl/en_ALL/images/logo.gif
ETag: "48b6a5bf-47f4-a0757"
/intl/en_ALL/images/logo.gif
ETag: "48b6a5bf-47f4-a0757"
/intl/en_ALL/images/logo.gif
ETag: "48b6a5bf-61a-21c86a4"
/intl/en_ALL/images/logo.gif
ETag: "48b6a5bf-47f4-a0757"
/intl/en_ALL/images/logo.gif
ETag: "48b6a5bf-61a-21c86a4"
Avoid Cache Expiration & Validation
/stylesheets/screen.css?1219926880
CACHE BUSTER!
<FilesMatch "\.(pdf|flv|jpg|jpeg|png|gif|js|css|swf)$"> Header set Cache-Control "public" ExpiresActive On ExpiresDefault “access plus 10 years” FileETag None Header unset Last-Modified Header unset ETag</FilesMatch>
Now
/images/beach.png?1241477547 /images/beach.png?1241477554
Server 1 Server 2
Option 1
#config/environments/production.rb
#SubversionENV['RAILS_ASSET_ID'] = YAML::load(`svn info $RAILS_ROOT`)["Revision"].to_i
#GIT (Check out Grit too)ENV['RAILS_ASSET_ID'] = File.read(RAILS_ROOT + '/.git/refs/heads/deploy').chomp
Option 2
task :finalize_update, :except => { :no_release => true } do stamp = Time.now.utc.strftime("%Y%m%d%H%M.%S") asset_paths = %w(images stylesheets javascripts).map do |asset| "#{latest_release}/public/#{p}" end.join(" ") run "find #{asset_paths} -exec touch -t #{stamp} {} ';'; true", :env => { "TZ" => "UTC" }end
Option 2
#Capistrano 2.4set :normalize_asset_timestamps, true
Option 3
use-commit-times
Option 3
Now
ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = true
Proxies & Via Header
/stylesheets/screen.css?1219926880
/stylesheets/screen.1219926880.css
Bag of Tricks
#asset_tag_helper.rbdef rewrite_asset_path(source) #... source + "?#{asset_id}" end
#Rules for Versioned Static FilesRewriteRule ^(scripts|css|images)/(.+)\.(.+)\.(js|css|jpg|gif|png)$ $1/$2.$4 [L]
AJAX
DUMB-ASSES
:method => :post
:method => :post
Superfluous values in URL
Superfluous values in URL
HTTP Headers
HTTP Headers
headers['Last-Modified'] = Time.now.httpdateheaders['Expires'] = '-1'headers['Pragma'] = 'no-cache'headers['Cache-Control'] = 'no-cache, must-revalidate, max-age=0, pre-check=0, post-check=0'
#http://github.com/dancroak/no_cacheno_cache :first_name_autocomplete, :index
Now
Cache Ajax?
Speed up rendering
default 2 connections per host for HTTP 1.1
connections
ActionController::Base.asset_host = "http://mt%d.google.com"
http://mt0.google.com
http://mt1.google.com
http://mt2.google.com
http://mt3.google.com
http://mt0.google.com http://mt1.google.com http://mt2.google.com http://mt3.google.com
CNAME Now
greater than 40% drop in page load time
host_names >= 2 && host_names <= 4
# http://github.com/dhh/asset-hosting-with-minimum-ssl
config.action_controller.asset_host = AssetHostingWithMinimumSsl.new( # will serve non-SSL assetts on http://assets[1-4].example.com "http://assets%d.example.com", # will serve SSL assets on https://assets1.example.com "https://assets1.example.com" )
Now
Less requests are better
ActionController::Base.perform_caching = true
Combine Assets
<%= javascript_include_tag 'application', 'user' %>
<script src="/javascripts/application.js?1219633350" type="text/javascript"></script><script src="/javascripts/user.js?1219633368" type="text/javascript"></script>
<%= javascript_include_tag 'application', 'user', :cache => :true %>
<script src="/javascripts/all.js?1219633651" type="text/javascript"></script>
<%= javascript_include_tag 'application', 'user', :cache=>‘login’ %>
<script src="/javascripts/login.js?1219633651" type="text/javascript"></script>
application.js user.js
+login.js
Compiles on Server
Dedicated Asset Server / CDN
Not all servers may have login.js
Timestamp Conflict
<script src="/javascripts/all.js?1219633651" type="text/javascript"></script> <script src="/javascripts/all.js?1219634734" type="text/javascript"></script>
asset_packager || cramjam || CSSJSC
Minify
JSMIN || PackR
CSMINSmurfrucksack
minified_cache
YUI Compressor
http://compressorrater.thruhere.net/
Now
bundle_fu
<% bundle do %>
<%= javascript_include_tag "prototype" %> <%= stylesheet_link_tag "basic.css" %> <%= calendar_date_select_includes "red" %> <script src="javascripts/application.js" type="text/javascript"></script>
<% end %>
<script src="/javascripts/cache/bundle.js?1220211999" type="text/javascript"></script>
Generates files locally during development
www.example.org dynamic contentstatic.example.org components (cookies don’t go here)
www.example.org dynamic contentstatic.example.org components (cookies don’t go here)
Now
CDN & Asset Servers
Let Google be your CDN
Google Ajax Libraries
jQuery
prototype
script.aculo.us
MooTools
dojo
“Once we host a release of a given library, we are committed to hosting that release indefinitely”
Last-Modified: Fri, 30 May 2008 06:03:19 GMTExpires: Sun, 17 Jan 2038 19:14:07 GMTCache-Control: publicDate: Sat, 30 Aug 2008 20:10:26 GMT
Cache-Control: public
mod_deflate vs mod_gzip
mod_deflate vs mod_gzipBaked w/ ApacheEasier on CPU~35% Compresion
mod_deflate vs mod_gzipBaked w/ ApacheEasier on CPU~35% Compresion
~29% Compresion
config.middleware.use Rack::Deflater
GZip + Minification + Minimum Components
Fewer Requests
Smaller Responses
There is more potential for improvement by focusing on the front-end.
Cutting it in half reduces response times by 40% or more, whereas cutting back-end performance in half results in less than a 10% reduction.
Front-end improvements typically require less time and resources than back-end projects (redesigning application architecture and code, finding
and optimizing critical code paths, adding or modifying hardware, distributing databases, etc.).
yslowLiveHTTPHeaders
Fiddlerhttp://compressorrater.thruhere.net/
Matthew Deiterswww.theAgileDeveloper.com