Eliminate Full‑Page Reloads with Turbo Streams in Rails 7
Turbo Streams let Rails push HTML fragments to the browser, eliminating full‑page reloads. This guide walks through a live‑updating posts list, explains key concepts, and discusses trade‑offs and best practices.
04 Oct 2026, 10:12 UTC

Problem: The Full‑Page Reload Tug‑of‑War
When building a dynamic web app in Rails, the most common pain point is that every form submission or data change forces a full page reload. The browser fetches a new HTML document, the server renders the entire view, and the user experiences a brief flicker or a loss of scroll position. For simple CRUD apps this is acceptable, but for real‑time dashboards, comment sections, or collaborative lists it becomes a bottleneck.
How Turbo Streams Cut Through the Noise
Turbo Streams, part of the Hotwire stack that ships with Rails 6.1+, let the server push small HTML fragments to the browser over a persistent connection. The browser applies those fragments to the existing DOM, so only the affected part of the page changes. This eliminates the need for full page reloads while keeping the server‑side rendering model intact.
Key concepts:
- Turbo Frame – a
<turbo-frame>tag with a uniqueidthat marks a region of the page that can be updated independently. - Turbo Stream – a server‑generated response (usually
text/vnd.turbo-stream.html) that contains one or more<turbo-stream>elements. Each element specifies an action (append, prepend, replace, remove) and a target frame id. - Transport – the connection used to deliver the stream. Turbo prefers a WebSocket (via
ActionCable), but falls back to HTTP long‑polling if the browser or network blocks WebSockets.
Worked Example: Live‑Updating a Post List
Below is a minimal Rails 7 app that demonstrates how to stream a new post into an index page without a reload.
# Gemfile
ruby '3.3'
gem 'rails', '~> 7.0.0'
# Turbo is included by default in Rails 7, but the JS package must be added
gem 'turbo-rails'
Run the Rails turbo installer to add the JavaScript package and import map:
bin/rails turbo:install
Generate a simple Post model and controller:
bin/rails g model Post title:string body:text
bin/rails g controller Posts index new create
Controller logic (app/controllers/posts_controller.rb):
class PostsController < ApplicationController
def index
@posts = Post.order(created_at: :desc)
end
def create
@post = Post.new(post_params)
if @post.save
respond_to do |format|
format.html { redirect_to posts_path, notice: "Post created." }
format.turbo_stream {
render turbo_stream: turbo_stream.append(
"posts", partial: "post", locals: { post: @post }
)
}
end
else
render :new
end
end
private
def post_params
params.require(:post).permit(:title, :body)
end
end
Index view (app/views/posts/index.html.erb):
<h1>Posts</h1>
<turbo-frame id="posts">
<ul>
<% @posts.each do |post| %>
<%= render partial: "post", locals: { post: post } %>
<% end %>
</ul>
</turbo-frame>
<%= form_with url: posts_path, method: :post, data: { turbo_stream: true } do |f| %>
<%= f.label :title %>
<%= f.text_field :title %>
<%= f.label :body %>
<%= f.text_area :body %>
<%= f.submit "Create Post" %>
<% end %>
Partial for a single post (app/views/posts/_post.html.erb):
<li id="post-<%= post.id %>">
<b><%= post.title %></b>
<p><%= post.body %></p>
</li>
When the form is submitted, Rails sends a turbo_stream response that contains:
<turbo-stream action="append" target="posts">- Inside it, the rendered
_postpartial.
The browser receives this stream via the WebSocket connection and automatically inserts the new <li> into the <ul> inside the <turbo-frame id="posts"> element. No full page reload occurs.
How to verify:
- Open the browser console and look for a
WebSocketconnection tows://localhost:3000/cable(or the relevant URL). - Submit the form and watch the Network tab. You should see a
POST /postsrequest that returnstext/vnd.turbo-stream.htmland aturbo_stream.appendelement. - Observe the new post appear instantly in the list.
Trade‑offs and Limitations
While Turbo Streams dramatically reduce latency and improve UX, they come with caveats:
- JavaScript bundle size – Adding
turbo.jsincreases the client bundle by ~50 KB. For very lightweight sites this may be noticeable. - Connection reliability – If a corporate firewall blocks WebSockets, the fallback to HTTP long‑polling is slower and may not support true push semantics.
- Complex state – Turbo Streams are great for CRUD‑style updates. Apps that maintain intricate client‑side state (e.g., drag‑and‑drop grids) may still require additional JavaScript to reconcile server pushes.
- Debugging difficulty – Because the updates are implicit, it can be harder to trace why a particular element changed. Inspecting the Network tab and using the
console.log(turbo)helpers can help.
Actionable Next Steps
1. Add Turbo to your existing Rails app by running bin/rails turbo:install and ensuring the turbo-rails gem is in your Gemfile.
2. Wrap dynamic sections in turbo-frame tags and expose controller actions that respond to format.turbo_stream.
3. Test the connection in development. Verify the WebSocket is open and that the server returns turbo_stream responses.
4. Plan for fallbacks by enabling HTTP long‑polling in environments where WebSockets are blocked. This is automatic if ActionCable.server.config.allowed_request_origins is misconfigured.
5. Monitor performance in production. Use tools like New Relic or Scout to measure the impact of the additional JavaScript and the latency of server pushes.
By following these steps, you can replace full page reloads with real‑time, server‑driven updates that keep your users engaged and your codebase maintainable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.