Boring Rails

Tip: Lazy-loading content with Turbo Frames and skeleton loader

:fire: Tiny Tips

Hotwire is a new suite of frontend tools from Basecamp for building “reactive Rails” apps while writing a minimal amount of JavaScript.

While the most exciting feature to some is the real-time streaming of server rendered HTML, my favorite addition is the Turbo Frame.

The Turbo Frame is a super-charged iFrame that doesn’t make you cringe when you use it. Frames represent a slice of your page and have their own navigation context.

One incredibly powerful feature is lazy-loading a Frame. One example of this pattern that you probably see everyday is the GitHub activity feed:

GitHub Activity Feed: Lazy load

First you load the “outer shell” of the page and then you can make an AJAX call to fetch more content to fill in the rest of the page. It’s a great way to speed up a slow page.

But one downside is that the page content jumps around a bit. The “Loading” spinner is one small rectangle, but the result is a long feed of events.

A way to solve this problem is to use a “skeleton screen” or “skeleton loader”. This UI pattern uses a blank version of the content as a placeholder and reduces the jarring impact when the content finally loads.

Skeleton loader

These two concepts go together like peanut butter and jelly.


A basic lazy-loaded Turbo Frame looks like this:

<turbo-frame id="feed" src="/feed">
  Content will be replaced when /feed has been loaded.

By specifying the src attribute, the Frame will automatically make an AJAX request when the page loads and replace its content with the matching <turbo-frame> in the response.

Additionally, you can set the loading property of the loading to be either “eager” (load right away) or “lazy” (load once the frame is visible on the page).

Here’s how the GitHub Activity feed might look in a Rails view:

<!-- app/views/home.html.erb -->

<div>Some other content...</div>

<%= turbo_frame_tag :feed, src: activity_feed_path, loading: :lazy do %>
<% end %>

You can take it to the next level by replacing the basic “Loading…” message with your own skeleton loader. Tailwind makes this really easy with the built-in animate-pulse class.

Simply add some gray rectangles as your initial frame contents:

<!-- app/views/home.html.erb -->

<div>Some other content...</div>

<%= turbo_frame_tag :feed, src: activity_feed_path, loading: :lazy do %>
  <div class="flex flex-col space-y-6">
    <% 10.times do %>
      <div class="animate-pulse flex space-x-4">
        <!-- Avatar -->
        <div class="rounded-full bg-gray-400 h-12 w-12"></div>

        <!-- Details -->
        <div class="flex-1 space-y-4 py-1">
          <div class="h-4 bg-gray-400 rounded w-3/4"></div>
          <div class="space-y-2">
            <div class="h-4 bg-gray-400 rounded"></div>
            <div class="h-4 bg-gray-400 rounded w-5/6"></div>
    <% end %>
<% end %>

One last thing: make sure your activity_feed_path action returns the content wrapped in a matching Turbo Frame so that it will automatically swap out the frame contents and replace the loading state.

class ActivityFeedController < ApplicationControler
  def show
    @events = Current.user.activity.last(20)

Note: we don’t want the src or loading attributes set on the Frame in this response, otherwise you would create an infinite loop!

<!-- app/views/activity_feed/show.html.erb -->

<%= turbo_frame_tag :feed do %>
  <%= render partial: "feed_item", collection: @events %>
<% end %>

Once you wrap your head around the power of Turbo Frames, soon you’ll be spotting all kinds of places in your app that can benefit from lazy-loading some good old HTML.

Additional Resources

Hotwire Docs: Turbo Frames

Tailwind Docs: animate-pulse

If you like these tips, you'll love my Twitter account. All killer, no filler.