jQuery

Removing the last of jQuery from a decade-old dashboard

JQ

One of our longest-running client relationships involves a dashboard with jQuery code dating back nearly a decade, through several rounds of "we should really rewrite this" that never got past the planning stage because the dashboard was always too business-critical to risk a big-bang rewrite. This year we finally finished a multi-year effort to remove jQuery entirely in favor of vanilla JavaScript and, where interactivity warranted it, Vue components, closing out a project that had been chipped away at incrementally since it started.

Why this took years instead of months

The dashboard couldn't go down for a rewrite, and it couldn't be frozen while a parallel rewrite caught up, since the client's team was actively shipping new features to it throughout the entire migration period. That constraint shaped everything about how the project was run: rather than a dedicated rewrite sprint, jQuery removal happened opportunistically, whenever a developer touched a file for an unrelated feature or bug fix, they'd also modernize whatever jQuery code lived in that file if it was reasonably contained. It's a slower approach than a focused rewrite, but it meant the dashboard kept shipping features the entire time, with zero dedicated "rewrite" time ever appearing on a sprint plan that a client would have had to specifically approve and fund.

Most jQuery usage in the codebase was doing things the DOM API has natively supported for years: DOM traversal, class toggling, simple AJAX requests, which made a large share of the migration genuinely mechanical once we built a small internal reference doc mapping common jQuery patterns to their vanilla equivalents. `$(el).addClass()` becomes `el.classList.add()`, `$.ajax()` becomes `fetch()`, and so on; tedious, but low-risk, since the behavior being replaced was already well understood and the native replacements are stable, well-documented APIs rather than anything experimental.

  • Most jQuery usage in the codebase was doing things the DOM API has natively supported for years, DOM traversal, class toggling, simple AJAX, which made a large share of the migration mechanical.
  • The genuinely hard parts were a handful of jQuery UI widgets with no direct modern equivalent, which we rebuilt from scratch as small Vue components.

The hard 20%

The genuinely hard parts were a handful of jQuery UI widgets with no direct modern equivalent: a custom date-range picker with business-specific logic baked into it, and a drag-and-drop dashboard-tile reordering widget that several key users depended on daily and had very particular expectations about. We rebuilt both from scratch as small Vue components rather than trying to find or force a drop-in replacement library, since after searching we couldn't find anything that matched the exact behavior users had come to expect without introducing its own new set of quirks and edge cases to work around.

The drag-and-drop widget in particular took longer than we initially estimated, because "rebuild it to work the same" turned out to include several small behaviors nobody had documented anywhere, snap-to-grid thresholds, what happens when you drag a tile past the edge of the visible grid, keyboard-accessible reordering for one power user who never used a mouse for this feature. We only discovered the full scope of expected behavior through a round of user testing with the client's team, which is a step we'd build into the plan earlier if doing a similar migration again.

What we got for it

Removing jQuery from a codebase this old was never urgent, which is exactly why it took years to get done at all; nothing was ever broken enough to force the issue, so it competed for priority against actual feature work on every single sprint. The payoff, now that it's finished, is concrete: a bundle roughly 30% smaller, since jQuery and several jQuery UI modules are no longer shipped to every page regardless of whether that page actually used them, and a codebase new hires no longer need jQuery-specific knowledge to touch, which matters increasingly as fewer developers entering the field have ever worked with jQuery directly.

Tracking progress without a dedicated project plan

Because the work happened opportunistically rather than under a dedicated project plan, we needed a way to track progress that didn't depend on anyone remembering to update a spreadsheet. We wrote a small internal script that scans the codebase for jQuery usage patterns, `$(`, `.ajax(`, known jQuery UI widget initializations, and reports a rough count by file, which we ran monthly and posted in the team channel. Watching that number decline steadily over roughly three years, even though no single sprint was ever explicitly "the jQuery sprint," kept the effort visible and gave the team a concrete sense of progress on something that otherwise would have felt like it was happening nowhere in particular.

What we'd do differently starting over

If we were starting this kind of migration today rather than finishing one that began years ago, we'd invest earlier in the internal jQuery-to-vanilla reference mapping that we only wrote a couple of years into the project, once the mechanical translation questions had come up often enough that documenting the answers was clearly worth the time. Having that reference from day one would have made the mechanical 80% of the migration faster from the start rather than something the team gradually got efficient at through repetition. We'd also budget explicit user-testing time for any custom widget replacement much earlier, rather than discovering the full scope of expected behavior only after we thought a rebuild was functionally complete.

A specific incident that nearly derailed the whole approach

Partway through the second year, a jQuery plugin update, applied as part of a routine dependency bump nobody thought to flag as risky, introduced a subtle behavior change in event delegation that broke a handful of interactive elements across the dashboard in a way that took most of a day to properly diagnose. It was a sharp reminder of exactly the risk the whole migration was meant to eliminate: depending on aging third-party code that nobody on the current team fully understands the internals of. That incident, more than any architectural argument, was what got budget approval to accelerate the pace of the migration in its final year rather than continuing purely opportunistically.

Knowledge transfer for a codebase few current engineers had touched

Several of the original developers who'd written the oldest jQuery code had left the company years before this migration effort began, which meant a fair amount of the work involved reverse-engineering intent from code with no comments and no surviving institutional memory to consult. We started keeping a shared log of "here's what this weird jQuery code was actually trying to accomplish" notes as each piece was understood and migrated, both to inform the replacement implementation and so that knowledge wouldn't be lost again the next time someone needed to understand a related piece of the system down the line.

Client communication throughout a project with no visible milestones

Explaining this work to the client, who was paying for engineering time against a project with no visible feature output for long stretches, required a different kind of reporting than a typical feature project. We started including a short "technical health" section in monthly status updates specifically covering the migration's progress, framed around risk reduction and future velocity rather than anything the client could see in the product itself, which kept the work funded and understood rather than looking like time spent on nothing the client could point to.

We also made a point of celebrating the milestone internally once the last jQuery reference was removed, which might sound like a small gesture for a project with no visible feature output, but mattered for team morale given how many quarters of unglamorous, incremental work had gone into it with no single dramatic finish line along the way. A short internal announcement and a genuinely well-earned team lunch turned three years of opportunistic cleanup into a recognized accomplishment rather than work that just quietly stopped being necessary one day.

The opportunistic, chip-away-at-it-over-time approach is not one we'd necessarily choose again from scratch if a rewrite were unavoidable regardless, but for a system that had to keep shipping the whole time, it was the only realistic path, and we'd recommend the same approach to anyone facing a similarly business-critical legacy codebase that can't tolerate a dedicated rewrite window. It's the kind of project that never looks urgent on a roadmap and never stops being worth finishing anyway, one file at a time, until one day it simply is.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients