Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

WebPerformance with Rails 5.2

WebPerformance with Rails 5.2

A talk I gave at https://wrocloverb.com 2018.

Stefan Wintermeyer

March 17, 2018
Tweet

More Decks by Stefan Wintermeyer

Other Decks in Programming

Transcript

  1. W E B P E R F W I T

    H R A I L S 5 . 2 S T E FA N W I N T E R M E Y E R
  2. What's the impact of slow sites? Lower conversions and engagement,

    higher bounce rates... Ilya Grigorik @igrigorik Make The Web Faster, Google
  3. Yo ho ho and a few billion pages of RUM

    How speed affects bounce rate @igrigorik
  4. Usability Engineering 101 Delay User reaction 0 - 100 ms

    Instant 100 - 300 ms Feels sluggish 300 - 1000 ms Machine is working... 1 s+ Mental context switch 10 s+ I'll come back later... Stay under 250 ms to feel "fast". Stay under 1000 ms to keep users attention. @igrigorik
  5. Web Search Delay Experiment Type of Delay Delay (ms) Duration

    (weeks) Impact on Avg. Daily Searches Pre-header 100 4 -0.20 % Pre-header 200 6 -0.59% Post-header 400 6 0.59% Post-ads 200 4 0.30% Source: https://www.igvita.com/slides/2012/webperf-crash-course.pdf
  6. For many, mobile is the one and only internet device!

    Country Mobile-only users Egypt 70% India 59% South Africa 57% Indonesia 44% United States 25% onDevice Research @igrigorik
  7. The (short) life of our 1000 ms budget 3G (200

    ms RTT) 4G(80 ms RTT) Control plane (200-2500 ms) (50-100 ms) DNS lookup 200 ms 80 ms TCP Connection 200 ms 80 ms TLS handshake (200-400 ms) (80-160 ms) HTTP request 200 ms 80 ms Leftover budget 0-400 ms 500-760 ms Network overhead of one HTTP request! @igrigorik
  8. Some WebPerf Problems can be fixed within Rails but not

    all. If your page loads 3 MB of JavaScript it will never be fast.
  9. The rendering process takes a minimum of 100 ms which

    we have to subtract from the 1,000 ms.
  10. Latency client Zeit 0 ms 80 ms 160 ms 240

    ms 320 ms 10 TCP Segmente (14.600 Bytes) 20 TCP Segmente (29.200 Bytes) 40 TCP Segmente (15.592 Bytes) server SYN ACK ACK GET SYN,ACK ACK ACK
  11. TCP Slow-Start KB 0 55 110 165 220 Roundtrip 1.

    2. 3. 4. 214KB 100KB 43KB 14KB 114KB 57KB 29KB 14KB
  12. $ rails new shop $ cd shop $ rails g

    scaffold Category name $ rails g scaffold Product category:references name description price:decimal{8,2} $ rails g scaffold User email first_name last_name password_digest $ rails g scaffold Review user:references product:references rating:integer $ rails db:migrate
  13. Shop domain model Category name string Product description text name

    string price decimal (8,2) Review rating integer User email string first_name string last_name string password_digest string
  14. app/models/product.rb: class Product < ApplicationRecord belongs_to :category has_many :reviews def

    number_of_stars if reviews.any? reviews.average(:rating).round else nil end end end
  15. db/seeds.rb: Category.create(name: "A") Category.create(name: "B") Category.create(name: "C") 100.times do Product.create(name:

    Faker::Food.dish, description: Faker::Food.description, category: Category.all.sample, price: rand(20)) end 50.times do user = User.create(first_name: Faker::Name.first_name, last_name: Faker::Name.last_name) products = Product.all 3.times do Review.create(user: user, product: products.sample, rating: rand(5)) end end
  16. app/views/products/index.html.erb: <table class="table table-striped"> […] <tbody> <% @products.each do |product|

    %> <tr> <td><%= product.category.name %></td> <td><%= product.name %></td> <td><%= product.description %></td> <td><%= number_to_currency(product.price) %></td> <td> <% product.number_of_stars.to_i.times do %> <img src="<%= asset_path( 'star.svg' ) %>“ /> <% end %> </td> […] </tr> <% end %> </tbody> </table>
  17. The (short) life of our 1000 ms budget 3G (200

    ms RTT) 4G(80 ms RTT) Control plane (200-2500 ms) (50-100 ms) DNS lookup 200 ms 80 ms TCP Connection 200 ms 80 ms TLS handshake (200-400 ms) (80-160 ms) HTTP request 200 ms 80 ms Leftover budget 0-400 ms 500-760 ms Network overhead of one HTTP request! @igrigorik 497ms And we don’t have product images yet.
  18. app/views/products/index.html.erb: <tbody> <% @products.each do |product| %> <% cache product

    do %> <tr> <td><%= product.category.name %></td> <td><%= product.name %></td> <td><%= product.description %></td> <td><%= number_to_currency(product.price) %></td> <td> <% product.number_of_stars.to_i.times do %> <img src="<%= asset_path( 'star.svg' ) %>" /> <% end %> </td> [...] </tr> <% end %> <% end %> </tbody>
  19. app/views/products/index.html.erb: <tbody> <% @products.each do |product| %> <% cache product

    do %> <tr> <td><%= product.category.name %></td> <td><%= product.name %></td> <td><%= product.description %></td> <td><%= number_to_currency(product.price) %></td> <td> <% product.number_of_stars.to_i.times do %> <img src="<%= asset_path( 'star.svg' ) %>" /> <% end %> </td> [...] </tr> <% end %> <% end %> </tbody>
  20. app/models/review.rb: class Review < ApplicationRecord belongs_to :user belongs_to :product, touch:

    true end Product description text name string price decimal (8,2) Review rating integer User email string first_name string last_name string password_digest string
  21. app/views/products/index.html.erb: <tbody> <% cache @products do %> <% @products.each do

    |product| %> <% cache product do %> <tr> <td><%= product.category.name %></td> <td><%= product.name %></td> <td><%= product.description %></td> <td><%= number_to_currency(product.price) %></td> <td> <% product.number_of_stars.to_i.times do %> <img src="<%= asset_path( 'star.svg' ) %>" /> <% end %> </td> […] </tr> <% end %> <% end %> <% end %> </tbody>
  22. app/views/products/index.html.erb: <tbody> <% cache @products do %> <% @products.each do

    |product| %> <% cache product do %> <tr> <td><%= product.category.name %></td> <td><%= product.name %></td> <td><%= product.description %></td> <td><%= number_to_currency(product.price) %></td> <td> <% product.number_of_stars.to_i.times do %> <img src="<%= asset_path( 'star.svg' ) %>" /> <% end %> </td> […] </tr> <% end %> <% end %> <% end %> </tbody>
  23. Total Views Activerecord Vanilla 497ms 460 ms 34 ms Fragment

    Cache Row 79 ms 74,6 ms 1 ms Fragment Cache Table 53 ms 49,5 ms 0,6 ms
  24. app/models/product.rb: class Product < ApplicationRecord belongs_to :category has_many :reviews def

    number_of_stars if reviews.any? reviews.average(:rating).round else nil end end end
  25. app/models/product.rb: class Product < ApplicationRecord belongs_to :category has_many :reviews end

    $ rails g migration AddNumberOfStarsToProduct number_of_stars:integer $ rails db:migrate
  26. app/models/product.rb: class Review < ApplicationRecord belongs_to :user belongs_to :product, touch:

    true after_create :recalculate_product_rating after_destroy :recalculate_product_rating private def recalculate_product_rating rating = product.reviews.average(:rating).round if rating != self.product.number_of_stars product.update_attribute(:number_of_stars, rating) end end end
  27. Warning: 
 Fragment Caching is slower the very first time.

    Why? Rails checks if the Fragment Cache exists. When it doesn’t it renders the view and writes the cache.
  28. Web browsers and proxies don‘t want to fetch webpages twice.

    They use Last-Modified and Etag to avoid that.
  29. Web browser: „My user wants to fetch xyz.html. I cached

    a copy last week. Is that still good?“
  30. Web server: „xyz.html hasn‘t changed since last week. Go a

    head with your copy!“ aka 304 Not Modified
  31. > curl -I http://0.0.0.0:3000/products HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8

    X-Ua-Compatible: IE=Edge Etag: "9a779b80e4b0ac3c60d29807e302deb7" [...] > curl -I http://0.0.0.0:3000/products HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 X-Ua-Compatible: IE=Edge Etag: "fa8fc1e981833a6885b583d351c4d823"
  32. > curl -I http://0.0.0.0:3000/products HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8

    X-Ua-Compatible: IE=Edge Etag: "9a779b80e4b0ac3c60d29807e302deb7" [...] > curl -I http://0.0.0.0:3000/products HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 X-Ua-Compatible: IE=Edge Etag: "fa8fc1e981833a6885b583d351c4d823"
  33. Set the Etag class ProductsController < ApplicationController # GET /products

    def index @products = Product.all fresh_when :etag => @products end [...]

  34. > curl -I http://0.0.0.0:3000/products -c cookies.txt HTTP/1.1 200 OK Etag:

    "4d348810e69400799e2ab684c0ef4777" > curl -I http://0.0.0.0:3000/products -b cookies.txt HTTP/1.1 200 OK Etag: "4d348810e69400799e2ab684c0ef4777" The cookie is needed for the CSRF-Token.
  35. Win-Win of a 304 • The Browser doesn’t have to

    download everything. • The Server doesn’t have to render the view which is the most time consuming bit.
  36. ├── Gemfile ├── [...] ├── public │ ├── 404.html │

    ├── 422.html │ ├── 500.html │ ├── favicon.ico │ └── robots.txt ├── [...] That‘s already done for the files in the public directory.
  37. Is there an easy way to save complete pages which

    are rendered by the Rails framework?
  38. Add caches_page to your controller to save views as static

    gz files in your public directory: caches_page :index, :show, :gzip => :true Add gem actionpack-page_caching for Rails 5.2
  39. Brute Force is your friend! During the night the server

    has a hard time to stay awake any way.
  40. /tmp ᐅ wget http://www.railsconf.com/2013/talks --2013-04-27 21:04:24-- http://www.railsconf.com/2013/talks Resolving www.railsconf.com... 107.20.162.205

    Connecting to www.railsconf.com|107.20.162.205|:80... connected. HTTP request sent, awaiting response... 200 OK Length: unspecified [text/html] Saving to: ‘talks’ [ <=> ] 74,321 258KB/ s in 0.3s 2013-04-27 21:04:25 (258 KB/s) - ‘talks’ saved [74321] /tmp ᐅ du -hs talks 76K talks /tmp ᐅ gzip talks /tmp ᐅ du -hs talks.gz 28K talks.gz /tmp ᐅ
  41. 28K * 10,000,000 = 0,26 TB Harddrive space is cheap.

    By saving the files non-gz and using a data deduplication file system you just need 5-10% of the 0,26 TB. Nginx can gzip the files on the fly.
  42. Nginx will happily read a cookie and find the pre-

    rendered page in a given directory structure.
  43. To setup a complex page_cache system is a lot of

    work. You have to tackle not only Rails but Nginx too. It does increase the snappiness of your application but might not be worth the effort for small systems.
  44. Long story short:
 In many cases where a CDN made

    sense with HTTP/1.1 it doesn’t make sense any more. Try to deliver everything from your Rails server.
  45. Active Storage should be used to minimize image file size

    and to deliver different formats to different browsers.
 
 JPEG, PNG or WebP
  46. Heroku is good for a quick start but has never

    been a good choice for good WebPerformance. Bare Metal is the way to go if you need maximum WebPerformance.
 
 BTW: It’s cheaper too.
  47. P R E L O A D I N G

    U N D P R E F E T C H I N G
  48. P R E L O A D I N G

    U N D P R E F E T C H I N G <link rel="dns-prefetch"... <link rel="prefetch"... <link rel="prerender"... DNS pre-resolution TCP pre-connect prefresh preloader
  49. M A N U A L D N S -

    P R E F E T C H <link rel="dns-prefetch" href="//abc.com"> http://www.chromium.org/developers/design-documents/dns-prefetching „Most common names like google.com and yahoo.com are resolved so often that most local ISP's name resolvers can answer in closer to 80-120ms. If the domain name in question is an uncommon name, then a query may have to go through numerous resolvers up and down the hierarchy, and the delay can average closer to 200-300ms.“
  50. P R E F E T C H <link rel="prefetch"

    href=„http://abc.com/important.js"> http://www.whatwg.org/specs/web-apps/current-work/#link-type-prefetch „The prefetch keyword indicates that preemptively fetching and caching the specified resource is likely to be beneficial, as it is highly likely that the user will require this resource.“ T I P P : " A C C E P T - R A N G E S : B Y T E S “ H E A D E R
  51. P R E R E N D E R <link

    rel="prerender" href=„http://abc.com/faq.html“>
  52. Y O U C A N T E L L

    N G I N X T O P U S H T H O S E F I L E S V I A H T T P / 2 .
  53. Set a Time Budget! If you run out of your

    time budget you have to cancel features on your website.