Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Please ask questions anytime. It’s going to be a bumpy ride.

Slide 3

Slide 3 text

Why are fast webpages important?

Slide 4

Slide 4 text

What's the impact of slow sites? Lower conversions and engagement, higher bounce rates... Ilya Grigorik @igrigorik Make The Web Faster, Google

Slide 5

Slide 5 text

Yo ho ho and a few billion pages of RUM How speed affects bounce rate @igrigorik

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

< 1.000 ms Page Loading Time on G3 ist der Mount Everest.

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Some WebPerf Problems can be fixed within Rails but not all. If your page loads 3 MB of JavaScript it will never be fast.

Slide 12

Slide 12 text

Webpage Rendering Basics

Slide 13

Slide 13 text

Network HTML DOM CSS CSSOM JavaScript Render tree Layout Paint

Slide 14

Slide 14 text

Network HTML DOM CSS CSSOM JavaScript Render tree Layout Paint

Slide 15

Slide 15 text

Network HTML DOM CSS CSSOM JavaScript Render tree Layout Paint

Slide 16

Slide 16 text

Network HTML DOM CSS CSSOM JavaScript Render tree Layout Paint

Slide 17

Slide 17 text

Network HTML DOM CSS CSSOM JavaScript Render tree Layout Paint

Slide 18

Slide 18 text

The rendering process takes a minimum of 100 ms which we have to subtract from the 1,000 ms.

Slide 19

Slide 19 text

Download a file with HTTP/TCP

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

TCP Slow-Start KB 0 55 110 165 220 Roundtrip 1. 2. 3. 4. 214KB 100KB 43KB 14KB 114KB 57KB 29KB 14KB

Slide 22

Slide 22 text

Waterfall www.webpagetest.org

Slide 23

Slide 23 text

https://www.webpagetest.org/result/ 180316_WK_94121529c3107aae272fa55c65f82216/

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

https://www.webpagetest.org/result/ 180316_V3_5a1acad4d9251fe1539e1394b309d80a/

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

https://www.webpagetest.org/result/ 180316_5B_de436fb724593d7b746b9c1f89ff3c2d/

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

Example Online Shop

Slide 35

Slide 35 text

$ 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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

app/views/products/index.html.erb: […] <% @products.each do |product| %> <%= product.category.name %> <%= product.name %> <%= product.description %> <%= number_to_currency(product.price) %> <% product.number_of_stars.to_i.times do %>

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

Completed 200 OK in 497ms (Views: 460.4ms | ActiveRecord: 34.1ms) Development env.

Slide 42

Slide 42 text

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.

Slide 43

Slide 43 text

Fragment Caching

Slide 44

Slide 44 text

$ rails dev:cache Development mode is now being cached.

Slide 45

Slide 45 text

1. Step Single Row

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

app/views/products/index.html.erb: <% @products.each do |product| %> <% cache product do %> <%= product.category.name %> <%= product.name %> <%= product.description %> <%= number_to_currency(product.price) %> <% product.number_of_stars.to_i.times do %> <% end %> [...] <% end %> <% end %>

Slide 49

Slide 49 text

app/views/products/index.html.erb: <% @products.each do |product| %> <% cache product do %> <%= product.category.name %> <%= product.name %> <%= product.description %> <%= number_to_currency(product.price) %> <% product.number_of_stars.to_i.times do %> <% end %> [...] <% end %> <% end %>

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Total Views Activerecord Vanilla 497ms 460 ms 34 ms Fragment Cache Row 79 ms 74,6 ms 1 ms

Slide 52

Slide 52 text

2. Step The Complete Table => Russian Doll

Slide 53

Slide 53 text

No content

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

app/views/products/index.html.erb: <% cache @products do %> <% @products.each do |product| %> <% cache product do %> <%= product.category.name %> <%= product.name %> <%= product.description %> <%= number_to_currency(product.price) %> <% product.number_of_stars.to_i.times do %> <% end %> […] <% end %> <% end %> <% end %>

Slide 56

Slide 56 text

app/views/products/index.html.erb: <% cache @products do %> <% @products.each do |product| %> <% cache product do %> <%= product.category.name %> <%= product.name %> <%= product.description %> <%= number_to_currency(product.price) %> <% product.number_of_stars.to_i.times do %> <% end %> […] <% end %> <% end %> <% end %>

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

Use the Database!

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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.

Slide 63

Slide 63 text

HTTP Caching

Slide 64

Slide 64 text

Web browsers and proxies don‘t want to fetch webpages twice. They use Last-Modified and Etag to avoid that.

Slide 65

Slide 65 text

The idea of Etags and Last-Modified

