Template Streaming

Rails plugin which enables progressive rendering for templates.

Background

A typical Rails client-side profile looks something like this:

Typical Rails Profile

In almost all cases, this is highly suboptimal, as many resources, such as external stylesheets, are static and could be loaded by the client while it's waiting for the server response.

The trick is to output the response progressively--flushing the stylesheet link tags out to the client before it has rendered the rest of the page. Depending on how other external resources such javascripts and images are used, they too may be flushed out early, significantly reducing the time for the page to become interactive.

The problem is Rails has never been geared to allow this. Most Rails applications use layouts, which require rendering the content of the page before the layout. Since the global stylesheet tag is usually in the layout, we can't simply flush the rendering buffer from a helper method.

Until now.

Template Streaming circumvents the template rendering order by introducing prelayouts. A prelayout wraps a layout, and is rendered before the layout and its content. By using the provided flush helper prior to yielding in the prelayout, one can now output content early in the rendering process, giving profiles that look more like:

Progressive Rendering Profile

API

The API is simple, but it's important to understand the change in control flow when a template is streamed. A controller's render no longer results in rendering templates immediately; instead, response.body is set to a StreamingBody object which will render the template when the server calls #each on the body after the action returns, as per the Rack specification. This has several implications:

  • Anything that needs to inspect or modify the body should be moved to a middleware.
  • Modifications to cookies (this includes the flash and session if using the cookie store!) should not be made in the view.
  • An exception during rendering cannot simply replace the body with a stacktrace or 500 page. (Solution to come.)

Helpers

  • flush - flush what has been rendered in the current template out to the client immediately.
  • push(data) - send the given data to the client immediately.

These can only do their job if the underlying web server supports progressive rendering via Rack. This has been tested successfully with Mongrel and Passenger. Thin is only supported if the Event Machine Flush gem is installed. WEBrick does not support progressive rendering. Please send me reports of success with other web servers!

Controller

  • when_streaming_template - defines a callback to be called during a render call when a template is streamed. This is before the body is rendered, or any data is sent to the client.

Example

Conventional wisdom says to put your external stylesheets in the HEAD of your page, and your external javascripts at the bottom of the BODY (markup in HAML):

app/views/prelayouts/application.html.haml

!!! 5
%html
  %head
    = stylesheet_link_tag 'one'
    = stylesheet_link_tag 'two'
 - flush
 = yield

app/views/layouts/application.html.haml

%body
  = yield
  = javascript_include_tag 'one'
  = javascript_include_tag 'two'

With progressive rendering, however, this could be improved. As Stoyan Stefanov writes, you can put your javascripts in the HEAD of your page if you fetch them via AJAX and append them to the HEAD of your page dynamically. This also reduces the time for the page to become interactive (e.g., scrollable), giving an even greater perceived performance boost.

Of course, rather than using an external library for the AJAX call, we can save ourselves a roundtrip by defining a getScript function ourselves in a small piece of inline javascript. This is done by define_get_script below. get_script then includes a call to this function which fetches the script asynchronously, and then appends the script tag to the HEAD.

app/views/prelayouts/application.html.haml

!!! 5
%html
  %head
    = define_get_script
    = stylesheet_link_tag 'one'
    = stylesheet_link_tag 'two'
    = get_script 'one'
    = get_script 'two'
 - flush
 = yield

app/views/layouts/application.html.haml

%body
  = yield

app/helpers/application_helper.rb

module ApplicationHelper
  def define_get_script
    javascript_tag do
      File.read(Rails.public_path + '/javascripts/get_script.js')
    end
  end

  def get_script(url)
    javascript_tag do
      "$.getScript('#{javascript_path(url)}');"
    end
  end
end

public/javascripts/get_script.js

//
// Written by Sam Cole. See http://gist.github.com/364746 for more info.
//
window.$ = {
  getScript: function(script_src, callback) {
    var done = false;
    var head = document.getElementsByTagName("head")[0] || document.documentElement;
    var script = document.createElement("script");
    script.src = script_src;
    script.onload = script.onreadystatechange = function() {
      if ( !done && (!this.readyState ||
          this.readyState === "loaded" || this.readyState === "complete") ) {
        if(callback) callback();

        // Handle memory leak in IE
        script.onload = script.onreadystatechange = null;
        if ( head && script.parentNode ) {
          head.removeChild( script );
        }

        done = true;
      }
    };
    head.insertBefore( script, head.firstChild );
  }
};

The second profile was created using this code.

Note on Patches/Pull Requests

Copyright (c) 2010 George Ogata. See LICENSE for details.