How we use Hotwire to reduce loading time

Guillaume Briday profile picture

Guillaume Briday

Recently we installed Hotwire at Per Angusta. It's now part of the default Rails Stack and for me it defines the future of Web development when it comes to build reactive applications.

Hotwire is an alternative approach to build reactive applications without all the complexity we had with React/Vue and traditional SPA applications.

With Hotwire, you embrace the Majestic Monolith philosophy. You don't need an API, micro-services, multiple developers to build the front and the back of your application. You also don't need a complicated toolchain.

Recently, I found an easy trick to massively reduce the loading time of a page by lazy loading the content of some partials.

Before Hotwire, this could be relatively complicated. We needed custom JavaScript, or library to reduce page loading time, which I found to be inelegant and not easily maintained.

In our application, we use tabs a lot.

Some tabs in our app

As we can see, there is no reasons to load the content of all tabs before users actually click on them.

In our case, loading some tabs could take quite a long time in some scenarios. So it would be great if we could load those tabs only when needed.

Our original implementation

<%# Rest of the budget show page here... %>

<div class="tab-pane">
  <%= render partial: 'comments/comments', locals: { comments: budget.comments } %>
  <%= render partial: 'comments/new', locals: { comment: comment } %>

<%# and here... %>

Those partials are rendered with the rest of the page. So, in this case, the comments are generated before the whole page is send back to the browser.

So, if rendering this partial take 300ms, the user will have to wait these extra 300ms before seeing anything in their browser. Not cool but really easy to implement as developer.

How to lazy load these tabs?

Enhanced by Hotwire

Create a controller to render the partial with the required variables.

class CommentsController < ApplicationController
  def index
    budget = Budget.find(params[:budget_id])

    render partial: 'comments/comments', locals: { comments: budget.comments }

In the routes, add a resources to call our new controller:

resources :comments, only: :index

In our views, we just need to replace the partial render with a turbo_frame_tag with the correct src and the attribute loading: 'lazy'.

<%# Rest of the budget show page here... %>

<div class="tab-pane">
  <%= turbo_frame_tag :comments, src: comments_path(budget_id:, loading: 'lazy' do
    <%# Adding a nice loader that will be replaced once the partial gets loaded. %>
    <i class="fas fa-spinner fa-spin"></i>
  <% end %>

  <%= render partial: 'comments/new', locals: { comment: comment } %>

<%# and here... %>

By default, the src endpoint will be fetched as soon as possible in AJAX. By adding the attribute loading: 'lazy', the content will be fetched in AJAX only when the <turbo-frame> gets visible on the page.

This is exactly what we want for our tabs! 🎉

Now, when the server generates the page, it does not contain the comments, so it is basically instant to generate it before send it back to the browser. The comments will only be loaded if needed if the user clicks on the tab.

Here is the result:

Lazy loading comments with a 2s sleep for the example.

After implementing it at Per Angusta, we shrunk loading time by 61% on most pages on average.

How cool is that!? 🚀