• Stars
    star
    147
  • Rank 251,347 (Top 5 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 14 years ago
  • Updated over 11 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Progressive rendering for Rails 2.3.x.

Template Streaming

Progressive rendering for Rails 2.3.x.

Background

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

Typical Rails Profile

This is highly suboptimal. Many resources, such as external stylesheets, are completely static and could be loaded by the client while it's waiting for the server response.

The trick is to stream the response--flushing the markup for the static resources to the client before it has rendered the rest of the page. In addition to being able to render styles and images earlier, the browser can download javascripts, making the page responsive to input events sooner.

The main barrier to this in Rails is that layouts are rendered before the content of the page. The control flow must thus be altered to render the page in the order the client needs to receive it - layout first.

With streaming, your profiles can look more like this:

Progressive Rendering Profile

How

Just add the template_streaming gem to your application, and add a stream call for the actions you'd like to stream. For example, to stream just the index action of your HomeController, it would look like this:

class HomeController
  stream :only => :index

  def index
    ...
  end
end

To stream everything, just add stream to your ApplicationController.

Now you may pepper flush calls strategically throughout your views to force a flush, such as just after the stylesheet and javascript tags. flush may occur in both templates and their layouts.

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) must not be made in the view. In fact, these objects will be frozen when streaming.
  • An exception during rendering cannot result in a 500 response, as the headers will have already been sent. Instead, the innermost partial which contains an error will simply render nothing, and error information is injected into the foot of the page in development mode.

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.

Support

Template Streaming supports Rails 2.3.x, from 2.3.11 onward. In rubygems version syntax, that's ~> 2.3.11. Rails 3.1 and upwards ship with native support for streaming.

Streaming also requires a web server that does not buffer Rack responses. It has been tested successfully with Passenger, Unicorn, and Mongrel. Note that Unicorn requires the :tcp_nopush => false configuration option. Thin is only supported if the Event Machine Flush gem is installed. WEBrick does not support streaming. Please send me your experiences with other web servers!

Controller

Class methods:

  • stream - stream responses for these actions. Takes :only or :except options, like before_filter.

  • when_streaming_template - registers a callback to be called during render when rendering progressively. This is before the body is rendered, or any data is sent to the client.

Instance methods:

  • render has been modified to accept a :stream option. If true, the response will be streamed, otherwise it won't. This overrides the setting set by the stream method above.

Error Recovery

As mentioned above, headers are sent to the client before view rendering starts, which means it's not possible to send an error response in the event of an uncaught exception. Instead, the innermost template which raised the error simply renders nothing. This has the added advantage of minimizing the impact on your visitors, as the rest of the page will render fine.

When an error is swallowed like this, it is passed to an error hander callback, which you can set as follows.

TemplateStreaming.on_streaming_error do |controller, exception|
  ...
end

This is where you should hook in your error notification system. Errors are also logged to the application log.

In addition, in development mode, error information is injected into the foot of the page. This is presented over the top of the rendered page, so the result looks much like when not streaming.

Streaming Templates Effectively

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/layouts/application.html.haml

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

When streaming, however, you can do better: put the javascripts at the top of the page too, and fetch them asynchronously. This can be done by appending a script tag to the HEAD of the page in a small piece of inline javascript:

app/views/layouts/application.html.haml

!!! 5
%html
  %head
    = stylesheet_link_tag 'one'
    = stylesheet_link_tag 'two'
    = javascript_tag do
      = File.read(Rails.public_path + '/javascripts/get_script.js')
      $.getScript('#{javascript_path('jquery')}');
      $.getScript('#{javascript_path('application')}');
%body
    - flush
    = yield

public/javascripts/get_script.js

//
// Credit: Sam Cole [https://gist.github.com/364746]
//
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 );
  }
};

If you have inline javascript that depends on the fetched scripts, you'll need to delay its execution until the scripts have been run. You can do this by wrapping the javascript in a function, with a guard which will delay execution until the script is loaded, unless the script has already been loaded. Example:

