Skip to content

Blogging On Rails

Everything on Rails!

Russian Doll Caching – Building HOTWire HNPWA #4

What if we can increase page load the speed by not duplicating work in the server rendering process? Since this app is set up to broadcast changes to the front end clients, it’s possible to reuse that work when a request comes in for the page. Russian Doll Caching describes layers of caching. Start from the smallest element, and then wrap it in bigger and bigger layers of caching, punching through to invalidate the cache with model updates.

Russian Doll Caching in HOTWire Hacker News Progressive Web App

Implementing Russian Doll Caching inside the Hacker News Progressive Web App requires exploiting nested relationships in the data model. The Top Items list is cached, it’s made up of cached TopItem records, which is made up of a cached Item. Updating one TopItem invalidates the list, but since all the other TopItems will be cached, it’s still fewer database calls. Updating one Item invalidates the associated TopItem, which invalidates the list. There are still fewer database calls over the aggregate of app usage, speeding up the overall page rendering.

If you’re also using TurboStreams, then the rendering to the cache is happening before any pages render, meaning there are already cached items the first time you need to load a page.

View Caching

Each Item is cached by adding the cache directive to the top of partial app/views/items/_item.html.erb and a matching end at the bottom:

<%= cache item do %>
  <div class="border rounded-sm flex flex-col h-full w-full" id="<%= dom_id item %>">
    <%= turbo_stream_from item %>
    <header class="border flex justify-between p-1">
      <span class="rounded-full py-1 px-3 bg-red-600 text-white"><%= item.score %></span>
      <p class="py-1 px-3">
        <% "job" if item.job? %>
        <%= item.host unless item.host.nil? %>
      </p>
    </header>
    <article class="border flex flex-col flex-grow">
      <p class="py-1 px-3 flex-grow text-2xl">
        <% if item.url.nil? %>
          <%= link_to item.title, item_path(item.hn_id) %>
        <% else %>
          <%= link_to item.title, item.url, { target: '_blank', rel: 'noopener' } %>
        <% end %>
      </p>
      <p class="py-1 px-3">
        <em><%= item.time.strftime('%c') %></em>
      </p>
    </article>
    <% unless item.job? %>
      <footer class="border flex justify-between">
        <p class="py-1 px-3">
          <%= link_to pluralize(item.descendants, 'comment'), item_path(item.hn_id) %>
        </p>
        <p class="py-1 px-3">
          <a href="/user/<%= item.by %>"><%= item.by %></a>
        </p>
      </footer>
    <% end %>
  </div>
<% end %>

Then, the TopItem partial, app/views/top_items/_top_item.html.erb is cached in a similar way:

<%= cache top_item do %>
  <div class="h-full flex-grow" id="<%= dom_id top_item %>">
    <%= turbo_stream_from top_item %>
    <%= render top_item.item %>
  </div>
<% end %>

In app/views/tops/show.html.erb, add the caching based on the most recent TopItem and the current page:

<div class=" w-3/4 mx-auto p-4">
  <header class="bg-red-600 py-1 px-3 text-white flex">
    <p class=" text-2xl flex-grow">Top Stories</p>
    <%= link_to "Refresh", top_path, method: :create, class: "pt-1" %>
  </header>
  <%= render partial: 'layouts/page_navigation', locals: { page: @page, total_pages: @total_pages, path: top_path } %>
  <%= cache ['top-list', @top_item, @page] do %>
    <div class="grid grid-cols-3 gap-4">
      <%= render @stories %>
    </div>
  <% end %>
</div>

@top_item comes from app/controllers/tops_controller.rb, where the most recent TopItem is loaded from the database.

  def show
    @page = params[:page] ? params[:page].to_i : FIRST_PAGE
    @total_pages = TopItem.count / ITEMS_PER_PAGE
    @stories = TopItem.order(:location)
                      .limit(ITEMS_PER_PAGE)
                      .offset(@page * ITEMS_PER_PAGE)
                      .includes(:item)
    @top_item = TopItem.order(:updated_at).last
  end

Breaking through the cache

Since the Item can change without the TopItem changing, it’s important that the cache is busted so the page is shown correctly. Item will now have a callback to update the associated TopItem.

