Streaming Phlex from Sinatra
Sinatra supports streaming, Phlex supports streaming, but until recently I hadn’t put the two together in phlex-sinatra. Well now I have and from version 0.3 you can pass stream: true
like so:
get '/foo' do
phlex MyView.new, stream: true
end
When streaming is enabled Phlex will automatically flush to the response after a closing </head>
which means that the browser can start processing external resources as quickly as possible while the rest of the view is being generated. Even with no further intervention this small change should improve your Time to First Byte and could improve your First Contentful Paint.
It took very little code to integrate Phlex with Sinatra’s streaming but it did require a lot of learning and understanding, some of which will be useful to you dear reader. The first thing to note is that streaming with Sinatra requires a compatible server like Puma (it appears that WEBrick isn’t compatible) but the main thing to be aware of is the assortment of buffers between your code and the receiving client.
Buffers buffers buffers
It’s normal for a web framework to wait until a page has been fully generated (to buffer) before sending the response – so that the Content-Length
can be determined, among other things. Sinatra (et al) provides a way to bypass its buffer and write directly to the response, and it’s this object that’s passed to Phlex.
I was using curl to inspect the response and although I could now see the immediately-streamed HTTP headers (pass -i
/--include
to curl to include the headers in its output) the rest of the response was only shown once complete. I spent an age investigating both Sinatra and Phlex’s internals only to realise that everything was working correctly and the culprit was curl itself which has its own buffer. Curl behaves like other command-line tools and outputs lines but Phlex “uglifies by default” (it doesn’t add extra whitespace to generate pretty HTML) and this lack of newlines exacerbated the apparent problem – the trick is to pass -N
/--no-buffer
.
But streaming still wasn’t working as expected. It was then I discovered that Phlex has a buffer that’s automatically flushed after a closing </head>
tag, but it’s otherwise left to the developer to choose if and when to call the provided #flush
method (I’m guessing Phlex doesn’t write directly to the buffer due to some sort of performance penalty).
One more thing that I didn’t encounter while testing but that it’s worth being aware of is that other intermediaries can buffer the response without your knowledge, for example the reason for adding a X-Accel-Buffering=no
header is explained here (the whole article is well worth a read).
When to use streaming
So… I think ideally you shouldn’t need to use streaming at all but it’s easy to find yourself with a particularly problematic page (I’m thinking of a very long list or if it depends on some slow external service) where streaming can help. Adding pagination or completely rethinking the approach is quite probably the proper solution but enabling streaming combined with some judicious calls to #flush
could be enough to patch over the issue and give some breathing room for the real solution.