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 ADDED
@@ -0,0 +1,3 @@
1
+ == 0.0.1 2010-04-13
2
+
3
+ * Hi.
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
@@ -0,0 +1,3 @@
1
+ module TemplateStreaming
2
+ VERSION = [0, 0, 1]
3
+ 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
+