Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Cache = Cash! 2.0

Cache = Cash! 2.0

RailsConf 2025 talk

Avatar for Stefan Wintermeyer

Stefan Wintermeyer

July 08, 2025
Tweet

More Decks by Stefan Wintermeyer

Other Decks in Programming

Transcript

  1. Kleinvieh macht auch Mist. As we Germans put it: "Even

    small animals make a mess." 
 Small steps can lead to something big.
  2. Cache vs. CPU Ruby: result = 1 + 1 Ruby:

    initials = "#{'Stefan'[0]} #{'Wintermeyer'[0]}"
  3. Program 1: Add 1 + 1 ──────────────────── mov al, 1

    ; 1 cycle add al, 1 ; 1 cycle mov [result], al ; 3 cycles ; = 5 cycles total
  4. Program 2: Make "S.W." Initials ──────────────────────────────── mov al, [Stefan] ;

    3 cycles mov [init], al ; 3 cycles mov al, [Wintermeyer]; 3 cycles mov [init+2], al ; 3 cycles ; = 12 cycles total
  5. Caching: One size doesn’t fi t all! The very same

    code can perform totally di ff erent on an other hardware!!!
  6. Cart state string LineItem price decimal (8,2) quantity integer Category

    name string Product description text name string price decimal (8,2) DiscountGroup discount integer name string User email string ∗ encrypted_password string ∗ first_name string last_name string Rating value integer
  7. $ bin/rails benchmark:shop Users: 300 (240 browsers, 60 buyers) Result:

    Total Time: 30289 ms ├─ Views: 20873 ms ├─ DB: 9416 ms Baseline
  8. $ bin/rails benchmark:shop Users: 300 (240 browsers, 60 buyers) Result:

    Total Time: 30289 ms ├─ Views: 20873 ms ├─ DB: 9416 ms Baseline
  9. stefan@SW-MacBook-Air-M4 shop3 % git diff --unified=0 | grep -E '^\+\+\+|^@@|^\+[^+]|^-[^-]'

    +++ b/app/controllers/application_controller.rb @@ -26 +26 @@ class ApplicationController < ActionController::Base - @current_cart = Cart.active.find_by(id: session[:cart_id]) + @current_cart = Cart.active.select(:id, :user_id, :state, :session_id).find_by(id: session[:cart_id]) @@ -34 +34 @@ class ApplicationController < ActionController::Base - cart = Cart.active.find_by(user: current_user) + cart = Cart.active.select(:id, :user_id, :state, :session_id).find_by(user: current_user) @@ -44 +44 @@ class ApplicationController < ActionController::Base - @session_cart = Cart.active.find_by(id: session[:cart_id]) + @session_cart = Cart.active.select(:id, :user_id, :state, :session_id).find_by(id: session[:cart_id]) @@ -66 +66 @@ class ApplicationController < ActionController::Base - session_cart = Cart.find_by(id: session[:cart_id]) + session_cart = Cart.select(:id, :user_id, :state).find_by(id: session[:cart_id]) +++ b/app/controllers/carts_controller.rb @@ -4 +4 @@ class CartsController < ApplicationController - @line_items = @cart ? @cart.line_items.includes(:product) : [] + @line_items = @cart ? @cart.line_items.preload(product: proc { select(:id, :name, :tagline, :price) }) : [] +++ b/app/controllers/home_controller.rb @@ -6 +6 @@ class HomeController < ApplicationController - .select("products.*, AVG(ratings.value) as avg_rating, COUNT(ratings.id) as ratings_count") + .select("products.id, products.name, products.tagline, products.price, products.weight, products.expiration_date, products.category_id, AVG(ratings.value) as avg_rating COUNT(ratings.id) as ratings_count") @@ -11 +11 @@ class HomeController < ApplicationController - .includes(:category) + .preload(category: proc { select(:id, :name) }) +++ b/app/controllers/line_items_controller.rb @@ -5 +5 @@ class LineItemsController < ApplicationController - product = Product.find(params[:product_id]) + product = Product.select(:id, :price).find(params[:product_id]) +++ b/app/controllers/products_controller.rb @@ -3 +3,4 @@ class ProductsController < ApplicationController - @product = Product.includes(:category, :ratings).find(params[:id]) + @product = Product + .select(:id, :name, :tagline, :price, :weight, :dimensions, :expiration_date, :details, :category_id, :created_at, :updated_at) + .preload(category: proc { select(:id, :name) }) + .find(params[:id]) +++ b/app/models/cart.rb @@ -22 +22 @@ class Cart < ApplicationRecord - current_item = line_items.find_by(product: product) + current_item = line_items.select(:id, :product_id, :quantity, :price).find_by(product: product) @@ -41 +41 @@ class Cart < ApplicationRecord - line_items.find_by(product: product)&.quantity || 0 + line_items.select(:quantity).find_by(product: product)&.quantity || 0 +++ b/app/views/shared/_product_cart_controls.html.erb @@ -3 +3 @@ - <% line_item = current_cart.line_items.find_by(product: product) %> + <% line_item = current_cart.line_items.select(:id, :quantity).find_by(product: product) %>
  10. select + optimized datatypes = - 3% 0 10.000 20.000

    30.000 40.000 Baseline select + datatype 0 29.380 30.289
  11. Indexes = - 6% 0 10.000 20.000 30.000 40.000 Baseline

    select + datatype Index 0 27.617 29.380 30.289
  12. class AddNameIndexesToUsers < ActiveRecord::Migration[8.0] def change add_index :users, :last_name, name:

    "ln" add_index :users, [:last_name, :first_name], name: "ln_and_fn" end end Index Pro Tipro-Tip
  13. Fragment Caching = - 60% 0 10.000 20.000 30.000 40.000

    Baseline select + datatype Index Fragment Caching 0 11.046 27.617 29.380 30.289
  14. HTTP caching still uses the controller but doesn’t always render

    the HTML Less computing on the server and less/faster payload to the web client.
  15. > curl -I http://0.0.0.0:3000/ 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/ HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 X-Ua-Compatible: IE=Edge Etag: "fa8fc1e981833a6885b583d351c4d823"
  16. > curl -I http://0.0.0.0:3000/ 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/ HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 X-Ua-Compatible: IE=Edge Etag: "fa8fc1e981833a6885b583d351c4d823"
  17. > curl -I http://0.0.0.0:3000/ -c cookies.txt HTTP/1.1 200 OK Etag:

    "4d348810e69400799e2ab684c0ef4777" > curl -I http://0.0.0.0:3000/ -b cookies.txt HTTP/1.1 200 OK Etag: "4d348810e69400799e2ab684c0ef4777" The cookie is needed for the CSRF-Token.
  18. Conditional Request - Using If-Modified-Since # Use the Last-Modified value

    from the first request curl -I http://localhost:3000/ \ -H "If-Modified-Since: Mon, 07 Jul 2025 10:30:45 GMT" # If content hasn't changed, you'll get: HTTP/1.1 304 Not Modified Last-Modified: Mon, 07 Jul 2025 10:30:45 GMT
  19. HTTP Caching = - 23% 0 10.000 20.000 30.000 40.000

    Baseline select + datatype Index Fragment Caching HTTP Caching 0 8.505 11.046 27.617 29.380 30.289
  20. ᵓᴷᴷ Gem fi le ᵓᴷᴷ [...] ᵓᴷᴷ public │ ᵓᴷᴷ

    404.html │ ᵓᴷᴷ 422.html │ ᵓᴷᴷ 500.html │ ᵓᴷᴷ favicon.ico │ └── robots.txt ᵓᴷᴷ [...] That‘s already done for the fi les in the public directory.
  21. stefan@Mac shop3 % tree public public ├── 400.html ├── 404.html

    ├── 406-unsupported-browser.html ├── 422.html ├── 500.html ├── icon.png ├── icon.svg ├── products │ ├── 501.html │ ├── 501.html.br │ ├── 502.html │ ├── 502.html.br │ ├── 503.html Disk space is cheap!
  22. Total = - 89% 0 10.000 20.000 30.000 40.000 Baseline

    select + datatype Index Fragment Caching HTTP Caching Autobahn 3.572 8.505 11.046 27.617 29.380 30.289
  23. 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
  24. Yo ho ho and a few billion pages of RUM

    How speed affects bounce rate @igrigorik