Speeding Up Rendering Rails Pages with render_async
semaphoreci.com
semaphoreci.com
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!
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" onerror="continue"/>
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.On the other hand, I really like to just throw HTML at the user without involving asynchronous JavaScript.
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.
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 :)
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.
Would love to see a write-up/notes/gist that demos this.
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...
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.
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.
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.
The limit applies only to persistent connections.
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
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.
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.
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?
@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.
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 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'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.
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.