Stopping the Page Flicker: Using Turbo Frames for Partial Updates
Learn how to eliminate full-page reloads in Rails using Turbo Frames to create a faster, more fluid user experience with partial HTML updates.
22 Jun 2026, 05:37 UTC

The Jarring Full-Page Reload
In a traditional Rails application, clicking a link or submitting a form triggers a full-page reload. Even if only a small section of the page changes—like a comment list updating or a user profile editing in place—the browser discards the entire DOM, re-downloads the layout, and re-executes global JavaScript. This creates a visible flicker and resets the user's scroll position, making the web app feel like a series of static documents rather than a fluid interface.
The solution is Turbo Frames. By declaring specific regions of a page as independent frames, you can tell Rails to update only that fragment of the HTML without touching the rest of the page. This provides the responsiveness of a Single Page Application (SPA) without requiring a complex JavaScript framework.
How Turbo Frames Intercept Navigation
A Turbo Frame is defined by the <turbo-frame> HTML element. When a link or form inside a frame is activated, Turbo intercepts the request. Instead of a full browser navigation, it makes a fetch request to the server. The server responds with a full HTML page, but Turbo extracts only the <turbo-frame> with a matching ID from that response and swaps it into the current page.
This mechanism relies on a strict ID matching convention. If the request originates from a frame with id="messages", the server's response must contain a <turbo-frame id="messages">. If the ID is missing or mismatched, Turbo will ignore the response or, in some versions, trigger a full-page reload to ensure the user isn't stuck with a broken interface.
Worked Example: An Inline Post Editor
Imagine a blog where you want to edit a post title without leaving the index page. In a standard Rails 7 app (where Hotwire is included by default), you can implement this by wrapping the post in a frame.
1. The Index View
Wrap the post content in a frame. The ID should be unique to the record, such as post_#{post.id}.
<!-- app/views/posts/index.html.erb -->
<turbo-frame id="post_<%= post.id %)">
<p><%= post.title %></p>
<%= link_to "Edit", edit_post_path(post) %></turbo-frame>
2. The Edit View
The edit page must return a frame with the exact same ID. You can do this by wrapping the form in the same frame tag.
<!-- app/views/posts/edit.html.erb -->
<turbo-frame id="post_<%= @post.id %)">
<%= form_with model: @post do |f| %%
<%= f.text_field :title %%
<%= f.submit "Save" %%
<% end %></turbo-frame>
3. Verification
To verify this is working, open your browser's Network tab and click "Edit". You should see a request that returns HTML, but the browser URL will not change, and only the text for that specific post will swap to a form. Check the response body to ensure it is wrapped in <turbo-frame id="post_...">.
Handling Expensive Content with Lazy Loading
Turbo Frames aren't just for navigation; they can also defer the loading of heavy components. By adding loading="lazy" and a src attribute, the frame will remain empty (or show a placeholder) until it enters the user's viewport.
<turbo-frame id="comments" src="/posts/1/comments" loading="lazy"> <p>Loading comments...</p> </turbo-frame>This reduces the initial page load time and server pressure by only fetching the comments when the user actually scrolls down to see them.
Trade-offs and Constraints
While powerful, Turbo Frames introduce a few architectural challenges:
- URL Synchronization: By default, navigating inside a frame does not update the browser's address bar. If a user refreshes the page, they will be returned to the original state, not the frame's current state. You can use
data-turbo-action="advance"on a link to force a URL update, but this changes the navigation behavior. - The "Missing Frame" Trap: If your controller redirects to a page that doesn't contain the matching frame ID, the update will fail. This requires developers to be disciplined about wrapping responses in frames.
- JavaScript Lifecycle: Since the page doesn't reload, standard
window.onloadevents won't fire. You must use Turbo-specific event listeners (liketurbo:load) to re-initialize JavaScript components inside a frame.
Practical Implementation Path
To start integrating frames, identify the most "flickery" parts of your app—usually search filters, pagination, or inline editing. Wrap these in <turbo-frame> tags and ensure your corresponding controller actions render views with matching IDs. If you are using an older Rails app, ensure the turbo-rails gem is installed and the JavaScript is properly imported in your application manifest.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.