Removing Vue, one component at a time
JavaScript was about 3% of the app's code and the main source of its security advisories. I removed Vue from the app without changing anything customers saw.
Make JavaScript less painful to maintain
- Problem
- JavaScript is about 3% of the app's code but the main source of its security advisories. Conflicting dependency versions have delayed at least two security patches by more than a month, and we rely on libraries that are no longer maintained, like Webpacker and Bootstrap 3.
- What success looks like
- Security patches go out within a week, without a cascade of other upgrades. No discontinued libraries, and critical dependencies stay up to date.
- Must and must not
- Must move off Webpacker
- Must move off Bootstrap 3
- Must stay compatible with Rails 7
- Must not remove anything customers use
Where it started
I wrote the problem statement above in March 2022. A few weeks earlier I'd tried to move us off Webpacker and had to roll it back, because Bootstrap 3 broke under the newer Webpack.
We fixed the tooling as a team over the next few weeks. I upgraded us to Bootstrap 5, a teammate moved us off Webpacker, and three of us shared converting the old CoffeeScript. That got us off the unmaintained libraries. Vue stayed, and so did the separate build it needed.
The approach
Four years later I went after the rest. Before touching any code, I wrote down how we'd decide what each Vue component should become:
- If the browser already does the job, use plain HTML. A native
<dialog>replaces a custom modal. - If a component only fetched JSON to build HTML, let Rails render the HTML and send it.
- If JavaScript is still needed, keep it small: a Web Component or a Stimulus controller.
It also matched where Rails 8 was heading, toward shipping behavior without a separate JavaScript build.
A decision per component
I started with the admin pages, then moved to the customer app. Each component got its own decision, most got their own pull request, and nothing customers saw was supposed to change.
| Component | Became | Why |
|---|---|---|
| Confirmation dialogs | Native <dialog> | The browser already handles opening, closing and focus. |
<modal> | Deleted | Nothing used it once every dialog was native. |
<async-block> | Turbo frame | It fetched JSON to build HTML. Rails can send the HTML. |
<domain-connection> | Server-rendered ERB | No client-side state. It rendered two values once. |
<datepicker> | Web Component | Needs real interaction, but works on its own. |
| YouTube embed | <lite-youtube> | An existing Web Component did the job. |
<import-dns> | Stimulus controller | Still interactive, so it kept a small controller. |
| Onboarding DNS zone page | Deleted | Adding a zone now uses the same flow as everything else. |
I wrote almost all of the pull requests, and keeping them small took some correcting. My first pass at dialogs was one pull request touching every modal in the admin, about 2,700 changed lines. That was too much to review well, so I closed it and redid it as three: close-only dialogs, cancel and delete confirmations, and confirmations where you type the name of what you're deleting.
Outcome
By then the team wanted to drop Vue's separate build entirely, so the default Rails stack would be enough. In July 2026 I removed the Vue runtime and its configuration. A week later the app loaded its JavaScript through importmaps, and DNSimple's public site followed.
Runtime dependencies went from 38 to 11. Nothing changed for customers, which was the point.
- 35 pull requests
- dnsimple-app
- Jan–Aug 2026
- after groundwork in 2022