Slide 66

Slide 66 text

Web browser: „My user wants to fetch xyz.html. I cached a copy last week. Is that still good?“

Slide 67

Slide 67 text

Web server: „xyz.html hasn‘t changed since last week. Go a head with your copy!“ aka 304 Not Modified

Slide 68

Slide 68 text

> 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"

Slide 69

Slide 69 text

> 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"

Slide 70

Slide 70 text

Set the Etag class ProductsController < ApplicationController # GET /products def index @products = Product.all fresh_when :etag => @products end [...]


Slide 71

Slide 71 text

> 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.

Slide 72

Slide 72 text

> curl -I http://0.0.0.0:3000/products -b cookies.txt --header 'If-None-Match: "4d348810e69400799e2ab684c0ef4777"' HTTP/1.1 304 Not Modified Etag: "4d348810e69400799e2ab684c0ef4777" 304!

Slide 73

Slide 73 text

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.

Slide 74

Slide 74 text

Not good enough?

Slide 75

Slide 75 text

Writing the initial cache wastes a lot of time.

Slide 76

Slide 76 text

Let‘s preheat the cache in off business hours! Cron is your friend.

Slide 77

Slide 77 text

Use the night to preheat your cache. And don‘t be afraid of brute force!

Slide 78

Slide 78 text

A U T O B A H N

Slide 79

Slide 79 text

The fastest page is delivered by Nginx without ever contacting Ruby on Rails.

Slide 80

Slide 80 text

├── Gemfile ├── [...] ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── favicon.ico │ └── robots.txt ├── [...] That‘s already done for the files in the public directory.

Slide 81

Slide 81 text

Is there an easy way to save complete pages which are rendered by the Rails framework?

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

Brute Force is your friend! During the night the server has a hard time to stay awake any way.

Slide 84

Slide 84 text

Tricky part: How to delete out of date gz files?

Slide 85

Slide 85 text

after_update :expire_cache before_destroy :expire_cache private def expire_cache ActionController::Base.expire_page(Rails.application.routes.url_h elpers.company_path(self)) ActionController::Base.expire_page(Rails.application.routes.url_h elpers.companies_path) end app/models/product.rb

Slide 86

Slide 86 text

caches_page vs. !current_user.nil? ???

Slide 87

Slide 87 text

caches_page is good to cache customized user content too. It just takes more thinking.

Slide 88

Slide 88 text

Let us assume a user base of 10,000,000 people.

Slide 89

Slide 89 text

/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 ᐅ

Slide 90

Slide 90 text

/tmp ᐅ du -hs talks.gz 28K talks.gz 28K * 10,000,000 = 0,26 TB

Slide 91

Slide 91 text

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.

Slide 92

Slide 92 text

Nginx will happily read a cookie and find the pre- rendered page in a given directory structure.

Slide 93

Slide 93 text

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.

Slide 94

Slide 94 text

HTTP/1.2 vs. HTTP/2

Slide 95

Slide 95 text

HTTP/2 provides an average WebPerformance improvement of 20%. It’s a no-brainer. You have to use it!

Slide 96

Slide 96 text

CDNs, HTTP/2 and Rails 5.2

Slide 97

Slide 97 text

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.

Slide 98

Slide 98 text

Active Storage

Slide 99

Slide 99 text

Active Storage should be used to minimize image file size and to deliver different formats to different browsers.
 
 JPEG, PNG or WebP

Slide 100

Slide 100 text

Apache vs. Nginx

Slide 101

Slide 101 text

It doesn’t matter!
 Use the one you like most.

Slide 102

Slide 102 text

Brotli vs. gzip

Slide 103

Slide 103 text

Always offer both. Brotli can be used to save bandwidth and CPU-Resources.

Slide 104

Slide 104 text

Heroku vs. Bare Metal

Slide 105

Slide 105 text

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.

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

M A N U A L D N S - P R E F E T C H 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.“

Slide 109

Slide 109 text

P R E F E T C H 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

Slide 110

Slide 110 text

P R E R E N D E R

Slide 111

Slide 111 text

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 .

Slide 112

Slide 112 text

The Most Important Tool?

Slide 113

Slide 113 text

Set a Time Budget! If you run out of your time budget you have to cancel features on your website.

Slide 114

Slide 114 text

Is WebPerformance really so hard?

Slide 115

Slide 115 text

The WebPerf Bible. => https://hpbn.co

Slide 116

Slide 116 text

20% Discount-Code: RubyAndRails

Slide 117

Slide 117 text

@wintermeyer

Slide 118

Slide 118 text

[email protected] last name | twitter | github e-mail