Speeding Up Rendering Rails Pages with render_async(semaphoreci.com)
semaphoreci.com
Speeding Up Rendering Rails Pages with render_async
https://semaphoreci.com/blog/2017/06/08/speeding-up-rails-pages-with-render-async.html
48 comments
I was the original author of this gem, so it is nice to see it here on HN, I have since passed it on the the caring hands of Semaphore since they wanted to maintain and improve it :-)
The software was built for a forum where the admin buttons was only for specific users, but the rest of the frontend was the same, so we used render_async to render some content for some users, and other content for admins, and the rest of the page could be cached statically.
We later changed it to use edge side includes with Varnish. An example of this is where you add the to your HTML:
The software was built for a forum where the admin buttons was only for specific users, but the rest of the frontend was the same, so we used render_async to render some content for some users, and other content for admins, and the rest of the page could be cached statically.
We later changed it to use edge side includes with Varnish. An example of this is where you add the to your HTML:
<esi:include src="http://example.com/1.html" alt="http://bak.example.com/2.html"
This will make Varnish fetch the URL(s), assemble the page and present it to the client, so your backend might see more requests, but the client only sees one.It's an interesting approach. It's only async from the client perspective though. Right? The server still processes the action/render inline? (i.e. it's not spawning a sidekiq process as far as I can tell).
We recently implemented something that's async on the server, using a combination of pusher and sidekiq. This was particularly useful for cases where we access an external API and don't want to hold the unicorn process for too long. So when the response comes back we push it back to the client and display it. Would be interesting to maybe combine this approach with some server-push and background workers and what this gem is doing.
We recently implemented something that's async on the server, using a combination of pusher and sidekiq. This was particularly useful for cases where we access an external API and don't want to hold the unicorn process for too long. So when the response comes back we push it back to the client and display it. Would be interesting to maybe combine this approach with some server-push and background workers and what this gem is doing.
Render-async lets the client render the page, and then request "partials" from the server, the backend sees two different requests from the browser
The inline includes that Varnish does is synchronous for the client, but asynchronous in your backend, your backend sees multiple requests.
I am interested in your pusher+sidekiq implementation, it is a good idea, but it might be a lot of work :)
The inline includes that Varnish does is synchronous for the client, but asynchronous in your backend, your backend sees multiple requests.
I am interested in your pusher+sidekiq implementation, it is a good idea, but it might be a lot of work :)
It's definitely more involved, and might be slightly more complex to generalize, but in essence it's quite straight-forward:
1. Client sends a request via Ajax to a rails controller and simultaneously opens a private pusher channel to wait for a response.
2. Controller returns a response immediately and fires-off a sidekiq job.
3. The sidekiq job does the heavy lifting and pushes the results back via pusher (it can be JSON data, or a rendered partial).
4. Client receives the response via the pusher channel and shows it on the page.
1. Client sends a request via Ajax to a rails controller and simultaneously opens a private pusher channel to wait for a response.
2. Controller returns a response immediately and fires-off a sidekiq job.
3. The sidekiq job does the heavy lifting and pushes the results back via pusher (it can be JSON data, or a rendered partial).
4. Client receives the response via the pusher channel and shows it on the page.
How do you set up a "pusher channel" to the browser? Websocket?
Would love to see a write-up/notes/gist that demos this.
Would love to see a write-up/notes/gist that demos this.
Pusher is based on websockets, yes.
https://pusher.com/websockets
https://pusher.com/websockets
So you are suggesting using a front end cache server like Varnish is better then what you originally programmed for?
It depends - Varnish is great, but in case of error-handling I think it is better to handle that with JavaScript, so you can retry, and you avoid that a timeout or slowdown doesn't block your clients.
On the other hand, I really like to just throw HTML at the user without involving asynchronous JavaScript.
On the other hand, I really like to just throw HTML at the user without involving asynchronous JavaScript.
That's a good use-case!
At the very least, it's a very nice stopgap until a full JS frontend is in place. I intend on throwing this into my existing rails app for various optimisations while it awaits an SPA frontend.
I'm also using react-rails on my case to make a bridge between rails and react and it has been very useful so far, you can even render components server-side to test them with rspec.
I don't quite get the reference to "magic"... It's quite easy to get what this does, and surely not every function call can be considered "magic"?
You're right, of course – I would consider this 'Rails magic' because it fits the general pattern of all those cool UJS helpers that are available. Things like `form_for(remote: true)` or `link_to remote: true`. This is just an extension of the same principle, which I think of as being basically magic!
I kinda wish it worked more like UJS though, rather than rendering a partial with <script> tags it could be just rendering some arbitrary <div> tag that, when it exists on the page, fires an event for rendering a partial asynchronously on the page. the UJS helpers for doing this already exist, because `link_to remote: true` is a thing. instead of making the GET request when the link is clicked, we're just triggering the link to be clicked right there in the page.
How would an iframe help avoid needing JS?
Well in this case, it feels like you could easily substitute out the AJAX request with an inline iframe that would embed the asynchronous content into the page. That's not possible, unless you have fixed-size elements or Javascript, since you won't be able to have the frame adapt to the size of its content. It's definitely useful in some cases though – I know that various social plugins used to work like this (I don't know if they do any more!)
just use regular <frame>s and <frameset> ;-)
I think there is a more Rails-y way to do this. Derek Prior gave an excellent talk[1] at the recent RailsConf. The TLDW is "all-REST all-the-time." Applying this principal to the example in the article, instead of adding a custom `movie_rating` action, you would create a `MovieRatingsController` with a `show` action.
Additionally, you might create an SJR[2] template which injects the rating directly into the page, possibly avoiding the need for a separate gem.
[1] https://www.youtube.com/watch?v=HctYHe-YjnE
[2] https://signalvnoise.com/posts/3697-server-generated-javascr...
Additionally, you might create an SJR[2] template which injects the rating directly into the page, possibly avoiding the need for a separate gem.
[1] https://www.youtube.com/watch?v=HctYHe-YjnE
[2] https://signalvnoise.com/posts/3697-server-generated-javascr...
This could be done without a gem relatively easy if you're using Rails5+ & ActionCable.
Basically generate a uuid to use as a channel identifier, then pass that uuid to the front end to subscribe to. Pass the uuid to your ActionCable background job to do the expensive rendering in a background queue and push the information over the websocket channel. Have the JS insert it appropriately (maybe the contents of the element with an id of the uuid).
This would have the benefit of displacing the expensive computation in a background job, which wouldn't impact other web requests.
Basically generate a uuid to use as a channel identifier, then pass that uuid to the front end to subscribe to. Pass the uuid to your ActionCable background job to do the expensive rendering in a background queue and push the information over the websocket channel. Have the JS insert it appropriately (maybe the contents of the element with an id of the uuid).
This would have the benefit of displacing the expensive computation in a background job, which wouldn't impact other web requests.
I would use a technique like this sparingly. It looks to be the "web request" equivalent of an n+1 query.
Imagine you have a table of results and each one is taking time to render. You add this for each row, but you only test it locally with a few rows. In production, maybe that table has hundreds of rows.
You've just DOSed your server.
Imagine you have a table of results and each one is taking time to render. You add this for each row, but you only test it locally with a few rows. In production, maybe that table has hundreds of rows.
You've just DOSed your server.
Browsers usually limit the number of requests to a certain domain (around 8 requests if I'm not mistaken, but it may vary). So I don't think it'd be the cause of a self inflicted DOS.
Also, every solution must be used carefully where it makes sense, if it doesn't make sense, then a different solutions must be thought of.
True, though note that the limit is on active requests on the browser end (obviously). If your subrequests are "heavy" and the user refreshes a bunch, you're now going to have 6x (or 8x or whatever the limit is on their browser) the number of "zombie" requests still being processed on your server.
It takes the total cost to a server to render a page and multiplies it.
Under low load, it could make it faster for one user.
Under higher load, each initial page request will be more expensive and therefore will result in exhausted resources sooner.
Under low load, it could make it faster for one user.
Under higher load, each initial page request will be more expensive and therefore will result in exhausted resources sooner.
Only over an HTTP/1 connection. Chrome and FF limit it to 6.
That's incorrect. Browsers limit request to 8 req/domain at the same time.
Don't know why I was downvoted. For reference: https://en.wikipedia.org/wiki/HTTP_persistent_connection#Use...
The limit applies only to persistent connections.
The limit applies only to persistent connections.
Well, as with everything:
Before using a library blindly 1. Ask yourself which problem you want to solve 2. Educate yourself 3. Does the library solve you problem 4. Use with caution 5. Learn from you mistakes :D
Before using a library blindly 1. Ask yourself which problem you want to solve 2. Educate yourself 3. Does the library solve you problem 4. Use with caution 5. Learn from you mistakes :D
Those are great steps to follow for yourself and to reach new members of your team :)
I still hold that sparing use is important.
The overhead for steps before your controller action method can often be a significant portion of the overall response time.
I still hold that sparing use is important.
The overhead for steps before your controller action method can often be a significant portion of the overall response time.
In a way that scales horizontally; run all of those web workers!
It does seem somewhat dangerous to use on a collection. But for scenarios like a few parts of the page which depend on the logged-in-user, it seems pretty safe.
If you're interested in these types of improvements, you'll love intercooler[0]! It's basically this, with lots of more options and patterns. It's been great for me in the (small-ish) projects I've used it.
[0] http://intercoolerjs.org/
[0] http://intercoolerjs.org/
It's good to see useful libraries still being created for Rails. I plan to use this on one of my projects. Thank you.
Is it possible to configure render_async to make the request and DOM manipulation without JQuery? Rails 5.1+ will not continue to bundle JQuery with UJS anymore since UJS has been re-written in vanilla Javascript.
If it were possible, I'd find this feature more useful for smaller Rails apps that are in the middle ground between server rendered HTML and full blown SPA.
If it were possible, I'd find this feature more useful for smaller Rails apps that are in the middle ground between server rendered HTML and full blown SPA.
Does this work with HTTP caching?
i.e. if I use this to render a movie rating asynchronously, can I return a 304 not modified response and the client will insert a previously-delivered response fragment?
i.e. if I use this to render a movie rating asynchronously, can I return a 304 not modified response and the client will insert a previously-delivered response fragment?
Yes - assuming the underlying request uses a GET verb, you can avoid a trip to the server altogether if your cache headers were set aggressively enough on the response to the initial request.
I think this with a combination of caching can be very powerful.
why not use fibers or something to do db calls async and then join them at render.
Saves you all the network calls
For their example, you could also just do the database lookups asynchronously in the controller, which is good performance practice for queries that don't absolutely need each other. For example, similarly to their example, let's say you start with something like movies, which each have many movie ratings, and your controller looks like this:
You also want to make sure you understand thread safety and the fact that @movie and @ratings above are not contained to their threads (which we're actually using to our advantage here). But in the above example, you replacing a single thread where everything sees everything anyway, with multiple threads where everything still sees everything, so you're not really changing that aspect in the example above anyway.
@movie = Movie.find(params[:id])
@ratings = @movie.movie_ratings # or equivalently, @ratings = MovieRating.where(movie_id: @movie.id)
render
The second line above needs to do the movie lookup, in order to get its ratings; and the second line basically constructs a SQL query that looks up records from the movie_ratings table using the movie_id foreign key that joins them. However, you already have the movie_id before you look up the movie, because it's in params[:id]. So, you could instead do something like this: @movie = Movie.find(params[:id])
@ratings = MovieRating.where(movie_id: params[:id[)
render
However, Ruby being synchronous by default, the above will still do the same thing and take the same amount of time, since line 2 will synchronously wait for line 1 to finish. But now that you've disentangled the query for line 2 with the results from line 1, you can now explicitly do them asynchronously: [].tap do |threads|
threads << Thread.new do
@movie = Movie.find(params[:id])
end
threads << Thread.new do
@ratings = MovieRating.where(movie_id: params[:id])
end
end.each(&:join)
render
I've had projects where I abstracted the above logic to reusable methods like: def asynchronously(&block)
@async_actions ||= []
@async_actions << Thread.new do
block.call
end
end
def synchronize
@async_actions.each(&:join)
end
And then in my controller actions, the previous code becomes: asynchronously do
@movie = Movie.find(params[:id])
end
asynchronously do
@ratings = MovieRating.where(movie_id: params[:id])
end
synchronize
render
Or if that still seems verbose, since each block above is a simple one-liner, you could use alternate syntax like: asynchronously { @movie = Movie.find(params[:id]) }
asynchronously { @ratings = MovieRating.where(movie_id: params[:id]) }
synchronize
render
The thing you have to be careful about when doing direct queries, like above where both queries directly use params[:id], is when you have a situation where authorization to access the data plays a role. For example, if you replace movie with user, and ratings with purchases, now you can't just blindly look up purchases by the user id the untrusted request passed in. You'd want to first validate that the params[:id] being passed is valid for the current user sending the request.You also want to make sure you understand thread safety and the fact that @movie and @ratings above are not contained to their threads (which we're actually using to our advantage here). But in the above example, you replacing a single thread where everything sees everything anyway, with multiple threads where everything still sees everything, so you're not really changing that aspect in the example above anyway.
Bur this would still only start rendering things for the user after the slow lookup is done, so it would not help in this situation.
Yup, I guess I should clarify. This solves half the problem without a gem, which may or may not be good enough depending on one's situation.
So this is an alternative of designing your app FE to use AJAX or using ESI (edge side includes) right? With the caveat this is super rails specific?
Why is this a gem? It's two view helpers and like 30 lines of Ruby code, if that.
I've solved this problem the same way probably 5 times in rails apps and it's super easy to build this interface in yourself without taking on another dependency
I've solved this problem the same way probably 5 times in rails apps and it's super easy to build this interface in yourself without taking on another dependency
> Why is this a gem?
> I've solved this problem the same way probably 5 times in rails apps
Because gems avoid such code duplication, they're the correct way to "share" code between projects rather than reinventing the solution each time you need it.
> I've solved this problem the same way probably 5 times in rails apps
Because gems avoid such code duplication, they're the correct way to "share" code between projects rather than reinventing the solution each time you need it.
I've solved this problem the same way probably 5 times in rails apps
You've just answered your own question :)
There is a trade-off, like always. You've solved this problem 5 times, sure – but maybe each of those 5 times you've dome something slightly different. Or maybe you fix a bug with an edge case, and that bug fix doesn't get incorporated into other projects. There are downsides.
On the other hand, a dependency removes some simple code from your control. I don't think anybody in the Ruby community wishes to end up with the silliness of e.g. `isNAN` from Node.
You've just answered your own question :)
There is a trade-off, like always. You've solved this problem 5 times, sure – but maybe each of those 5 times you've dome something slightly different. Or maybe you fix a bug with an edge case, and that bug fix doesn't get incorporated into other projects. There are downsides.
On the other hand, a dependency removes some simple code from your control. I don't think anybody in the Ruby community wishes to end up with the silliness of e.g. `isNAN` from Node.
I originally built it as a gem because the company I worked at used it across several projects, I almost think it was my first Ruby gem back in 2013.
I also hoped others would find it useful, and they did! The people from Semaphore showed interest in maintaining my 30 lines of Ruby code, and now they even wrote a blogpost about it :)
I also hoped others would find it useful, and they did! The people from Semaphore showed interest in maintaining my 30 lines of Ruby code, and now they even wrote a blogpost about it :)
It would be nice if it worked without JavaScript though – an increasing pain-in-the-arse about the web generally. If only it were possible to have <iframe>s adjust to their content size, then we wouldn't need JavaScript at all!