render-later 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.travis.yml +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +131 -0
- data/Rakefile +10 -0
- data/app/helpers/render_later/view_helper.rb +44 -0
- data/lib/render-later.rb +6 -0
- data/lib/render-later/engine.rb +4 -0
- data/lib/render-later/version.rb +3 -0
- data/render-later.gemspec +28 -0
- metadata +141 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
@@ -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
|
data/lib/render-later.rb
ADDED
@@ -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: []
|