Published February 6, 2025 · Last updated April 30, 2026 · Neel Shah, with visual designer Logan LaBo

ShiftGen is an ER admin staff scheduling application originally built in 2013 on Ruby 1.8 and Rails 2.3. With visual designer Logan LaBo, I led a frontend modernization onto ViewComponent, Stimulus, and Tailwind. 350+ development hours, 300+ files touched, over 11,000 lines of code, and the largest schedules' render times reduced from blocked-CPU stalls to just a few seconds.
The client's goal was to preserve the functionality while modernizing the application and improving user workflows on the frontend where possible without backend changes. We approached this challenge with the following plan:
Throughout the process, with help from my visual design buddy Logan, we were able to formalize design concepts and gather feedback from the client and iterate where needed. There was a lot of creative freedom, and we leveraged that to tackle some key obstacles.
One of the views critical to the application is the Schedule Modification view. This administrative view displays a Schedule consisting of individual Shifts on each day, each with an assignee, and enables various administrative actions.
The Shift Calendar ended up being the most complicated and feature-rich component due to the fact that a lot of the views presented information this way, and we wanted a consistent experience for the user. In the existing UI, the Shift Calendar is presented as a table where each cell is a Shift and admins can take actions on a Shift by clicking on the cell and then selecting an action.
In the new design, we replace the tabular display with a set of collapsible containers for each week, and allow hover interactions to expand the Shift actions. This creates a more dynamic space for the user to work in and gives us a more modular visual framework to use in the other views where the Shift Calendar is presented with different feature sets.
As an admin audits the Schedule and makes assignment changes, they need a way to visualize which Shifts a worker is assigned, when workers are unavailable, and even if they have put in a request to work a Shift. The Shift Highlight feature makes this possible with Highlighter indicators, however the old solution presented the user with potentially crowded visual information, leading to increased cognitive load. The information would be compounded when multiple workers were highlighted, which made it more difficult for an admin to work effectively with the view.
In the new design, there is a more subtle approach and consistent visual treatment between the primary and secondary highlighted workers to minimize the cognitive load for the admin. This approach allows them to understand worker status at a glance and get more information deliberately by focusing on a particular shift.
Shifts are organized into Shift Groups which can be used to filter the displayed Shifts. The existing design uses a simple and rigid checkbox-filled table to enable filtering.
We were able to improve the UX of the Shift filtering feature by introducing the concept of dynamic MultiToken Selects inside of a popover to replace the existing expansive and rugged design.
The server application was originally built in 2013 using Ruby on Rails and largely maintained on the original versions of Ruby 1.8 and Rails 2.3. The existing frontend was largely built using vanilla HTML/CSS and JavaScript with jQuery. For the new frontend, we proposed introducing some frameworks that would make development more efficient and save my client money in the long run. We arrived at the following stack after researching frameworks that would be more compatible with legacy Ruby on Rails projects and minimize friction with the existing development team's experience:
In order to use some of these frameworks though, we needed to upgrade the backend several major versions. We intially implemented UI Components in a sandbox in parallel with ShiftGen's backend engineers until the minimum requirements for the new frontend were met. We periodically helped with debugging and gave advice on prioritization of issues based on what was in scope for the frontend changes. Once the backend stack upgrade was completed, we ported the Component code into the existing application, which presented some new challenges involving the new Rails asset pipeline.
Now that we had a basic set of components integrated into the application, the refactoring began. The basic refactoring pattern was to use modular SSR ViewComponents styled with Tailwind and inject any necessary interactivity using Stimulus. Due to the high degree of coupling between the frontend and backend in the legacy code, refactoring several times was often necessary. We eventually found decent, reusable paradigms to replace all sorts of existing legacy micropatterns: CSRF token injection, form submission actions, and even interpretation of vanilla JS server responses.
Ultimately, after logging over 350 development hours, we touched more than 300 files and wrote over 11,000 lines of code to complete the project.
When this application was originally written, it was done incredibly simply, which minimized its client-side footprint. One of the notable changes after refactoring and implementing the new designs is the fact that the sheer scale of the size of the client-side HTML and JS code is an order of magnitude higher than before. There is significantly more rendered HTML code due to the use of ViewComponent and Tailwind (more complex layouts, repeated class names, etc.) and quite a bit more interactive and dynamic code execution due to the improved UI/UX decisions.
With a modern computer and browser, the average Schedule rendered and performed just fine in initial testing. However, it became clear that the outlier Schedules with more than a few hundred Shifts were slow to load. Load time was also a major issue on the All Site View, which presented data from multiple Schedules all on the same page. And so, we put on our code performance hard hat and started to dig.
Let's define Load Time as the time it takes to enable smooth user interactivity with the UI. This is a very important metric to optimize, as it directly affects the user experience and can be a major source of frustration.
By observing load times for different schedules, we can infer performance scales roughly linearly with the number of ShiftCell components.
We see that there are two parts to the delay:
We cannot dig deeper yet with the current implementation, as it is impossible to complete a Performance monitoring run in Chrome dev tools. From this we can infer that there is a significant CPU draw on the client after the server response.
Initially, the Tailwind classes seemed to be quite verbose, so steps were taken to move lower-level component Tailwind references into the component-specific Tailwind @layer. This reduced the response size by ~20% but did not seem to make a huge difference in the load time.
Downloading the page's HTML and analyzing node sizes, we see that the Options component HTML for all the Availability dropdowns in each ShiftCell totaled about 100MB. Removing these from the response reduced the response time by 40% and reduced CPU usage enough that Performance monitoring was able to be completed, but notably did not reduce the render time.
We also noted that the original implementation only included Availability on shifts assigned to the logged-in user, so we could actually make these render conditionally. This change alone is already a huge improvement.
After reviewing some Performance monitoring runs, we see the render time is largely comprised of querySelectorAll calls. With some research, we find that those calls were being made by the Stimulus framework in order to register ShiftCell outlets for the ShiftCalendar controller when it is initialized. Since these outlets are not needed during the initialization period, we use another mechanism to reference the ShiftCell controllers dynamically (for filtering/highlighting). We also take a final step to avoid applying filters when they were initialized as it was a no-op.
The new mechanism is significantly more efficient and reduced render times to just a few seconds!
Schedule render time reduced from minutes of blocked CPU to just a few seconds, after a targeted attack on the Stimulus querySelectorAll cost@layerThis project was a great experience refactoring a large, legacy codebase while modernizing the tech stack and UI/UX of an application that real ER admins depend on. It also came with unique UX challenges that were satisfying to address, and technical challenges that required thorough research and debugging skills. We delivered a holistic solution that improved both the UX and the further design and development roadmaps for the application, and the stakeholders of the project were quite pleased with the results.
The same playbook (modernize the frontend, leave the backend mostly alone, ship in phases, hand off something the in-house team can actually maintain) applies to a lot of nonprofit and product-team situations where a full rewrite is too risky or too expensive. If your software is showing its age and you need to move it forward without starting over, this is the kind of work I do.
Working on something similar?