Technology
Introducing ViewComponent - The Next Level In Rails Views
First, there were partials. Partials were good. They helped us create amazing views. They helped us reuse existing code. And they helped us stay sane (code-wise). Then they made everything slow. Enter ViewComponent, the saviour, the messiah, the greatest thing since - the partials.
Ever wonder why your rails application slows down the longer you build it? Few might have this experience. Many of the applications that we build aren't big enough for this problem to manifest itself. Once in a blue moon, however, your app will get big enough for your partials to give you some serious heartburn. What follows is the story of how I developed that heartburn and what medicine I used to heal my app and my heartburn.
The Early Signs
It's September. We've just finished building a very simple prototype of our app - TeamHQ. It had several screens that showed the general planning flow. You could create projects, add tasks, move them to boards—all good stuff.
Our architecture was straightforward. Rails controllers, models, views, a bit of javascript and just pure HTML and CSS on the front end. (We try to stay away from complex javascript front-end frameworks because they are complex.) We were running on ruby 2.7 and used Dokku for MVP production deployment.
In all, things should have been easy and quick. But about 15 controllers in, we started to notice that some screens were slower than normal. Rendering task details screen often took at least 1.5 seconds.
After doing a little bit of digging, we found a simple answer. Some of the views were fetching objects inefficiently, and lazy loading was done incorrectly. So we figured that if we fix that, it will make the issue go away.
And it did. For a while.
We used Skylight.io for monitoring performance and noticed a significant decrease in response time. All good things. Life was awesome.
Little did we know that our troubles were just beginning.
Anonymous Pains
In November, we had deployed the MVP version of TeamHQ and had 10 beta users giving it a go.
We didn't notice our main problem right away because it was masked by another issue that arose from our misuse of javascript's anonymous functions.
You've seen anonymous functions before if you've used jQuery. It looks something like this:
$(document).on("ready", function() { // some code here })
The function() part of that piece of code is an anonymous function. And if you are navigating from page to page using a good old HTML method by fetching everything on each new request, then anonymous functions usually give you no trouble.
However, in the world of Turbolinks and now in Turbo, anonymous functions can become a severe memory leak and slow your pages down to a crawl.
For example, here's what happens when RailsUjs uses anonymous functions to handle a submit event. Every time it encounters a form, it adds a new function to handle it.
In this case, the page has 4 forms. So RailsUjs attaches 4 of exactly the same functions to the submit event handler. An even worse offender is the click event. At one point, we could see 15 anonymous functions attached to the same event.
Normally it wouldn't be a big deal. The functions are small, and your browser can handle things properly. In most cases, you'd be navigating from page to page, constantly clearing the memory and those stacked functions.
In our case, because of the combination of bad design and anonymous function's usage, we ended up with a scenario where certain pages became really slow.
In one use case, the issue was with the drag-n-drop library we were using. On the board page, we would trigger drag-n-drop library reload every time we added a new task. This created new anonymous functions every time. If you kept the page open for 3-5 days, it would become very slow. This was a severe browser memory leak.
Other users had experienced the same issue and complained about it.
We figured out the problem, the hard way. As I mentioned, bad design on our part and anonymous function's peculiarities combined to create this unsatisfactory experience for our users and us.
In the end, we were able to get through this part of our issues. We replaced our drag-n-drop library with dragulajs. It resolved many of our problems.
But the major pain was still ahead of us.
The Major Pain
In the middle of December, the real problem started to manifest itself. Up to now, we thought our app was slow, mainly because of javascript and lazy loading issues.
But it was the whole different beast that reared its ugly head and forced us to seek a different approach to building rails applications.
The Partially Ugly Beast
Few Rails developers might have these problems. Most applications people build with Rails are fairly simple, with a few thousand lines of code. There are some bigger ones for sure. Those probably use React for the front end or something similar. And very few are big enough to cause this problem to show itself.
Ours is in the middle range of those apps. But we came to this problem, not because of size.
We stumbled upon this issue because of over architecture.
Let me give you some numbers. As of today, we have 112 controllers, 34 models, 20 concerns, and 520 views.
In December, we had 99 controllers, 30 models, 15 concerns, and 539 views.
As you can see, we were view heavy. Our views consisted of template files like index.html.erb and many, many, many partials.
For example, our board view showed a list of tasks, and if you went through the code and counted how many times we loaded a partial, you'd see our problem with the over architecture.
This is our simplified board view load process:
Main File: boards/sprints/show.html.erb Load: boards/sprints/_todo_bucket.html.erb Load: _incomplete_list.html.erb For Each Task Load _task.html.erb _task_meta.html.erb meta/_name.html.erb name/_editable.html.erb name/_display.html.erb meta/_description.html.erb description/_editable.html.erb description/_display.html.erb meta/_subtasks_count.html.erb _task_details.html.erb details/_due_date.html.erb details/_editable.html.erb details/_display.html.erb _with_icon.html.erb details/_effort.html.erb details/_editable.html.erb details/_impact.html.erb details/_editable.html.erb details/_assigned_to.html.erb details/_editable.html.erb _task_menu.html.erb menu/_sprint_menu.html.erb menu/_edit_menu.html.erb
As you can see from this snapshot, there were many partials to load for each task. On a page with 20-30 tasks, the total load time was close to 2.5 seconds in development mode. On production, it was under 1 second, but still, it was too slow.
This example doesn't consider all the other partials that a Rails app also loads in the layout, headers, footers, etc.
So there was a lot of over-engineering happening.
You might be asking yourself a question. Why so many partials?
One reason was that partials are awesome, and you can isolate different sections of a complex element into many small files. It makes it easier to find things, fix bugs and reuse views.
For example, all of the task details and meta partials were also reused on several other screens. Having individual partials allowed for easy updating of specific sections of a task, for example. There were a few other advantages.
But all of this organization came at a high cost. Speed.
The more partials we added, the slower the application became.
We tried many different ways to fix it. We tried using caching. It improved a little. We tried ruby 3.0. That made things faster. We tried combining partials where possible. It improved it a little.
But those improvements weren't as good as we had hoped.
One reason for this was the dreaded allocations counts. Ours were in hundreds of thousands.
Allocations show the number of ruby objects allocated during an event's start and end time. So when you render a partial, it will tell you that it allocated 100 objects during that process.
This happens all the time, but unfortunately, partials and templates can become allocation hogs, which will immensely slow down your application.
This is important to know because almost every <%= => causes an allocation to happen. Every if statement, every for loop, every helper call. All of this matters. The more views/partials you have, the higher the number of allocations each of these views will produce.
As you can see from the image above, our application spent the longest amount of time processing each request's views. Active Record was usually the quickest part of the whole journey.
Rendering views is a costly operation. And if you over-engineer your partials, which we did, the result could be a complete disaster when it comes to speed.
What is the solution?
To solve this problem, some will go for a front-end javascript library to handle user interaction. And it might make sense in certain situations.
Others might suggest simplifying the views or maybe trying Haml or something similar.
And while these solutions might work, we have found a much better option: ViewComponent.
Seeing All Components
ViewComponent is a Rails library for building reusable components. GitHub developed it, and it has been in production for several years there.
With a ViewComponent system, you can create reusable, self-contained components and use them in your views instead of partials. React's component architecture inspired it, and it works on a similar principle.
Using components has several benefits. You can standardize your code and reuse your components throughout the app. And ViewComponent offers these benefits out of the box.
But it's claim to fame is this. It helps you:
- Write testable view components.
- And it improves the speed of your views 10x (according to their benchmarks).
Another reason for ViewComponent being a better choice for your views is how your data flows through your views (dealing with those if statements).
Let's focus on the main benefits. Starting with speed improvement benefit.
Before I dive into how View Component improves the speed, I would like to give you a high-level overview of how rails partials are loaded. Understanding this process will help you appreciate why ViewComponent is a better choice for your views.
How Partials Work
Every time a user requests a page, the following steps take place on the request's response end. This is the part that happens after your action method finishes executing.
Rails will:
1. Find the template to render
2. Load the template from the file
3. Compile the template (convert HTML and <%= code %> to ruby code)
4. If the template has other partials, repeat steps 1 - 4 until all partials have been compiled
5. Find the layout
6. Compile the layout
7. If the layout has other partials, repeat steps 1-4 until all layout partials have been compiled
8. Convert it all to a string
9. Send it back over the wire to the browser.
This is a high-level overview. Basically, what happens all the time is that your HTML partials are converted to ruby code. Then Rails runs that code, which produces a string, and Rails sends that string back to the browser.
What makes all of this slow is the number of partials you need to render and the amount of generated code (that's what happens when you compile partials). Every partial has to be loaded from the disk on each request, processed, compiled and executed. The code inside the partial's <%= %> tags ain't cheap, so it takes time and effort to process that as well -- on each request.
The more partials you have, and the bigger they are, the more time it will take to process each one.
You can speed things up by using caching and utilizing various partial-specific strategies. But it doesn't help reduce the processing time by that much.
How does ViewComponent make a difference?
To put it simply, ViewComponent is a better implementation of Rails partials. Instead of creating partials to display pieces of data in your views, you create components. They go in their own directory app/components. You follow a consistent standard for creating and writing these components.
Here's an example of the first component I wrote for the TeamHQ app.
class Ui::LinkComponent < ApplicationComponent def initialize(remote: false, type: "", **args) @link_type = type.to_s @link_args = args @extra_classes = @link_args.delete :class @link_classes = "text-#{@link_type} #{@extra_classes}" @link_args.merge!({ class: @link_classes}) @link_args.merge!({ data: { remote: true } }) if remote end def call content_tag :a, content, @link_args end end
It has two methods. The first one, initialize, setups the instance of the component with the data that this component will need. The second method, call, is called when the component needs to render itself.
From this code, you can see that we first set up the classes that we want all LinkComponents to have, and in the call method, we just render an A tag and pass link arguments to the tag.
This is how we use this component:
<%= render Ui::LinkComponent.new href: task_delete_path(task), remote: true, type: :danger do %> <%= icon :fas, "trash" %> Delete This Task? <% end %>
It seems a bit redundant to use a whole component for a link. And in some circumstances, it could be. However, if you want your whole application to maintain a similar theme, components offer unprecedented control.
For example, if you wanted all links with the type :danger to use a different class, you can just change it in the component, and it would replicate across the whole application. Amazing.
You could create a helper in this particular instance, but using a component is a better choice.
For larger components, this componentization makes even more sense. Other than making it easily reusable and standardizing UI, making chunks of your views into components will make your application faster.
Consider this piece of code:
<%= tag.div id: dom_id(task) do %> <div><%= task.name %></div> ... lots of code showing how task looks ... ... other partials, maybe a list of all the fields ... <% end %>
You could put it into a partial and reuse it using the <%= render %> method. But you will run into the same issue with speed and memory usage I had discussed before. The more views you have and the more complex they are, the longer it will take to load them, and the slower your user's experience will be.
ViewComponent solves this problem by encapsulating this partial into a component and then speeding up the loading time by compiling it at application boot time.
Here's how you can convert the partial above into a component.
apps/component/task_component.rb
class TaskComponent < ApplicationComponent def initialize(task:) @task = task end end
This is the component definition. It only has one method this time, initialize. You use it to pass the data to your component. To make the view part of the component, you create a new file that lives next to this component.
apps/component/task_component.html.erb
<%= tag.div id: dom_id(@task) do %> <div><%= @task.name %></div> ... lots of code showing how task looks ... ... other partials, maybe a list of all the fields ... <% end %>
As you can see, the HTML part of the component is almost the same as the partial.
When you create a component, you create a standalone object whose job is to produce an output based on the provided input. You give it some data, and you get back a string of HTML or whatever other data output you require.
This simplicity makes it easy to test and also makes it easy to make it load faster.
As opposed to partials, which are loaded on each request, components are preloaded when Rails boots up. So when your app is processing the view, it doesn't have to look for files, load them and compile them. All it has to do is instantiate a component instance.
This architecture design makes ViewComponent a lot faster, 10x faster than partials.
In our own experiments and now with the switch to components, we've noticed a significant performance improvement in development mode. For example, our Board view used to take 1746ms to load in development. Now it loads in 546ms. All averages, of course. And we haven't fully converted it to components, just about 1/4 of the way through that section of the views.
Testing Made Faster
Another primary benefit that ViewComponent provides is the ability to test your view code. With partials, the only way to test them was through expensive integration tests. For each test, you have to load the data and simulate the actual user browser interaction with the views. This takes a lot of time.
In our case, between 5-10 seconds per test, depending on the test. Sometimes longer.
Our AvatarComponent, for example, now takes less than a second to run even though it creates a vector image on the fly if an avatar png is not present. I can't tell you what it was before because we didn't test our avatar partial before.
ViewComponent GitHub's benchmarks show Component tests to be about 25ms. And you can probably get there with some optimizations and parallelization. But a decrease from 7sec on avg to less than 1 sec is an amazing improvement.
Additional Benefits
ViewComponent is a fairly new library, and besides providing the speed and testing improvements, it also comes with additional benefits.
The first one is standardization. I've already shown the example of using the LinkComponent for A links. It helps you create a standard library of components for your app and ensure a consistent look and feel across all views.
The second one is related to standardization: inheritance. You can create components that inherit functionality from other components. For example, we have a ButtonComponent that creates a simple Bootstrap UI button. We've also created a RemoteButtonComponent that inherits from the ButtonComponent. It allows us to create a button that handles remote AJAX submissions.
The third one is conditional rendering. We use this option to render certain components based on user access level. For example, when a user is looking at a board, and this user is not a specific project member, we don't want to show them that project name. So we use the render? method on the ProjectNameComponent to show or hide this component.
class Project::NameComponent < ApplicationComponent def initialize(project:, user:) @project = project @user = user end def render? @project.has_user? @user end end
The fourth benefit is rendering collections. When we render our TaskComponent in a board view or the project view, we can use each iterator. Like this:
<% tasks.each do %> <%= render Task::ProjectViewComponent.new task: @task %> <% end %>
Or we can make the code simpler using the with_collection method:
<%= render Task::ProjectViewComponent.with_collection @tasks %>
The fifth one is component previews. We haven't taken advantage of them yet. Component previews are similar to mailer previews. They help you see what each component will look like.
There are other benefits of the ViewComponent library. Many relating the development experience. I encourage you to give it a go, read through the documentation and watch the videos.
Here's Joel Hawksley's video that I found informative and useful for understanding how components work and how GitHub uses them.
Conclusion
We have spent the better part of the last two weeks integrating ViewComponent into our codebase. Our progress has been slow because we want to make sure that the components we create are well thought out and not over-engineered. We also have many partials. A few have now been converted to components. Many have been removed altogether.
We are contemplating converting some of the helpers into components. So far, the speed improvements have been out of this world. Testing views (components) has now become an important part of our process.
We're still learning, and as we continue our journey, we'll share some of our experiences here on this blog as part of our Tech and Tips Thursday or as separate blog posts.
For now, we hope you check out ViewComponent and see for yourself how useful it is.