erb ruby javascript 94 lines · 3 tabs

Speed up perceived performance with Turbo preload links

Shared by codesnips Jan 2026
3 tabs
<nav class="site-nav" data-controller="preload">
  <ul>
    <li>
      <%= link_to "Dashboard", dashboard_path,
            class: "nav-link",
            data: { turbo_preload: true } %>
    </li>

    <% @featured_articles.each do |article| %>
      <li>
        <%= link_to article.title, article_path(article),
              class: "nav-link",
              data: {
                turbo_preload: true,
                preload_target: "link"
              } %>
      </li>
    <% end %>

    <li>
      <%# not preloaded: low intent + has query params %>
      <%= link_to "Search", search_path, class: "nav-link" %>
    </li>
  </ul>
</nav>
3 files · erb, ruby, javascript Explain with highlit

This snippet shows how Turbo Drive's link preloading is wired up in a Rails app to make navigation feel instant. The core idea is that Turbo can fetch a page in the background before a click happens — either on hover (data-turbo-preload combined with the built-in preloading behavior) or eagerly when the link scrolls into view — so that by the time a user actually clicks, the HTML is already sitting in Turbo's cache and rendering is near-instantaneous. It trades a little extra network traffic for a large improvement in perceived performance, which is often what matters for a snappy UI.

In _nav.html.erb, the important links carry data: { turbo_preload: true }. Turbo's LinkPrefetchObserver picks these up and issues a background fetch for the target href, storing the response so the subsequent visit is served from cache rather than a fresh round-trip. Marking only high-intent links (dashboard, top articles) avoids preloading everything, which would waste bandwidth and hammer the server.

Because preloading only helps when the response is cacheable, ArticlesController sets an explicit Cache-Control header via expires_in and relies on fresh_when with an ETag so repeat fetches can be revalidated cheaply. The show action is deliberately idempotent and side-effect free; anything with side effects (like recording a view) must not run during a speculative preload, which is why that work is pushed to a RecordViewJob triggered separately.

The preload_controller.js Stimulus controller handles the eager case that Turbo does not do out of the box: it uses an IntersectionObserver to detect when a flagged link enters the viewport and asks Turbo to preload it via Turbo.session.preloadOnHover-style prefetching. Disconnecting the observer in disconnect() prevents leaks across Turbo navigations.

The trade-offs are real: speculative fetches can trigger analytics or write paths if a controller is careless, cached HTML can go stale, and mobile users may not want the extra data. Guarding side effects, setting sane cache lifetimes, and preloading selectively keep those risks in check while delivering the instant-feel navigation Turbo makes possible.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Speed up perceived performance with Turbo preload links — share card
Link copied