「ページの表示が重い。でも遅いのは一部の集計やフィードだけで、そこだけ後から出せれば十分なのに」——そんなふうに感じたことはありませんか?
ダッシュボードのアクティビティ一覧、外部APIを叩く箇所、重い集計。こうした「一部だけ遅い」ページを速く見せるのに、JavaScriptをガリガリ書く必要はありません。
RailsにはHotwire標準のTurbo Framesがあり、loading: :lazy を付けるだけで「重い部分だけを別リクエストで後から読み込む」設計が実現できます。TurboはRails 7以降は標準添付(turbo-rails)なので、Rails 7でも8でも追加のgemなしで使えます。この記事では、最小構成のコードとつまずきポイントまで、まとめて解説します。
この記事で作るもの(完成イメージ)
今回作るのは、次のような動きのダッシュボードです。
- ページ本体(見出しやナビ)はすぐに表示される
- 重いアクティビティ一覧の場所には、最初はスケルトン(灰色のプレースホルダ)が出る
- その部分だけ別のエンドポイントから後追いで読み込まれ、中身が差し込まれる

ポイントは、重い部分を「別のアクション」に切り出しておくことです。メインの表示をその重さで止めないので、体感速度が大きく変わります。しかも書くのはほぼERBだけ。順番に見ていきましょう。
遅延読み込みの基本形
まず、メインのビュー側です。重い部分を turbo_frame_tag で囲み、src(読み込み先のパス)と loading: :lazy を指定します。
<%# app/views/dashboards/show.html.erb %>
<h1>ダッシュボード</h1>
<%= turbo_frame_tag "activity_feed", src: activity_feed_path, loading: :lazy do %>
<p>読み込み中...</p>
<% end %>これだけで、ページを開くと <h1> はすぐ表示され、activity_feed フレームは activity_feed_path を裏で取りにいきます。do ... end の中身(ここでは「読み込み中…」)は、読み込みが終わるまでのあいだ表示されるプレースホルダです。
ここで大事なのは、loading: :lazy にすると読み込みがフレームが画面に見えたタイミングまで遅延される点です。ファーストビューの外にあるフレームは、スクロールして見える位置に来るまでリクエストが飛びません。ページ下部の重い一覧に付けると、初回表示のコストをさらに減らせます。
ルーティングとコントローラを用意する
次に、src の飛び先を用意します。ここが「重い処理を切り出す」本体です。まずはルーティング。
# config/routes.rb
resource :dashboard, only: :show
resource :activity_feed, only: :show続いてコントローラです。重い集計やクエリは、このアクションの中だけで完結させます。メインのダッシュボード側からはこの処理を切り離せるので、本体の表示はこの重さに引きずられません。
# app/controllers/activity_feeds_controller.rb
class ActivityFeedsController < ApplicationController
def show
# 重い集計・関連の読み込みはここに閉じ込める
@activities = Activity.recent.includes(:user).limit(20)
end
endそして読み込み先のビュー。ここが最大のポイントで、返すHTMLの中にメイン側と同じIDの turbo_frame_tag(= <turbo-frame>)を必ず含めます。Turboはこの同じIDのフレームを探し出し、その中身だけを差し替えます。
<%# app/views/activity_feeds/show.html.erb %>
<%= turbo_frame_tag "activity_feed" do %>
<ul>
<% @activities.each do |activity| %>
<li><%= activity.summary %></li>
<% end %>
</ul>
<% end %>これで動きます。メインの activity_feed フレームが activity_feed_path を取得し、返ってきたHTMLの中の同IDフレームの中身(<ul>...</ul>)で置き換わる、という流れです。
スケルトンで「空白の一瞬」を消す
「読み込み中…」のテキストのままでも動きますが、実務ではスケルトン(骨組みだけのプレースホルダ)を置くと、待ち時間の印象がぐっと良くなります。読み込みが終わると、この骨組みが実データに置き換わります。
プレースホルダは turbo_frame_tag の do ... end に書くだけです。
<%= turbo_frame_tag "activity_feed", src: activity_feed_path, loading: :lazy do %>
<div class="skeleton">
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
</div>
<% end %>スケルトンの見た目はCSSだけで作れます。JavaScriptは不要です。
.skeleton-line {
height: 1rem;
margin-bottom: 0.75rem;
border-radius: 4px;
background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
background-size: 200% 100%;
animation: skeleton 1.2s ease-in-out infinite;
}
@keyframes skeleton {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}「空白 → いきなり表示」よりも「骨組み → 実データ」のほうが、体感の待ち時間は短く感じられます。画像を多く含む一覧(たとえばActiveStorageのアバター機能を並べる画面)でも、この置き換えは有効です。
eager と lazy の違い
loading には eager(デフォルト)と lazy の2つがあります。どちらも「別リクエストで読み込む」点は同じですが、いつ読み込むかが違います。
- eager(既定): フレームがDOMに現れた瞬間、すぐに
srcを読みにいく。ファーストビュー内の重要な部分向き - lazy: フレームが画面に見える位置に入るまで読み込みを遅らせる。ページ下部やタブの裏など、すぐ見えない部分向き
使い分けの目安はシンプルです。すぐ見える重要な塊は eager、スクロールしないと見えない塊は lazy。lazy にすると、ユーザーがそこまで見なければリクエスト自体が発生しないので、サーバー負荷も下げられます。
よくあるつまずきと対処
① 中身が差し込まれない(IDが一致していない)
いちばん多いのがこれです。読み込み先のビューに、メインと同じIDの turbo_frame_tag が無いと、Turboは差し替える先を見つけられず、中身が入りません。
src 側のフレームIDと、返すHTMLのフレームIDが完全に一致しているかを最初に確認してください。dom_id(@post) のように動的なIDを使う場合は、両側で同じ式になっているかに注意します。
② ルーティング・アクション・部分テンプレートの用意漏れ
src に指定したパスは、実在するルーティングとアクションを指している必要があります。activity_feed_path を書いたのに routes.rb に定義が無い、といったケアレスミスが起きがちです。bin/rails routes でパスヘルパーの有無を確認しましょう。
③ レイアウトを二重に描画してしまう
フレーム読み込みのレスポンスに、ヘッダーやフッターまで含んだフルのレイアウトが乗るのは無駄です。Turboは同IDのフレーム部分しか使わないので、レイアウトを外しておくと転送量を減らせます。
turbo_frame_request? でフレームからのリクエストかどうかを判定し、その場合だけレイアウトを外すのが定番です。
def show
@activities = Activity.recent.includes(:user).limit(20)
# フレームからの読み込みならレイアウトを外して軽くする
render layout: false if turbo_frame_request?
end「遅い原因を潰す」→「残りは遅延で逃がす」の合わせ技
遅延読み込みは強力ですが、遅いものを遅いまま後回しにするだけでは、フレームの中身がなかなか出てこず、結局ユーザーを待たせます。順番としては、まず遅い原因そのものを潰すのが先です。
典型的なのがN+1クエリです。上のコントローラで includes(:user) を付けているのは、一覧表示でユーザーを1件ずつ引かないための対策です。クエリを最適化して速くできる部分は速くし、それでも重い外部API・重い集計だけを遅延で逃がす——この順番が体感速度を最大化します。
フィードや一覧の作り方そのものは、Railsブログ機能の実装方法7選でも触れています。あわせて読むと、どこを別フレームに切り出すかのイメージが掴みやすいはずです。
まとめ
Turbo Framesの遅延読み込みは、JavaScriptをほとんど書かずに「一部だけ遅いページ」を速く見せる、Rails標準の武器です。ポイントを振り返ります。
- 重い部分を
turbo_frame_tag ..., src:, loading: :lazyで囲み、別アクションに切り出す - 読み込み先のビューには同じIDのフレームを必ず置く(差し込まれない最大の原因)
do ... endにスケルトンを入れて、待ち時間の印象を良くする- すぐ見える塊は
eager、スクロール先の塊はlazy - まずN+1などの遅い原因を潰し、それでも重い部分だけを遅延で逃がす
JavaScriptを増やさずにUXを上げたい、という方針はimportmapでJSライブラリを使う方法とも相性が良いです。まずは一番重い一箇所を loading: :lazy に置き換えるところから、試してみてください。
スポンサーリンク