Layout

!!! 5
%html
  %head
    = stylesheet_link_tag 'one'
    = stylesheet_link_tag 'two'
    = javascript_tag do
      = File.read(Rails.public_path + '/javascripts/get_script.js')
      $.getScript('#{javascript_path('jquery')}', function() {
        window.script_loaded = 1;

        // If the inline code has been loaded (but not yet run), run it
        // now. Otherwise, it will be run immediately when it's available.
        if (window.inline)
          inline();
      });
%body
    - flush
    = yield

View

- javascript_tag do
  window.inline() {
    // ... inline javascript code ...
  }

  // If the script is already loaded, run it now. Otherwise, the callback
  // above will run it after the script is loaded.
  if (window.script_loaded)
    inline();

In public/javascripts/get_script.js

//
// Credit: Sam Cole [https://gist.github.com/364746]
//
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 );
  }
};

Contributing

  • Bug reports
  • Source
  • Patches: Fork on Github, send pull request.
    • Include tests where practical.
    • Leave the version alone, or bump it in a separate commit.

Copyright

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

More Repositories

1

looksee

Supercharged method introspection in IRB
C
365
star
2

cast

C parser and code generator for ruby
Ruby
53
star
3

1pass

1Password on the command line.
CoffeeScript
49
star
4

respec

Rerun failing RSpec examples easily.
Ruby
48
star
5

redis_master_slave

Redis master-slave client.
Ruby
31
star
6

rack_after_reply

Rack hook which fires after the socket to the client is closed.
Ruby
25
star
7

tab-switcher

Like a window switcher for your Atom tabs.
CoffeeScript
23
star
8

inverse_of

Backport of ActiveRecord 2.3.6's inverse association support.
Ruby
14
star
9

junit_merge

Merges JUnit XML reports.
Ruby
12
star
10

ruby-bash-completion

Bash completion for ruby-related shell commands.
12
star
11

event_machine_flush

Flush EventMachine::Connection output buffers immediately.
C++
12
star
12

ritual

Sweet, simple Rakefiles for your gem.
Ruby
9
star
13

rspec_outlines

Cucumber-style scenario outlines for RSpec.
Ruby
8
star
14

db_nazi

Encourage good DB practices in Active Record migrations
Ruby
6
star
15

gem-fuzzy

Rubygems plugin to print info about installed gems, with fuzzy matching.
Ruby
6
star
16

to_json_template

Simple JSON templates for rails.
3
star
17

active_nomad

ActiveRecord objects with a customizable persistence strategy.
Ruby
3
star
18

command_rat

Ruby acceptance testing for command line applications
Ruby
3
star
19

freedom_patch

Monkeypatch without aliasing methods. Ever.
Ruby
2
star
20

flipit

Flip tables.
Ruby
2
star
21

dotemacs

My Emacs configuration
Emacs Lisp
2
star
22

dotfiles

My dot files.
Shell
2
star
23

frag

Generate fragments of files from shell commands.
Ruby
2
star
24

command_test

Test your ruby programs run commands exactly the way you expect.
Ruby
2
star
25

temporaries

Set temporary values declaratively.
Ruby
1
star
26

roc

An encrypted rolodex.
Ruby
1
star
27

hoard

Speeds up the load time of Ruby programs.
Ruby
1
star
28

bundler_autoloading

Adds support to Bundler for autoloading gems.
Ruby
1
star
29

utest.el

Emacs lisp unit testing DSL.
Emacs Lisp
1
star
30

vertical-motion

Atom commands to move to the vertically-previous/next line.
CoffeeScript
1
star
31

redis_scripts

Elegant redis scripting for ruby.
Ruby
1
star
32

active_email

Makes using ActionMailer fun.
Ruby
1
star
33

bulldog

A heavy-duty Paperclip - ActiveRecord attachments with flexible processing.
Ruby
1
star
34

dotatom

My Atom configuration
CSS
1
star