Blog Asynchronous render, but faster, and without XHR/AJAX/Turbo

Asynchronous render, but faster, and without XHR/AJAX/Turbo

I've been using a simple trick on updown.io since May 2015 to make pages feel faster without doing much work. And I extracted it as a gem so you can too. It's called render-later and I thought it deserved a quick write-up (better late than never).

The problem

You know that annoying moment when you're building a dashboard and there's one metric that takes 200ms to compute? Maybe it's a database aggregation, an API call to some external service, or just a heavy calculation. The rest of your page is blazing fast, but this one thing is holding everything up. Of course you can (and should) cache it, but there will always be that first uncached load which will feel slow, and that's what the user will remember.

The usual solutions are:

  1. Render everything inline - Simple but slow. Your users stare at a white screen for 1s+ waiting for the server to finish.
  2. Load it with AJAX/XHR - For example using Turbo. Faster initial paint, but wow is it complex. You need a new endpoint, an Ajax call, loading spinners, error handling, and a bunch of JavaScript. Plus it's often slower to load because the browser has to parse all your JS, wait for domready, then start the Ajax call. That's a whole extra round-trip. It can be quicker in total if there's multiple partials loading in parallel and your server is able to serve them efficiently.

The trick

What if you could get the fast initial paint of the AJAX version, but with the simplicity of rendering inline? That's what render-later does.

It's quite simple: wrap your slow code in a render_later block, and it'll render an invisible <span> instead. Then at the end of your page (right before </body>), call <%= render_now %> to render all those deferred blocks and injects them in the corresponding <span> with inline JavaScript.

<div class="server">
  <%= server.name %>
  <%= render_later "srv-uptime-#{server.id}" do %>
    <%= server.uptime %> <!-- slow database call -->
  <% end %>
</div>

The magic happens when you enable HTTP streaming in your controller:

def index
  render stream: true
end

render-later demo GIF

Now the browser receives and renders the initial page while the server is still computing the slow parts. On a very slow network (say 3G with 500ms RTT), by the time your brother received the initial HTML, the server may have already completed the partial rendering and send it over the wire already. Because while the network was lagging, the server was working 🎉

Why it's faster than XHR/AJAX

Sequence diagram comparing response time with and without render-later

This is the part I find interesting. With XHR/AJAX, here's what happens:

  1. Browser loads the page
  2. Browser downloads and parses all JavaScript
  3. Browser waits for domready
  4. Only then does it start the AJAX call
  5. Server processes the request
  6. Browser updates the DOM

With render-later and streaming:

  1. Browser starts receiving the page
  2. Server is already rendering the slow parts in the background during the first HTML download
  3. Browser receives the complete data over the same socket as soon as it's ready and update the page in place.

You're leveraging the existing open connection. No additional latency, no loading spinners, no javascript parsing, no custom error handling (if it fails, the browser shows its native error page). The browser's loading indicator stays visible until everything is done, which is exactly what you want.

Some gotchas

I wouldn't be me if I didn't mention a few gotchas 😅

Nginx buffering: If you're behind nginx (and you probably are), you'll want to disable proxy buffering for streamed responses. Otherwise nginx batches everything together and defeats the whole purpose:

headers['X-Accel-Buffering'] = 'no'

Don't disable it globally though - that breaks caching and makes your site vulnerable to slow client attacks.

Flash & CSRF tokens: There's an annoying Rails bug where flash messages and CSRF tokens stick around forever with streaming. The workaround is to access them before rendering:

form_authenticity_token; flash
render stream: true

Rack 2.2.x ETag middleware: It broke streaming because it now processes the whole body to generate ETags. Quick fix is to set a Last-Modified header (which tells the ETag middleware to skip):

headers['Last-Modified'] = Time.now.httpdate

Server support: Not all Ruby servers support streaming. Passenger, Unicorn, and Puma work fine. WEBrick and Thin don't. In development, rails s uses Puma so you're good.

See the README for the up-to-date gotchas.

Is it worth it?

For updown.io, absolutely. Displaying up-to-date stats for dozens-hundreds of checks at once can be a bit slow. render-later lets me show the page structure instantly while the stats stream in. This way the UI feels very quick and the user can already interact with the page (e.g. edit a check) while the less critical data is loading.

The gem works on IE6+ (yes really, just DOM level 2), Rails 4.1+, and Ruby 2.3+. It has 71 stars on GitHub which means approximately 71 people found it useful enough to click a button, so I'd say mission accomplished 😄

All of that in less than 50 lines of code (inline javascript included)!

If you're dealing with a similar situation and don't want the complexity/latency of AJAX/XHR/Turbo, give it a try. Installation is just gem 'render-later' and wrapping your slow code. No JavaScript dependencies, no extra endpoints, no loading spinners, and some gotchas depending on your app setup.

And if you find issues or have ideas for improvements, PRs are welcome!

For example, parallel rendering would be cool especially for IO-bound computation, but as far as I tried the capture helper isn't thread-safe which makes it quite difficult.


Adrien Rey-Jarthon
Created on March 18, 2026
Comments