If you're working with a huge app that has lots of Stimulus controllers, the recommended strategy to load all controllers upfront might not be suitable for you.
A few months ago, I encountered this problem at work while revamping how we load stimulus controllers on our Rails app to improve performance and reduce bundle size.
We are working with a Rails app. We have normal Stimulus controllers that live in
But we also heavily use view_components. A lot of view components have their own stimulus controllers too that live alongside their ruby/template/css files in
With vite, you usually have various entrypoints that you can then include as needed based on the interface. For example, you might have a separate entrypoints for admin, your marketing site, and that of the public facing side of your app. Webpack(er) calls these "packs".
Normally, a small sized app can usually just load all the controllers upfront in a single entrypoint and call it a day.
And that's what the official docs recommend. You can justify that on a small-medium sized app as usually it's not a lot of JS or all the JS is going to be needed soon anyway. And so the cost to load it may turn out to be a much better/simpler option.
Some bigger apps might also split loading stimulus controller into different entrypoints for different interfaces. One drawback to this splitting approach is that you have to maintain which controllers are to be loaded on a particular entrypoint in one way or another.
Or you might be fine with loading controllers lazily on the client as they become required using something like stimulus-controller-resolver.
For us, none of this was enough. The Rails app I was working with is huge. Really huge! It has a lot of different interfaces. And some of these interfaces by themselves are pretty big. And the approach to lazily load controllers can cause layout shifts in certain cases that we weren't too happy with.
We needed a way to only load JS for stimulus controllers that were used when the page was rendered.
And this needed to happen during rendering on the server by inserting
<script> tags in the
<head> tag so browsers load these scripts ASAP.
NOT lazily through a client side helper that watched the DOM for any elements with a data-controller attribute.
But also a way to load additional controllers if something changed due to an AJAX update.
What we settled on is configuring each stimulus controller as an entrypoint in vite. This is what our
vite.json looks like roughly with those changes:
Then, to know which controllers were used during rendering a page, we had to introduce a requirement to never write
Instead, we added a helper called
stimulus_controller that accepts the controller name (identifier) and emits the
data-controller attribute. But it also remembers the controller that was used in a
You can read more about current attributes here if you don't know what they are.
### In app/models/current.rb class Current < ActiveSupport::CurrentAttributes attribute :entrypoints_to_autoload end
<%# In any template like app/views/payments/show.html.erb %> ... <div class='h-10 w-full' <%= stimulus_controller('user-info-card') %>> ...
That should give you all the entrypoints that a page rendered in
Current.entrypoints_to_autoload. Now we need to render the script tag for those entrypoints.
And that should now be including scripts for the stimulus controllers when they are used on a page.
NOTE: If you're using stimulus controllers in your layout file itself, then, those won't be included in the head tag. For those, you will have to do the thing in the
<head> again before closing the
But wait! That only loads the JS. We still need to register those controllers to our Stimulus application with the name of the identifier.
Let's take a look at our stimulus application and an example stimulus controller so you know how to do that:
Optional - Automate registering controllers with a Vite plugin
One downside you can see from the above controller code is that we now have to remember to register the controller manually using
That's not a huge deal. But if you want you can totally automate that with a build plugin. The build plugin will allow you to write your stimulus controllers like:
Since we use vite, we can write a plugin for it that transforms this source to the one above automatically when serving/bundling the JS. It will transform your controller files to:
- Name your controller class if it's unnamed like above
- Register your stimulus controller class with your application using the imported helper
Lazy loading controllers after initial page load
This part is only necessary if you do AJAX requests that modify the DOM elements on the page and introduce new elements that use stimulus controllers that were not there on the initial page load.
We can use the
AttributeObserver provided by stimulus (which IIRC uses
MutationObserver under the hood) to watch the DOM for any new stimulus controllers and load them as soon as they come into play.
And that's all. You should now have a system for automatically loading controllers that were used on the page when rendering. And also the ability to automatically load new controllers when they are dynamically introduced on the page through an AJAX request.
It might seem a lot considering it's just loading stimulus controllers, but depending on your situation, it might be very worth it. It definitely was for us.
We did consider a bunch of alternative approaches, but settled on this approach as we found this to be the best fit for our use-case.
The code here is inspired from the actual implementation I did on the Rails app I was working on when I came across this problem, but I've modified/removed a lot of things that weren't directly related.
If you've got other ideas or questions, feel free to tweet at me @owaiswiz.