template_streaming 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +3 -0
- data/LICENSE +20 -0
- data/README.markdown +163 -0
- data/Rakefile +22 -0
- data/doc/fast-profile.png +0 -0
- data/doc/slow-profile.png +0 -0
- data/lib/template_streaming.rb +172 -0
- data/lib/template_streaming/version.rb +3 -0
- metadata +88 -0
data/CHANGELOG
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 George Ogata
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
# Template Streaming
|
2
|
+
|
3
|
+
Rails plugin which enables progressive rendering for templates.
|
4
|
+
|
5
|
+
## Background
|
6
|
+
|
7
|
+
A typical Rails client-side profile looks something like this:
|
8
|
+
|
9
|
+
<img
|
10
|
+
alt="Typical Rails Profile"
|
11
|
+
src="http://github.com/oggy/template_streaming/raw/master/doc/slow-profile.png"
|
12
|
+
style="width: 100%"
|
13
|
+
/>
|
14
|
+
|
15
|
+
In almost all cases, this is highly suboptimal, as many resources, such as
|
16
|
+
external stylesheets, are static and could be loaded by the client while it's
|
17
|
+
waiting for the server response.
|
18
|
+
|
19
|
+
The trick is to output the response *progressively*--flushing the stylesheet
|
20
|
+
link tags out to the client before it has rendered the rest of the
|
21
|
+
page. Depending on how other external resources such javascripts and images are
|
22
|
+
used, they too may be flushed out early, significantly reducing the time for the
|
23
|
+
page to become interactive.
|
24
|
+
|
25
|
+
The problem is Rails has never been geared to allow this. Most Rails
|
26
|
+
applications use layouts, which require rendering the content of the page before
|
27
|
+
the layout. Since the global stylesheet tag is usually in the layout, we can't
|
28
|
+
simply flush the rendering buffer from a helper method.
|
29
|
+
|
30
|
+
Until now.
|
31
|
+
|
32
|
+
Template Streaming circumvents the template rendering order by introducing
|
33
|
+
*prelayouts*. A prelayout wraps a layout, and is rendered *before* the layout
|
34
|
+
and its content. By using the provided `flush` helper prior to yielding in the
|
35
|
+
prelayout, one can now output content early in the rendering process, giving
|
36
|
+
profiles that look more like:
|
37
|
+
|
38
|
+
<img
|
39
|
+
alt="Progressive Rendering Profile"
|
40
|
+
src="http://github.com/oggy/template_streaming/raw/master/doc/fast-profile.png"
|
41
|
+
style="width: 100%"
|
42
|
+
/>
|
43
|
+
|
44
|
+
Also provided is a `#push(data)` method which can be used to send extra tags to
|
45
|
+
the client as their need becomes apparent. For instance, you may wish to `push`
|
46
|
+
out a stylesheet link tag only if a particular partial is reached which contains
|
47
|
+
a complex widget.
|
48
|
+
|
49
|
+
## Example
|
50
|
+
|
51
|
+
Conventional wisdom says to put your external stylesheets in the HEAD of your
|
52
|
+
page, and your external javascripts at the bottom of the BODY (markup in
|
53
|
+
[HAML][haml]):
|
54
|
+
|
55
|
+
### `app/views/prelayouts/application.html.haml`
|
56
|
+
|
57
|
+
!!! 5
|
58
|
+
%html
|
59
|
+
%head
|
60
|
+
= stylesheet_link_tag 'one'
|
61
|
+
= stylesheet_link_tag 'two'
|
62
|
+
- flush
|
63
|
+
= yield
|
64
|
+
|
65
|
+
### `app/views/layouts/application.html.haml`
|
66
|
+
|
67
|
+
%body
|
68
|
+
= yield
|
69
|
+
= javascript_include_tag 'one'
|
70
|
+
= javascript_include_tag 'two'
|
71
|
+
|
72
|
+
With progressive rendering, however, this could be improved. As [Stoyan Stefanov
|
73
|
+
writes][stefanov], you can put your javascripts in the HEAD of your page if you
|
74
|
+
fetch them via AJAX and append them to the HEAD of your page dynamically. This
|
75
|
+
also reduces the time for the page to become interactive (e.g., scrollable),
|
76
|
+
giving an even greater perceived performance boost.
|
77
|
+
|
78
|
+
Of course, rather than using an external library for the AJAX call, we can save
|
79
|
+
ourselves a roundtrip by defining a `getScript` function ourselves in a small
|
80
|
+
piece of inline javascript. This is done by `define_get_script`
|
81
|
+
below. `get_script` then includes a call to this function which fetches the
|
82
|
+
script asynchronously, and then appends the script tag to the HEAD.
|
83
|
+
|
84
|
+
### `app/views/prelayouts/application.html.haml`
|
85
|
+
|
86
|
+
!!! 5
|
87
|
+
%html
|
88
|
+
%head
|
89
|
+
= define_get_script
|
90
|
+
= stylesheet_link_tag 'one'
|
91
|
+
= stylesheet_link_tag 'two'
|
92
|
+
= get_script 'one'
|
93
|
+
= get_script 'two'
|
94
|
+
- flush
|
95
|
+
= yield
|
96
|
+
|
97
|
+
### `app/views/layouts/application.html.haml`
|
98
|
+
|
99
|
+
%body
|
100
|
+
= yield
|
101
|
+
|
102
|
+
### `app/helpers/application_helper.rb`
|
103
|
+
|
104
|
+
module ApplicationHelper
|
105
|
+
def define_get_script
|
106
|
+
javascript_tag do
|
107
|
+
File.read(Rails.public_path + '/javascripts/get_script.js')
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def get_script(url)
|
112
|
+
javascript_tag do
|
113
|
+
"$.getScript('#{javascript_path(url)}');"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
### `public/javascripts/get_script.js`
|
119
|
+
|
120
|
+
//
|
121
|
+
// Written by Sam Cole. See http://gist.github.com/364746 for more info.
|
122
|
+
//
|
123
|
+
window.$ = {
|
124
|
+
getScript: function(script_src, callback) {
|
125
|
+
var done = false;
|
126
|
+
var head = document.getElementsByTagName("head")[0] || document.documentElement;
|
127
|
+
var script = document.createElement("script");
|
128
|
+
script.src = script_src;
|
129
|
+
script.onload = script.onreadystatechange = function() {
|
130
|
+
if ( !done && (!this.readyState ||
|
131
|
+
this.readyState === "loaded" || this.readyState === "complete") ) {
|
132
|
+
if(callback) callback();
|
133
|
+
|
134
|
+
// Handle memory leak in IE
|
135
|
+
script.onload = script.onreadystatechange = null;
|
136
|
+
if ( head && script.parentNode ) {
|
137
|
+
head.removeChild( script );
|
138
|
+
}
|
139
|
+
|
140
|
+
done = true;
|
141
|
+
}
|
142
|
+
};
|
143
|
+
head.insertBefore( script, head.firstChild );
|
144
|
+
}
|
145
|
+
};
|
146
|
+
|
147
|
+
The second profile was created using this code.
|
148
|
+
|
149
|
+
[haml]: http://haml-lang.com
|
150
|
+
[stefanov]: http://www.yuiblog.com/blog/2008/07/22/non-blocking-scripts
|
151
|
+
[get-script]: http://gist.github.com/364746
|
152
|
+
|
153
|
+
## Note on Patches/Pull Requests
|
154
|
+
|
155
|
+
* Bug reports: http://github.com/oggy/template_streaming/issues
|
156
|
+
* Source: http://github.com/oggy/template_streaming
|
157
|
+
* Patches: Fork on Github, send pull request.
|
158
|
+
* Ensure patch includes tests.
|
159
|
+
* Leave the version alone, or bump it in a separate commit.
|
160
|
+
|
161
|
+
## Copyright
|
162
|
+
|
163
|
+
Copyright (c) 2010 George Ogata. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
gem 'ritual'
|
2
|
+
require 'ritual'
|
3
|
+
|
4
|
+
spec_task :spec do |t|
|
5
|
+
t.libs << 'lib' << 'spec'
|
6
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
7
|
+
end
|
8
|
+
|
9
|
+
spec_task :rcov do |t|
|
10
|
+
t.libs << 'lib' << 'spec'
|
11
|
+
t.pattern = 'spec/**/*_spec.rb'
|
12
|
+
t.rcov = true
|
13
|
+
end
|
14
|
+
|
15
|
+
rdoc_task do |t|
|
16
|
+
t.rdoc_dir = 'rdoc'
|
17
|
+
t.title = "Template Streaming #{version}"
|
18
|
+
t.rdoc_files.include('README*')
|
19
|
+
t.rdoc_files.include('lib/**/*.rb')
|
20
|
+
end
|
21
|
+
|
22
|
+
task :default => :spec
|
Binary file
|
Binary file
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module TemplateStreaming
|
2
|
+
module Controller
|
3
|
+
def self.included(base)
|
4
|
+
base.alias_method_chain :render, :template_streaming
|
5
|
+
base.helper_method :flush, :push
|
6
|
+
end
|
7
|
+
|
8
|
+
def render_with_template_streaming(*args, &block)
|
9
|
+
# Only install our StreamingBody in the toplevel #render call.
|
10
|
+
@render_stack_height ||= 0
|
11
|
+
@render_stack_height += 1
|
12
|
+
begin
|
13
|
+
if @render_stack_height == 1
|
14
|
+
@performed_render = true
|
15
|
+
@streaming_body = StreamingBody.new(progressive_rendering_threshold) do
|
16
|
+
@performed_render = false
|
17
|
+
last_piece = render_without_template_streaming(*args, &block)
|
18
|
+
# The original render will clobber our response.body, so
|
19
|
+
# we must push the buffer ourselves.
|
20
|
+
push last_piece
|
21
|
+
end
|
22
|
+
response.body = @streaming_body
|
23
|
+
response.prepare!
|
24
|
+
else
|
25
|
+
render_without_template_streaming(*args, &block)
|
26
|
+
end
|
27
|
+
ensure
|
28
|
+
@render_stack_height -= 1
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Flush the current template's output buffer out to the client
|
34
|
+
# immediately.
|
35
|
+
#
|
36
|
+
def flush
|
37
|
+
unless @template.output_buffer.nil?
|
38
|
+
push @template.output_buffer.slice!(0..-1)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Push the given data to the client immediately.
|
44
|
+
#
|
45
|
+
def push(data)
|
46
|
+
@streaming_body.push(data)
|
47
|
+
end
|
48
|
+
|
49
|
+
private # --------------------------------------------------------
|
50
|
+
|
51
|
+
#
|
52
|
+
# The number of bytes that must be received by the client before
|
53
|
+
# anything will be rendered.
|
54
|
+
#
|
55
|
+
def progressive_rendering_threshold
|
56
|
+
response.header['Content-type'] =~ %r'\Atext/html' or
|
57
|
+
return 0
|
58
|
+
|
59
|
+
case request.env['HTTP_USER_AGENT']
|
60
|
+
when /MSIE/
|
61
|
+
255
|
62
|
+
when /Chrome/
|
63
|
+
# Note: Chrome's UA string includes "Safari", so it must precede.
|
64
|
+
2048
|
65
|
+
when /Safari/
|
66
|
+
1024
|
67
|
+
else
|
68
|
+
0
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Only prepare once.
|
74
|
+
module Response
|
75
|
+
def self.included(base)
|
76
|
+
base.alias_method_chain :prepare!, :template_streaming
|
77
|
+
base.alias_method_chain :set_content_length!, :template_streaming
|
78
|
+
end
|
79
|
+
|
80
|
+
def prepare_with_template_streaming!
|
81
|
+
return if defined?(@prepared)
|
82
|
+
prepare_without_template_streaming!
|
83
|
+
@prepared = true
|
84
|
+
end
|
85
|
+
|
86
|
+
def set_content_length_with_template_streaming!
|
87
|
+
if body.is_a?(StreamingBody)
|
88
|
+
# pass
|
89
|
+
else
|
90
|
+
set_content_length_without_template_streaming!
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
module View
|
96
|
+
def self.included(base)
|
97
|
+
base.alias_method_chain :_render_with_layout, :template_streaming
|
98
|
+
end
|
99
|
+
|
100
|
+
def _render_with_layout_with_template_streaming(options, local_assigns, &block)
|
101
|
+
with_prelayout prelayout_for(options), local_assigns do
|
102
|
+
_render_with_layout_without_template_streaming(options, local_assigns, &block)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def with_prelayout(prelayout, locals, &block)
|
107
|
+
if prelayout
|
108
|
+
begin
|
109
|
+
@_proc_for_layout = lambda do
|
110
|
+
# nil out @_proc_for_layout else rendering with the layout will call it again.
|
111
|
+
@_proc_for_layout, original_proc_for_layout = nil, @_proc_for_layout
|
112
|
+
begin
|
113
|
+
block.call
|
114
|
+
ensure
|
115
|
+
@_proc_for_layout = original_proc_for_layout
|
116
|
+
end
|
117
|
+
end
|
118
|
+
render(:file => prelayout, :locals => locals)
|
119
|
+
ensure
|
120
|
+
@_proc_for_layout = nil
|
121
|
+
end
|
122
|
+
else
|
123
|
+
yield
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def prelayout_for(options)
|
128
|
+
layout = options[:layout] or
|
129
|
+
return nil
|
130
|
+
# Views can call #render with :layout to render a layout
|
131
|
+
# *partial* which we don't want to interfere with. Only the
|
132
|
+
# interlal toplevel #render calls :layout with an
|
133
|
+
# ActionView::Template
|
134
|
+
layout.is_a?(ActionView::Template) or
|
135
|
+
return nil
|
136
|
+
view_paths.find_template('pre' + layout.path_without_format_and_extension, layout.format)
|
137
|
+
rescue ActionView::MissingTemplate
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class StreamingBody
|
142
|
+
def initialize(threshold, &block)
|
143
|
+
@process = block
|
144
|
+
@bytes_to_threshold = threshold
|
145
|
+
end
|
146
|
+
|
147
|
+
def each(&block)
|
148
|
+
@push = block
|
149
|
+
@process.call
|
150
|
+
end
|
151
|
+
|
152
|
+
def push(data)
|
153
|
+
if @bytes_to_threshold > 0
|
154
|
+
@push.call(data + padding)
|
155
|
+
@bytes_to_threshold = 0
|
156
|
+
else
|
157
|
+
@push.call(data)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
private # -------------------------------------------------------
|
162
|
+
|
163
|
+
def padding
|
164
|
+
content_length = [@bytes_to_threshold - 7, 0].max
|
165
|
+
"<!--#{'-'*content_length}-->"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
ActionView::Base.send :include, View
|
170
|
+
ActionController::Base.send :include, Controller
|
171
|
+
ActionController::Response.send :include, Response
|
172
|
+
end
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: template_streaming
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- George Ogata
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-04-13 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :development
|
31
|
+
version_requirements: *id001
|
32
|
+
description: |
|
33
|
+
Adds a #flush helper to Rails which lets you flush the output
|
34
|
+
buffer to the client early, allowing the client to begin fetching
|
35
|
+
external resources while the server is rendering the page.
|
36
|
+
|
37
|
+
email:
|
38
|
+
- george.ogata@gmail.com
|
39
|
+
executables: []
|
40
|
+
|
41
|
+
extensions: []
|
42
|
+
|
43
|
+
extra_rdoc_files:
|
44
|
+
- LICENSE
|
45
|
+
- README.markdown
|
46
|
+
files:
|
47
|
+
- doc/fast-profile.png
|
48
|
+
- doc/slow-profile.png
|
49
|
+
- lib/template_streaming/version.rb
|
50
|
+
- lib/template_streaming.rb
|
51
|
+
- LICENSE
|
52
|
+
- README.markdown
|
53
|
+
- Rakefile
|
54
|
+
- CHANGELOG
|
55
|
+
has_rdoc: true
|
56
|
+
homepage: http://github.com/oggy/template_streaming
|
57
|
+
licenses: []
|
58
|
+
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options:
|
61
|
+
- --charset=UTF-8
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
segments:
|
69
|
+
- 0
|
70
|
+
version: "0"
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
segments:
|
76
|
+
- 1
|
77
|
+
- 3
|
78
|
+
- 6
|
79
|
+
version: 1.3.6
|
80
|
+
requirements: []
|
81
|
+
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 1.3.6
|
84
|
+
signing_key:
|
85
|
+
specification_version: 3
|
86
|
+
summary: Rails plugin which enables progressive rendering.
|
87
|
+
test_files: []
|
88
|
+
|