# frozen_string_literal: true # :markup: markdown module ActionController # :nodoc: # # Action Controller Streaming # # Allows views to be streamed back to the client as they are rendered. # # By default, Rails renders views by first rendering the template and then the # layout. The response is sent to the client after the whole template is # rendered, all queries are made, and the layout is processed. # # Streaming inverts the rendering flow by rendering the layout first and # subsequently each part of the layout as they are processed. This allows the # header of the HTML (which is usually in the layout) to be streamed back to # client very quickly, enabling JavaScripts and stylesheets to be loaded earlier # than usual. # # Several Rack middlewares may not work and you need to be careful when # streaming. This is covered in more detail below, see the Streaming@Middlewares # section. # # Streaming can be added to a given template easily, all you need to do is to # pass the `:stream` option to `render`. # # class PostsController # def index # @posts = Post.all # render stream: true # end # end # # ## When to use streaming # # Streaming may be considered to be overkill for lightweight actions like `new` # or `edit`. The real benefit of streaming is on expensive actions that, for # example, do a lot of queries on the database. # # In such actions, you want to delay queries execution as much as you can. For # example, imagine the following `dashboard` action: # # def dashboard # @posts = Post.all # @pages = Page.all # @articles = Article.all # end # # Most of the queries here are happening in the controller. In order to benefit # from streaming you would want to rewrite it as: # # def dashboard # # Allow lazy execution of the queries # @posts = Post.all # @pages = Page.all # @articles = Article.all # render stream: true # end # # Notice that `:stream` only works with templates. Rendering `:json` or `:xml` # with `:stream` won't work. # # ## Communication between layout and template # # When streaming, rendering happens top-down instead of inside-out. Rails starts # with the layout, and the template is rendered later, when its `yield` is # reached. # # This means that, if your application currently relies on instance variables # set in the template to be used in the layout, they won't work once you move to # streaming. The proper way to communicate between layout and template, # regardless of whether you use streaming or not, is by using `content_for`, # `provide`, and `yield`. # # Take a simple example where the layout expects the template to tell which # title to use: # # # <%= yield :title %> # <%= yield %> # # # You would use `content_for` in your template to specify the title: # # <%= content_for :title, "Main" %> # Hello # # And the final result would be: # # # Main # Hello # # # However, if `content_for` is called several times, the final result would have # all calls concatenated. For instance, if we have the following template: # # <%= content_for :title, "Main" %> # Hello # <%= content_for :title, " page" %> # # The final result would be: # # # Main page # Hello # # # This means that, if you have `yield :title` in your layout and you want to use # streaming, you would have to render the whole template (and eventually trigger # all queries) before streaming the title and all assets, which defeats the # purpose of streaming. Alternatively, you can use a helper called `provide` # that does the same as `content_for` but tells the layout to stop searching for # other entries and continue rendering. # # For instance, the template above using `provide` would be: # # <%= provide :title, "Main" %> # Hello # <%= content_for :title, " page" %> # # Resulting in: # # # Main # Hello # # # That said, when streaming, you need to properly check your templates and # choose when to use `provide` and `content_for`. # # See also ActionView::Helpers::CaptureHelper for more information. # # ## Headers, cookies, session, and flash # # When streaming, the HTTP headers are sent to the client right before it # renders the first line. This means that, modifying headers, cookies, session # or flash after the template starts rendering will not propagate to the client. # # ## Middlewares # # Middlewares that need to manipulate the body won't work with streaming. You # should disable those middlewares whenever streaming in development or # production. For instance, `Rack::Bug` won't work when streaming as it needs to # inject contents in the HTML body. # # Also `Rack::Cache` won't work with streaming as it does not support streaming # bodies yet. Whenever streaming `Cache-Control` is automatically set to # "no-cache". # # ## Errors # # When it comes to streaming, exceptions get a bit more complicated. This # happens because part of the template was already rendered and streamed to the # client, making it impossible to render a whole exception page. # # Currently, when an exception happens in development or production, Rails will # automatically stream to the client: # # "> # # The first two characters (`">`) are required in case the exception happens # while rendering attributes for a given tag. You can check the real cause for # the exception in your logger. # # ## Web server support # # Rack 3+ compatible servers all support streaming. module Streaming private # Call render_body if we are streaming instead of usual `render`. def _render_template(options) if options.delete(:stream) # It shoudn't be necessary to set this. headers["cache-control"] ||= "no-cache" view_renderer.render_body(view_context, options) else super end end end end