class Item < ApplicationRecord
  has_one :top_item
  after_save :update_associates
  broadcasts
  enum hn_type: [:story, :job]

  def populate(json) 
    if json.nil?
      return
    end
    self.hn_id = json['id'] if json['id']
    self.hn_type = json['type'] if json['type']
    self.by = json['by'] if json['by']
    self.time = DateTime.strptime("#{json['time']}",'%s') if json['time']
    self.text = json['text'] if json['text']
    self.parent = json['parent'] if json['parent']
    if json['url']
      self.url = json['url'] 
      host = URI.parse( json['url'] ).host
      self.host = host.gsub("www.", "") unless host.nil?
    end 
    self.score = json['score'] if json['score']
    self.descendants = json['descendants'] if json['descendants']
    self.title = json['title'] if json['title']
  end

  def update_associates
    top_item.touch if top_item.present?
  end
end

The Result

If you get rid of includes(:item) optimization in top_items#show and clear the cache, this is a log of the rendering. It’s 152 milliseconds, but when the page is refreshed, it’s only 16 milliseconds. That’s about a 10x difference. The rendering that happens in the background because of TurboStreams is also going to cut down on processing, since each Item and TopItem is cached.

Processing by TopsController#show as TURBO_STREAM
  Parameters: {"page"=>"2"}
   (1.0ms)  SELECT COUNT(*) FROM "top_items"
  ↳ app/controllers/tops_controller.rb:8:in `show'
  TopItem Load (1.1ms)  SELECT "top_items".* FROM "top_items" ORDER BY "top_items"."updated_at" DESC LIMIT $1  [["LIMIT", 1]]
  ↳ app/controllers/tops_controller.rb:12:in `show'
  Rendering layout layouts/application.html.erb
  Rendering tops/show.html.erb within layouts/application
  Rendered layouts/_page_navigation.html.erb (Duration: 0.1ms | Allocations: 74)
