6 ms·
Speeding Up Rendering Rails Pages with render_async
- matthewmacleod 9y agoThat's actually a relatively nice, Rails-magic-style approach to solving this sort of thing. Of course, if you were building a more interactive application, you'd already have a JS framework in place that would negate these benefits – but I'm still convinced there's a nice middle-ground for server-rendered Rails apps that avoids the various problems of SPAs. 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!
- marsRoverDev 9y agoAt 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.
- tbranyen 9y agoHow would an iframe help avoid needing JS?
- matthewmacleod 9y agoWell 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!)
- realusername 9y agoI'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.
- kawsper 9y agoI 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: <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.
- ksec 9y agoSo you are suggesting using a front end cache server like Varnish is better then what you originally programmed for?
- kawsper 9y agoIt 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.
- gingerlime 9y agoIt'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.
- matt4077 9y agoI 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"?
- matthewmacleod 9y agoYou'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!
- tomphoolery 9y agoI 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.
- tomphoolery 9y agojust use regular <frame>s and <frameset> ;-)
- ewalk153 9y agoI 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.
- Colex 9y agoBrowsers 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.
- ewalk153 9y agoIt 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.
- mnarayan01 9y agoTrue, 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.
- nateberkopec 9y agoOnly over an HTTP/1 connection. Chrome and FF limit it to 6.
- mtarnovan 9y agoThat's incorrect. Browsers limit request to 8 req/domain at the same time.
- voidlogic 9y agoSo 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?
- alttab 9y agoWhy 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
- kawsper 9y agoI 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 :)
- gnaritas 9y ago> 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.
- matthewmacleod 9y agoI'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.
- alttab 9y agoWhy 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
- juliand 9y agoIt's good to see useful libraries still being created for Rails. I plan to use this on one of my projects. Thank you.
- makerightmoney 9y agowill it work?
- nateberkopec 9y agoDoes 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?
- nyargh 9y agoYes - 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.
- jonathanhefner 9y agoI 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 https://www.youtube.com/watch?v=HctYHe-YjnE [2] https://signalvnoise.com/posts/3697-server-generated-javascript-responses https://signalvnoise.com/posts/3697-server-generated-javascr...
- deedubaya 9y agoThis 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.
- jbverschoor 9y agowhy not use fibers or something to do db calls async and then join them at render. Saves you all the network calls
- JangoSteve 9y agoFor 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: @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.
- Dangeranger 9y agoIs 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.
- raman162 9y agoI think this with a combination of caching can be very powerful.
- mendelk 9y agoIf 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/ http://intercoolerjs.org/