This is a hacky implementation of out-of-order streaming for Rails applications.
because it's cool. And lets you defer loading of assets until they are ready without compromising TTFB.
in your controller, include ApplicationController::Suspending
and then use response.stream.write render_to_string
to make sure you don't close the response when rendering:
class PostsController < ApplicationController
include ApplicationController::Suspending
def show
response.stream.write render_to_string
end
end
in your ERB view, you can now use suspense
helper to render a fallback until the data you're looking at is ready.
This is using partials:
-
app/views/posts/show.html.erb
:<p>Comments:</p> <%= suspense( # the partial to render that will eventually replace the fallback partial: "comments", locals: { post: @post } ) do %> <%# this is the fallback we render while waiting for the partial to render %> Loading comments... <% end %>
-
app/views/posts/_comments.html.erb
:<%# we're sleeping to simulate a slow database query %> <% sleep 1 %> <ul> <% post.comments.each do |comment| %> <li><%= comment.body %></li> <% end %> </ul>
That's it.
tl;dr: Out of order streaming.
- We kick off the partial rendering in a different thread, storing it in a Thread::Queue so we can retrieve it later. (note: maybe we want to use Fiber in the future?)
- The
suspense
helper renders a<x-rails-suspense data-id="unique-id">
tag with the fallback content. - On
after_action
, we start draining the thread queue.- On each thread, we get the value and render:
<template data-for-suspense="unique-id">
tag- a
<script>
tag that replaces the<x-rails-suspense>
tag with the template content. then we remove the template from the html. This can probably use Turbo Streams to piggyback on the Turbo implementation.
- On each thread, we get the value and render:
- We close the response stream
- ./app/helpers/application_helper.rb is where the
suspense
function is implemented - ./app/controllers/application_controller.rb is where the
Suspending
module is implemented