22 · side path · src

An empty frame with a src fills itself.

<%= turbo_frame_tag "sidebar", src: related_path do %>
  <div class="spinner">loading…</div>
<% end %>

Reload the page with the ⟳ button and watch the wire: the page arrives, then a second request goes out for /related, and its turbo-frame#sidebar lands in the box. Whatever you put inside the tag shows until then.

The messages page painted without waiting for the sidebar. The slow part arrives a moment later, and it can be cached on its own. Turbo sets aria-busy="true" on the frame while it fetches and complete once it has content, so a one-line CSS rule gives you a loading state.

→ press ⟳ on the browser
22 · loading="lazy"

Below the fold, don't even ask.

<%= turbo_frame_tag "archive", src: archive_path, loading: "lazy" do %>
  older messages load when you scroll here…
<% end %>

There's now an archive frame far down the messages page. It works like a lazy image: no request until it scrolls into view. Scroll the page down and watch the wire wake up.

Same trick for anything hidden at first: a tab that isn't selected, a <details> that starts closed, a modal. Those frames cost nothing until someone looks.

→ scroll the page down to the archive
22 · why this matters for caching

Split a page by how often each part changes.

A page that mixes "same for everyone" with "this user only" is hard to cache: one key per combination. Put the per-user part in a frame and the rest of the page can be served from a single cached copy. Or the reverse: a heavily personal page with one shared widget, framed.

Each eager frame is its own request, and content popping in late looks jittery. Lazy frames are free; eager ones aren't quite.