How to Migrate a Legacy jQuery Site to Modern JavaScript

Plenty of sites still running jQuery are perfectly healthy. The pages load, the forms submit, and nobody has complained in years. The real problem is the cost of change: the plugin you rely on was last updated in 2016, the build is a pile of concatenated files, and every new feature means negotiating with code nobody wants to touch. Migrating isn't about fashion. It's about making the front end editable again.

It also doesn't have to be a rewrite. A controlled migration — audit, replace, test, roll out — gets you to modern JavaScript without freezing the roadmap for six months.

Audit what jQuery is actually doing

Start with a search, not a plan. Run a project-wide search for "$(" and "jQuery." across your templates and scripts, and note where each hit lives. Then categorise them by the job they do: DOM selection and class juggling, AJAX requests, animation, form validation, or a third-party plugin.

Plugins decide your timeline, so list every one with its version and whether it is still maintained. Date pickers, carousels, modals and table sorters are the usual suspects, and most now have a native or well-supported replacement. While you're in there, flag the places where JavaScript is load-bearing: content that only appears after a click, navigation that depends on a script, forms that will not submit without one. Those are your risk points, and they set the order of work.

Decide whether jQuery stays or goes

There are two honest strategies. The first keeps jQuery loaded while you replace features one at a time, then removes the library in a final pass. The second rebuilds the front end wholesale. Choose the first unless the templates are being rewritten anyway, because it keeps the site shippable every week instead of every quarter.

Serving both libraries has a cost. jQuery is roughly 30KB minified and gzipped, and it can't be tree-shaken, so you pay for all of it even if you use a handful of methods. If what remains is two AJAX calls and some class toggling, the final removal is straightforward. If you have eight plugins, it isn't, and that's worth knowing before you promise a deadline.

Make progressive enhancement the backbone

A migration is the right moment to stop depending on JavaScript for things the browser already does well. Server-rendered links and forms should work with scripting switched off; the script layer then adds speed, filtering and inline validation on top. If a menu only opens with JavaScript, it still needs a real button element to trigger it — not an anchor with a href of "#", which keyboard users and screen readers will thank you for avoiding.

For newer APIs, detect the feature rather than the browser:

  • Check whether "fetch" is in window before you replace an AJAX helper.
  • Check for "IntersectionObserver" before you swap in lazy loading.
  • Check for the native dialog element before you drop a modal plugin.
  • Use CSS @supports for layout and animation, with a sensible fallback rather than a polyfill.

Where a polyfill is genuinely needed, load it conditionally: a short inline check that injects the script only when the feature is missing, or the nomodule attribute for older browsers. Never sniff the user agent string. It lies, and it keeps lying after every browser update.

Replace the patterns, not the whole file

Most jQuery code is a small set of idioms repeated many times. Learn the modern equivalent of each and the migration becomes mechanical:

  • $(document).ready(fn) — put the defer attribute on your script tag, or listen for DOMContentLoaded.
  • $('.card') — document.querySelectorAll('.card'), remembering it returns a NodeList you can iterate with for...of.
  • $el.on('click', handler) — addEventListener, with delegation via closest() when binding to a container.
  • $.ajax and $.getJSON — fetch, wrapped in one small helper. Don't forget response.ok: fetch does not reject on a 404.
  • addClass, removeClass, hasClass — classList, which is just as readable and considerably faster.
  • .animate() — CSS transitions or the Web Animations API. Most jQuery animations were opacity and transform anyway.
  • .html() and .text() — innerHTML and textContent. Use textContent for anything a user typed.
  • Plugins — look for a native element or a maintained library first: the dialog element for modals, input type="date" for pickers, details for accordions.

If you must write innerHTML with content from a form, a query string or a third-party API, sanitise it first. That risk existed in the jQuery version too, but a rewrite is a good time to fix it.

Build a test harness before you break anything

Write end-to-end tests against the current site first. Playwright or Cypress will do; pick whichever your team already argues about least. Cover the paths that cost money or trust: search, sign-up, checkout, password reset, the contact form. Run them against the jQuery version until they pass. That passing suite is your baseline, and it's what lets you move quickly without holding your breath.

What to cover

  • Critical user journeys, end to end, in a real browser.
  • The same journeys with JavaScript disabled, to confirm the page still functions. Playwright can turn scripting off per context — that's the fastest progressive-enhancement test you will ever write.
  • Unit tests for any logic you extract: price formatting, date handling, validation rules.
  • Visual snapshots for layout-heavy pages, accepting that fonts, animations and dates make them flaky if you're careless.

Then work in small changes. Replace one behaviour, run the suite, commit. When something breaks you'll know exactly which change caused it, which is the entire point of the exercise.

Roll out in slices and watch for errors

Ship each replacement behind a flag or to a small share of traffic, and watch your error reporting and Core Web Vitals for a few days. Removing jQuery can change timing in ways you didn't plan for: code that quietly relied on jQuery's ready ordering, or on a global "$", will throw the moment the library disappears. Keep the previous release easy to restore for at least one cycle.

A migration you can actually finish

Start with the component that has the most traffic and the least risk — a header menu, a filter, a newsletter form. Replace it, test it, ship it, then repeat. Keep a running list of remaining jQuery usage somewhere visible and watch the number shrink. When it reaches zero, delete the script tag and enjoy the smaller bundle.

The goal isn't to be modern for its own sake. It's to reach a front end where a new feature takes an afternoon rather than a week of negotiating with code from 2014.

Photo: Alicia Christin Gerald / Pexels

Related News
New Year Codebase Health Check: A January Checklist for Development Teams

A practical January checklist for development teams: audit dependencies, target test coverage, prune...

Why Your CSS Grid Layout Breaks on Mobile: Common Mistakes and Fixes

CSS Grid usually breaks on mobile because of sizing floors, not the grid itself. Here's why implicit...

A Developer's Checklist for GDPR-Compliant Logging

A practical checklist for keeping personal data out of application logs, setting sensible retention...