render-later 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3ed24ef6ef278f6b32265a03b669da4dfd2d89a5
4
+ data.tar.gz: 679cb4db160e2f287656ccfe4ef1b8e05484af3f
5
+ SHA512:
6
+ metadata.gz: ce8efe885bf50f0cb29b4070d739f812a1049109567198b39fd4f90e76d80def1b7c2771c5ea8fe658051673ad67c8f3af5cc61d46b883d6ac6cc06b79fcb8ec
7
+ data.tar.gz: 6db85e49c804372d9e887e288ce9390075410e2123e7e6089f57b128ac389f87fc5a86a4f5a848fe9bd5f177d15554787eb853369127b423de6c35df6de94af4
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /test/dummy/tmp
11
+ /test/dummy/log
12
+ /test/gemfiles/*.lock
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 2.1.8
5
+ - 2.2.4
6
+ - 2.3.0
7
+ gemfile:
8
+ - test/gemfiles/rails-4.1.gemfile
9
+ - test/gemfiles/rails-4.2.gemfile
10
+ matrix:
11
+ include:
12
+ - rvm: 2.3.0
13
+ gemfile: test/gemfiles/rails-5.0.gemfile
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in render-later.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Adrien Jarthon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,131 @@
1
+ # render-later [![Build Status](https://travis-ci.org/jarthod/render-later.svg?branch=master)](https://travis-ci.org/jarthod/render-later)
2
+
3
+ render-later is a Rails helper allowing you to **defer** the rendering of slow parts of your views to the end of the page, drastically improving the time to first paint and thus the perceived performance.
4
+
5
+ ![render-later-demo-gif](https://cloud.githubusercontent.com/assets/201687/12373435/779addfc-bc79-11e5-8863-64e985387d48.gif)
6
+
7
+ It works pretty simply by storing the blocks to render later and putting an invisible span with a unique ID instead. Then at the end of the page (right before `</body>`) it'll execute these blocks and insert inline javascript tags to replace the invisible spans with the real content. The trick is to enable HTTP streaming so the browser can render all the page quickly and add the deferred parts as they are received.
8
+
9
+ I've been using this since May 2015 on [updown.io](https://updown.io) without any trouble and decided it is time to extract it as a gem so everyone can use it. It is easy to use and pretty low-tech as it only requires HTTP/1.1 and DOM level 2 (IE6+). See the [compatibility section](#compatibility) for more details.
10
+
11
+ ## Usage
12
+
13
+ Add the gem to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'render-later'
17
+ ```
18
+
19
+ In your views, simply wrap a slow piece of code into a `render_later` block, and gives it a unique key as argument, like you would do with `cache`. Example:
20
+ ```erb
21
+ <div class="server">
22
+ <%= server.name %>
23
+ <%= render_later "srv-uptime-#{server.id}" do %>
24
+ <%= server.uptime %>
25
+ <% end %>
26
+ </div>
27
+ ```
28
+ It's important to use the `<%=` erb tag here, as the helper will render a hidden span tag.
29
+
30
+ In your layout, before the end of the body tag, add a call to `render_now`:
31
+ ```erb
32
+ <body>
33
+ <%= yield %>
34
+ <%= render_now %>
35
+ </body>
36
+ ```
37
+ This is where the deferred blocks will be rendered and injected into the page using inline javascript.
38
+
39
+ ## Performance
40
+
41
+ Performance-wise, this approach is quite interesting, let me explain why:
42
+ We know for a time that with bandwidth always increasing, web performance is getting more and more constrained by latency.
43
+
44
+ For cases like this, there is usually two ways:
45
+
46
+ 1. Render everything inline and slow down the page load.
47
+ 2. Load the slow data with Ajax, after the initial page is loaded.
48
+
49
+ The first solution is, IMO, decent **if using streaming**, because the browser can start loading css and js while the rest of the page is being streamed. But let's be honest, nobody does streaming, especially in the rails world, often leading to 1s+ page generation time (= 2s+ white screen for the user).
50
+
51
+ The second solution gets the first page rendered quickly but is much more complex to setup (needs another endpoint to expose the data, an Ajax call to fetch them, and some js to put the data back where it belongs, handle loading spinner, errors, etc.). It is also much **slower to load**, because the browser needs to load & parse all the javascript, wait to the entire page to be ready (domready), and only then it will start the ajax call, adding the whole round-trip time once again to the page loading.
52
+
53
+ Whereas with async-render, you leverage the existing open socket currently downloading the document to stream the deferred data:
54
+ - You don't have to handle errors, because it's the same request. So if it fails at this point, the browser will simply show it's native error page.
55
+ - You don't have to show any kind of spinner because the browser is still showing it's native loading indicator.
56
+ - You don't increase latency because the additional data start streaming right after the page is sent. And even better, the deferred data **starts rendering even if the browser didn't receive a single byte yet**, so if you're on a slow network (ex: 3G, 500ms rtt), instead of delaying even more (Ajax call), you'll actually receive the document **already complete**, because while the network lagged, the server was working :)
57
+
58
+ So in the end, this solutions is the best of both world: you get the first paint time of the Ajax version, but with the simplicity and time-to-full-document of the simple inline version.
59
+
60
+ The only downside of this approach is that the domeady event will only be executed at the end of the request, once the body is complete. See the [Gotcha section](#gotcha) for more details.
61
+
62
+ ## Compatibility
63
+
64
+ On the server side, the gem is tested against `ruby 2.1+` and `rails 4.1+`.
65
+ The dependency is not strictly enforced though and it may work with others versions but we don't guaranty anything.
66
+
67
+ The generated javascript is a simple inline function using nothing else than DOM Core level 2 properties, so it should work flawlessly on IE6+, Firefox 2+, Chrome 1+, Edge, Safari, etc.
68
+
69
+ ## Gotcha
70
+
71
+ #### domready
72
+ The domeady event will only be executed at the end of the request, once the body is complete. So if you have a lot of javascript on domready, which for example bind clicks to popups and ajax actions, there will be a timeframe during which the user will be able to click and the js isn't executed yet.
73
+
74
+ This is actually an issue with streaming or async javascript loading and has nothing to do with render-later, but as render-later pushes you to use streaming it feels right to warn you about this.
75
+
76
+ To circumvent this you can use event delegation and bind outside the domready event for example, and/or make sure your links won't lead to an error page if clicked without javascript.
77
+
78
+ #### Web server
79
+ Not all ruby web servers supports HTTP streaming unfortunately. So you obviously need to use one which does. Here is a non-exhaustive list of servers along with their support:
80
+
81
+ Server | Supported | Comment
82
+ ------------------|------------| -------
83
+ Phusion Passenger | ✔️ |
84
+ Unicorn | ✔️ | _with `tcp_nopush: false`_
85
+ puma | ✔️ |
86
+ WEBrick | ❌ | _default for `rails s`_
87
+
88
+ To try it in development I recommend simply adding `gem 'puma'` to your `Gemfile` and `rails s` will use it. Though it's totally fine to work with a server which doesn't support streaming, you just won't benefit from the speed.
89
+
90
+ #### Template Engine
91
+ Like for web servers, some template engine in Rails doesn't support streaming and requires to generate the entire page before sending it on the wire. You will need to avoid them at least for the layout page which will contain the `render_now` statement.
92
+
93
+ Template engine | Supported | Comment
94
+ ----------------|------------| -------
95
+ ERB | ✔️ | _rails default_
96
+ Slim | ✔️ |
97
+ Haml | ❌ |
98
+
99
+ #### Flash message & CSRF token
100
+ There is currently an [issue with Rails i'm trying to revive](https://github.com/rails/rails/issues/11476) about streaming causing trouble with flash messages and CSRF tokens. This is because rails waits the end of the request to check if you have used the flash message, if so it is removed from the store, otherwise it stays for the next request. But with streaming on, rails considers the requests as ended very early, before the flash message ever gets a change to be rendered. So this logic fails and keeps the flash message forever. The problem is exactly the same with the CSRF token.
101
+
102
+ The best **workaround** we currently have is to fool rails by accessing the flash message and CSRF token before the render call:
103
+
104
+ ```ruby
105
+ # in the controller, before every streaming render.
106
+ def index
107
+ form_authenticity_token; flash
108
+ render stream: true
109
+ end
110
+ ```
111
+
112
+ #### Caching
113
+ Finally, before your scratch your head, be careful not to put fragment caching **outside** a `render_later` block, because that would actually cache the empty span but not the inline script (which is generated by `render_now`), so it would be a waste and the real content will never be injected. It's totally **fine/recommended** to use fragment caching **inside** the `render_later` block.
114
+
115
+ ## Contributing
116
+
117
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jarthod/render-later.
118
+
119
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rake` to run the tests.
120
+
121
+ ## Ideas for improvement
122
+
123
+ - Wider test coverage across browsers using [Sauce Labs](https://saucelabs.com/opensauce/)
124
+ - Parallel rendering (I tried quickly but the `capture` helper isn't thread-safe)
125
+ - Add options to do caching at the same time (with the same key)
126
+ - Add options to customize the generated temporary element (could be used for loading indication or to avoid CSS conflict)
127
+
128
+ ## License
129
+
130
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
131
+
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/render-later/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,44 @@
1
+ module RenderLater
2
+ module ViewHelper
3
+ INSERT_FUNCTION = <<-JAVASCRIPT.freeze
4
+ function rl_insert(name, data) {
5
+ if (node = document.getElementById(name)) {
6
+ var div = document.createElement('div');
7
+ div.innerHTML = data;
8
+ var elements = div.childNodes;
9
+ for (var i = elements.length; i > 0; i--) {
10
+ node.parentNode.insertBefore(elements[0], node);
11
+ }
12
+ node.parentNode.removeChild(node);
13
+ }
14
+ };
15
+ JAVASCRIPT
16
+
17
+ def render_later key, &block
18
+ store_object(key, &block)
19
+ content_tag(:span, nil, id: "rl-#{key}", class: "rl-placeholder", style: 'display: none')
20
+ end
21
+
22
+ def render_now
23
+ return nil if deferred_objects.empty?
24
+ concat content_tag('script', raw(INSERT_FUNCTION))
25
+ deferred_objects.each do |key, block|
26
+ concat content_tag('script', raw("rl_insert('rl-#{key}', '#{j capture(&block)}');\n"))
27
+ end
28
+ nil
29
+ end
30
+
31
+ private
32
+
33
+ def store_object key, &block
34
+ objects = deferred_objects
35
+ raise Error, "duplicate key: #{key}" if objects.has_key?(key)
36
+ objects[key] = block
37
+ request.instance_variable_set(:@deferred_objects, objects)
38
+ end
39
+
40
+ def deferred_objects
41
+ request.instance_variable_get(:@deferred_objects) || {}
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,6 @@
1
+ require "render-later/version"
2
+ require "render-later/engine" if defined?(::Rails)
3
+
4
+ module RenderLater
5
+ class Error < StandardError; end
6
+ end
@@ -0,0 +1,4 @@
1
+ module RenderLater
2
+ class Engine < Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module RenderLater
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'render-later/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "render-later"
8
+ spec.version = RenderLater::VERSION
9
+ spec.authors = ["Adrien Jarthon"]
10
+ spec.email = ["me@adrienjarthon.com"]
11
+
12
+ spec.summary = %q{Defer rendering of slow parts to the end of the page}
13
+ spec.description = %q{Render-later allows you to defer the rendering of slow parts of your views to the end of the page, allowing you to drastically improve the time to first paint and perceived performance.}
14
+ spec.homepage = "https://github.com/jarthod/render-later"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.11"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "minitest", "~> 5.0"
25
+ spec.add_development_dependency "rails", ">= 4.0"
26
+ spec.add_development_dependency "puma"
27
+ spec.add_development_dependency "poltergeist"
28
+ end
metadata ADDED
@@ -0,0 +1,141 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: render-later
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Adrien Jarthon
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-01-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '4.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '4.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: puma
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: poltergeist
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Render-later allows you to defer the rendering of slow parts of your
98
+ views to the end of the page, allowing you to drastically improve the time to first
99
+ paint and perceived performance.
100
+ email:
101
+ - me@adrienjarthon.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - ".travis.yml"
108
+ - Gemfile
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - app/helpers/render_later/view_helper.rb
113
+ - lib/render-later.rb
114
+ - lib/render-later/engine.rb
115
+ - lib/render-later/version.rb
116
+ - render-later.gemspec
117
+ homepage: https://github.com/jarthod/render-later
118
+ licenses:
119
+ - MIT
120
+ metadata: {}
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ requirements: []
136
+ rubyforge_project:
137
+ rubygems_version: 2.5.1
138
+ signing_key:
139
+ specification_version: 4
140
+ summary: Defer rendering of slow parts to the end of the page
141
+ test_files: []