rails: ActionController::Streaming doesn't stream
ActionController::Streaming
appears to not actually stream anything on Rails master.
Reproduction script here. Run it as a rack app with puma config.ru
.
What I Expect
Viewing this page in the browser should immediately show “Starting…” and then “1”, “2”, “3” etc should appear gradually as the bytes are streamed to the browser.
What Actually Happens
Nothing is displayed until the entire response has completed rendering.
This can be confirmed in Chrome’s net-internals tab. The request is sent at 3507 milliseconds, and even the headers are not received until 9528 milliseconds.
I’ve tried a lot of different permutations of this. At first I thought maybe it was my webserver - nope, same behavior with Puma, Webrick and Unicorn (even with the fancy config or whatever you’re supposed to use). Then I thought it was because Rails might be rendering the layout, then flushing, then rendering the view template, then flushing, then back to the layout before flushing again - but no combination of sleep
s anywhere produced streaming output.
About this issue
- Original URL
- State: open
- Created 8 years ago
- Reactions: 2
- Comments: 22 (20 by maintainers)
The idea that HTTP/2 doesn’t support streaming because it doesn’t support
transfer-encoding: chunked
is wrong. ALL HTTP/2 streams are full-duplex “chunked” which is why “transfer-encoding: chunked” must not be used because it doesn’t make any sense for HTTP/2 which uses binary framing to sent “chunks”.@jakeNiemiec I saw HTTP/2 (H2) doesn’t support chunking which initially was a bummer. But from some very brief reading it sounds like H1.1 to H2 proxies can convert H1.1 chunks to H2 streams…
What I’m going to try doing is running Rails over standard H1.1 with chunking enabled, then proxy it to H2 and hopefully get the best of both worlds without rewriting anything. CloudFlare as a free H1.1 to H2 proxy option (via NGINX I believe) so I’ll try that first as low barrier to entry 🎉
If someone fancies rewriting Rails chunking to work with H2 streams (or my above idea doesn’t work) that would be awesome! Unfortunately Rack doesn’t support H2 yet so that would also need rewriting 😬
My findings:
Streaming of the layout works. AKA your head content (stylesheets and whatnot) will stream, then the view
<%= yield %>
always blocks. That’s what the demo app here demonstrates.It would of course be nice if views and partials streamed bit by bit too, then slow DB calls wouldn’t block all the content. Nonetheless, streaming in assets and maybe your navigation first is still very useful as the browser can parse the JS and CSS early ready for the content (especially with huge modern JS frameworks that take a while to parse on mobile).
Things that break it:
include ActionController::Live
bafflingly you can’t use Live and streaming templates in the same controller even though they seem to be 2 sides of the same coin.yield
withincontent_for
blocks streaming. Instead have aapplication_before.html.haml
andapplication_after.html.haml
to extract common layout bits into, so you don’t need to nest.content_for
(just switch toprovide
, easy change, nearly same semantics).yield :foo
s. AKA if you have ayield :bar
at the top of your template but theprovide :bar
is within your view then it blocks. Try to change your ordering or unnest views.Webpacker dev proxy broke streaming but I submitted a fixFix is merged.@tenderlove’s suggestion
content = template.render(view, locals, output, &yielder)
does get views and partials to output their contents early without blocking, unfortunately in the wrong order — the layout comes out after.So provided Webpacker gets fixed I plan to get our app streaming the head, that’ll be a really nice little performance gain site wide for almost no work 😄
I’d guess almost nobody has used streaming since 2016 which is a shame. Perhaps because it’s quite invisible when working, and hard to debug when not. It seems like a really great feature though.
I don’t think this can be closed, because the feature doesn’t do what it’s supposed to.
Since the release of Rack 3 standardises the underlying streaming mechanism, and I added a convenience method
#<<
must be supported by the output interface, in most cases it should be possible to use a streaming body and use the given stream argument to the ERB (or other template engine) for full streaming support.IIRC, I could only get streaming to work when I had a layout that had multiple
yield
s. It seemed to be the case that one chunk would be sent for everyprovide
block in the template (note thatprovide
names must be unique as described in the docs).Early hint headers are meant to be sent before response content, otherwise it has little value. I guess if streaming is used the client will parse the received content and detect required resources anyway. Then it’s not necessary for early hints?