Read fragment views/tops/show:22c98a01c47411a5d6288e4df0b392c2/top-list/top_items/502-20201229191446314957/2 (0.2ms)
  TopItem Load (1.3ms)  SELECT "top_items".* FROM "top_items" ORDER BY "top_items"."location" ASC LIMIT $1 OFFSET $2  [["LIMIT", 30], ["OFFSET", 60]]
  ↳ app/views/tops/show.html.erb:9
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/63-20201229191122794264 (0.2ms)
  Item Load (1.7ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 980], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/980-20201229191122780225 (0.2ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/980-20201229191122780225 (0.1ms)
  Rendered items/_item.html.erb (Duration: 1.2ms | Allocations: 353) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/63-20201229191122794264 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/64-20201229191123154327 (0.1ms)
  Item Load (1.5ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 721], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/721-20201229191123141063 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/721-20201229191123141063 (0.2ms)
  Rendered items/_item.html.erb (Duration: 1.2ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/64-20201229191123154327 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/65-20201229191123588280 (0.1ms)
  Item Load (1.1ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 725], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/725-20201229191123577284 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/725-20201229191123577284 (0.1ms)
  Rendered items/_item.html.erb (Duration: 0.7ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/65-20201229191123588280 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/66-20201229191123984823 (0.1ms)
  Item Load (1.0ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 724], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/724-20201229191123973217 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/724-20201229191123973217 (0.1ms)
  Rendered items/_item.html.erb (Duration: 0.9ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/66-20201229191123984823 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/67-20201229191124338618 (0.1ms)
  Item Load (1.0ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 723], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/723-20201229182850194829 (0.2ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/723-20201229182850194829 (0.1ms)
  Rendered items/_item.html.erb (Duration: 1.3ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/67-20201229191124338618 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/68-20201229191124704711 (0.1ms)
  Item Load (1.5ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 726], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/726-20201229191124694651 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/726-20201229191124694651 (0.1ms)
  Rendered items/_item.html.erb (Duration: 0.9ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/68-20201229191124704711 (0.2ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/69-20201229191125153308 (0.1ms)
  Item Load (1.1ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 981], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/981-20201229191125138731 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/981-20201229191125138731 (0.1ms)
  Rendered items/_item.html.erb (Duration: 0.9ms | Allocations: 352) [cache miss]
  ↳ app/views/top_items/_top_item.html.erb:4ab0ca71e68ba60f0c5a8c6792672d/top_items/69-20201229191125153308 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/70-20201229191125516443 (0.1ms)
  Item Load (0.9ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 982], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/982-20201229191125499167 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/982-20201229191125499167 (0.1ms)
  Rendered items/_item.html.erb (Duration: 0.7ms | Allocations: 352) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/70-20201229191125516443 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/71-20201229191125902277 (0.1ms)
  Item Load (1.2ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 969], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/969-20201229191125891173 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/969-20201229191125891173 (0.1ms)
  Rendered items/_item.html.erb (Duration: 0.8ms | Allocations: 352) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/71-20201229191125902277 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/72-20201229191126343039 (0.1ms)
  Item Load (1.0ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 731], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/731-20201229191126331992 (0.2ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/731-20201229191126331992 (0.1ms)
  Rendered items/_item.html.erb (Duration: 1.1ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/72-20201229191126343039 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/73-20201229191126779284 (0.1ms)
  Item Load (1.2ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 729], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/729-20201229181339631341 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/729-20201229181339631341 (0.1ms)
  Rendered items/_item.html.erb (Duration: 1.1ms | Allocations: 350) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/73-20201229191126779284 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/74-20201229191127230847 (0.1ms)
  Item Load (1.0ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 505], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/505-20201229181341812678 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/505-20201229181341812678 (0.1ms)
  Rendered items/_item.html.erb (Duration: 0.8ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/74-20201229191127230847 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/75-20201229191127585826 (0.1ms)
  Item Load (1.0ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 745], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/745-20201229191127570616 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/745-20201229191127570616 (0.1ms)
  Rendered items/_item.html.erb (Duration: 1.0ms | Allocations: 352) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/75-20201229191127585826 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/76-20201229191127984311 (0.1ms)
  Item Load (0.9ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 970], ["LIMIT", 1]]

Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/970-20201229191127972006 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/970-20201229191127972006 (0.1ms)
  Rendered items/_item.html.erb (Duration: 1.1ms | Allocations: 350) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/76-20201229191127984311 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/77-20201229191128461470 (0.1ms)
  Item Load (1.0ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 733], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/733-20201229181341203527 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/733-20201229181341203527 (0.1ms)
  Rendered items/_item.html.erb (Duration: 0.9ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/77-20201229191128461470 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/78-20201229191128861481 (0.1ms)
  Item Load (1.1ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 732], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/732-20201229181340874908 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/732-20201229181340874908 (0.1ms)
  Rendered items/_item.html.erb (Duration: 1.0ms | Allocations: 350) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/78-20201229191128861481 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/79-20201229191129304039 (0.1ms)
  Item Load (1.2ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 737], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/737-20201229191129290517 (0.2ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/737-20201229191129290517 (0.1ms)
  Rendered items/_item.html.erb (Duration: 1.7ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/79-20201229191129304039 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/80-20201229191129746965 (0.1ms)
  Item Load (1.2ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 736], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/736-20201229183133401103 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/736-20201229183133401103 (0.1ms)
  Rendered items/_item.html.erb (Duration: 0.8ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/80-20201229191129746965 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/81-20201229191130181131 (0.1ms)
  Item Load (1.0ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 738], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/738-20201229182856474404 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/738-20201229182856474404 (0.1ms)
  Rendered items/_item.html.erb (Duration: 1.0ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/81-20201229191130181131 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/82-20201229191130541701 (0.1ms)
  Item Load (1.1ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 739], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/739-20201229191130530560 (0.2ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/739-20201229191130530560 (0.1ms)
  Rendered items/_item.html.erb (Duration: 1.1ms | Allocations: 352) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/82-20201229191130541701 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/83-20201229191130939531 (0.1ms)
  Item Load (1.1ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 740], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/740-20201229181344557695 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/740-20201229181344557695 (0.1ms)
  Rendered items/_item.html.erb (Duration: 0.9ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/83-20201229191130939531 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/84-20201229191131339796 (0.1ms)
  Item Load (1.0ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 741], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/741-20201229181346433167 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/741-20201229181346433167 (0.1ms)
  Rendered items/_item.html.erb (Duration: 0.7ms | Allocations: 351) [cache miss]
Unsubscribing from channel: {"channel":"Turbo::StreamsChannel","signed_stream_name":"IloybGtPaTh2YUdodWNIZGhMMVJ2Y0VsMFpXMHZNek0i--1ed0dfda6af953cac3909192c617768195ceb2f09482619cd656c1b54ac46193"}
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/85-20201229191131702623 (0.1ms)
  Item Load (0.9ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 730], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/730-20201229191131690621 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/730-20201229191131690621 (0.2ms)
  Rendered items/_item.html.erb (Duration: 0.9ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/85-20201229191131702623 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/86-20201229191132062586 (0.1ms)
  Item Load (1.2ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 734], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/734-20201229191132050650 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/734-20201229191132050650 (0.1ms)
  Rendered items/_item.html.erb (Duration: 1.0ms | Allocations: 352) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/86-20201229191132062586 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/87-20201229191132424954 (0.1ms)
  Item Load (1.0ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 742], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/742-20201229191132412114 (0.2ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/742-20201229191132412114 (0.1ms)
  Rendered items/_item.html.erb (Duration: 1.1ms | Allocations: 352) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/87-20201229191132424954 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/88-20201229191132790004 (0.1ms)
  Item Load (1.2ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 728], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/728-20201229181339168369 (0.2ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/728-20201229181339168369 (0.1ms)
  Rendered items/_item.html.erb (Duration: 1.0ms | Allocations: 352) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/88-20201229191132790004 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/89-20201229191133147506 (0.1ms)
  Item Load (1.1ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 743], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/743-20201229191133135191 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/743-20201229191133135191 (0.1ms)
  Rendered items/_item.html.erb (Duration: 0.9ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/89-20201229191133147506 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/90-20201229191133542295 (0.1ms)
  Item Load (1.0ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 727], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/727-20201229182853324197 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/727-20201229182853324197 (0.1ms)
  Rendered items/_item.html.erb (Duration: 1.0ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/90-20201229191133542295 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/91-20201229191133984661 (0.1ms)
  Item Load (1.0ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 715], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/715-20201229191133972911 (0.1ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/715-20201229191133972911 (0.1ms)
  Rendered items/_item.html.erb (Duration: 0.9ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/91-20201229191133984661 (0.1ms)
Read fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/92-20201229191134341696 (0.1ms)
  Item Load (1.2ms)  SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2  [["id", 735], ["LIMIT", 1]]
  ↳ app/views/top_items/_top_item.html.erb:4
Read fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/735-20201229191134330370 (0.2ms)
Write fragment views/items/_item:85e5b8da7bdc64d700153bd8f212848d/items/735-20201229191134330370 (0.1ms)
  Rendered items/_item.html.erb (Duration: 1.4ms | Allocations: 351) [cache miss]
Write fragment views/top_items/_top_item:668ab0ca71e68ba60f0c5a8c6792672d/top_items/92-20201229191134341696 (0.1ms)
  Rendered collection of top_items/_top_item.html.erb [30 times] (Duration: 131.5ms | Allocations: 46011)
Write fragment views/tops/show:22c98a01c47411a5d6288e4df0b392c2/top-list/top_items/502-20201229191446314957/2 (0.1ms)
  Rendered tops/show.html.erb within layouts/application (Duration: 136.8ms | Allocations: 47454)
[Webpacker] Everything's up-to-date. Nothing to do
  Rendered layout layouts/application.html.erb (Duration: 144.8ms | Allocations: 50988)
Completed 200 OK in 152ms (Views: 110.6ms | ActiveRecord: 36.8ms | Allocations: 52640)

Here is the cached result:

Started GET "/top?page=2" for 127.0.0.1 at 2020-12-29 14:19:47 -0500
Processing by TopsController#show as TURBO_STREAM
  Parameters: {"page"=>"2"}
  [1m[35m (1.2ms)[0m  [1m[34mSELECT COUNT(*) FROM "top_items"[0m
  ↳ app/controllers/tops_controller.rb:8:in `show'
  [1m[36mTopItem Load (1.1ms)[0m  [1m[34mSELECT "top_items".* FROM "top_items" ORDER BY "top_items"."updated_at" DESC LIMIT $1[0m  [["LIMIT", 1]]
  ↳ app/controllers/tops_controller.rb:12:in `show'
  Rendering layout layouts/application.html.erb
  Rendering tops/show.html.erb within layouts/application
  Rendered layouts/_page_navigation.html.erb (Duration: 0.2ms | Allocations: 74)
Read fragment views/tops/show:22c98a01c47411a5d6288e4df0b392c2/top-list/top_items/502-20201229191446314957/2 (0.2ms)
  Rendered tops/show.html.erb within layouts/application (Duration: 0.9ms | Allocations: 299)
[Webpacker] Everything's up-to-date. Nothing to do
  Rendered layout layouts/application.html.erb (Duration: 8.0ms | Allocations: 3833)
Completed 200 OK in 16ms (Views: 8.5ms | ActiveRecord: 2.3ms | Allocations: 5483)

Subscribe for updates as this project takes shape. You can see all the code at Github: https://github.com/OnRailsBlog/hhnpwa

Other HOTWire Tutorials

Looking to make your Rails app more interactive?

Subscribe below, and you won’t miss the next Rails tutorial, that will teach you how to make really interactive web apps that just work.

We won’t send you spam. Unsubscribe at any time.

2 comments on “Russian Doll Caching – Building HOTWire HNPWA #4”

Leave a Reply

Your email address will not be published. Required fields are marked *