jQuery solved real browser compatibility problems for over a decade. In 2008, when this article was first published, writing cross-browser JavaScript without jQuery was genuinely painful. Internet Explorer 6 handled events differently, CSS selectors were inconsistent, and AJAX required browser-specific XMLHttpRequest implementations.
Learn more about WordPress speed optimization at WPPoland.
In 2026, every problem jQuery solved is now handled natively by browsers. The question is no longer whether to migrate, but how to do it safely without breaking existing functionality.
Why jQuery is technical debt in 2026
jQuery 3.7 weighs 87KB uncompressed (30KB gzipped). That may sound small, but consider what it costs:
- Total Blocking Time (TBT): jQuery must parse and execute before any dependent code runs. On mid-range mobile devices, this adds 150-300ms to TBT.
- Interaction to Next Paint (INP): jQuery’s event delegation system adds overhead to every user interaction, measurably worsening INP scores.
- Dependency chain: Loading jQuery means every script that depends on it must wait, creating a waterfall of blocking resources.
- Redundant code: Every jQuery method you call has a native equivalent that the browser already ships. You are paying twice for the same functionality.
Performance benchmarks: jQuery vs vanilla JS
Real-world measurements on a WordPress theme with typical interactions (menu toggle, tab switching, form validation, AJAX load-more):
| Metric | With jQuery | Without jQuery | Improvement |
|---|---|---|---|
| Total JS size | 142KB | 55KB | -61% |
| TBT (mobile) | 480ms | 180ms | -62% |
| INP (p75) | 220ms | 95ms | -57% |
| LCP | 2.1s | 1.7s | -19% |
| Lighthouse Performance | 72 | 94 | +22 points |
These numbers come from a production WordPress site running GeneratePress with WooCommerce, tested on a Moto G Power (a representative mid-range device).
Modern JavaScript (ES2024+) replaces every jQuery pattern
The ES2024 specification, fully supported in Chrome 124+, Firefox 126+, Safari 17.4+, and Edge 124+, provides native alternatives for every common jQuery pattern.
DOM selection
// jQuery
const $buttons = $('.btn');
const $container = $('#main-container');
const $firstItem = $('.menu-item:first');
// Vanilla JS (ES2024+)
const buttons = document.querySelectorAll('.btn');
const container = document.getElementById('main-container');
const firstItem = document.querySelector('.menu-item');
// Scoped selection (like jQuery .find())
const navLinks = container.querySelectorAll('a.nav-link');
Key difference: querySelectorAll returns a static NodeList, not a live collection. This is actually safer because the list does not change unexpectedly when the DOM mutates.
Event handling
// jQuery
$('.btn').click(function () {
$(this).toggleClass('active');
});
$('.menu').on('click', '.menu-item', function () {
// delegated event
});
// Vanilla JS
document.querySelectorAll('.btn').forEach(btn => {
btn.addEventListener('click', () => {
btn.classList.toggle('active');
});
});
// Event delegation (replaces .on() with selector)
document.querySelector('.menu').addEventListener('click', (e) => {
const item = e.target.closest('.menu-item');
if (item) {
// handle menu item click
}
});
The closest() method is the modern equivalent of jQuery’s delegated event matching. It traverses up the DOM tree to find the nearest ancestor matching a selector.
Class manipulation
// jQuery
$el.addClass('active');
$el.removeClass('hidden');
$el.toggleClass('open');
$el.hasClass('visible');
// Vanilla JS
el.classList.add('active');
el.classList.remove('hidden');
el.classList.toggle('open');
el.classList.contains('visible');
// Multiple classes at once
el.classList.add('active', 'highlighted', 'animate-in');
el.classList.remove('hidden', 'collapsed');
AJAX with fetch API and async/await
// jQuery
$.ajax({
url: '/wp-json/wp/v2/posts',
method: 'GET',
data: { per_page: 5 },
success: function (posts) { renderPosts(posts); },
error: function (xhr) { console.error(xhr); }
});
// Vanilla JS (modern async/await)
async function loadPosts() {
try {
const response = await fetch('/wp-json/wp/v2/posts?per_page=5');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const posts = await response.json();
renderPosts(posts);
} catch (error) {
console.error('Failed to load posts:', error);
}
}
// POST with nonce (WordPress pattern)
async function submitForm(data) {
const response = await fetch('/wp-json/custom/v1/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': wpApiSettings.nonce,
},
body: JSON.stringify(data),
});
return response.json();
}
Animations without jQuery
jQuery’s .fadeIn(), .slideDown(), and .animate() can all be replaced with CSS transitions, CSS animations, or the Web Animations API.
// jQuery
$('.panel').slideDown(300);
$('.modal').fadeIn(200);
// CSS approach (preferred for performance)
// In CSS:
// .panel { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
// .panel.open { max-height: 500px; }
// In JS:
panel.classList.add('open');
// Web Animations API (for complex, programmatic animations)
modal.animate(
[
{ opacity: 0, transform: 'scale(0.95)' },
{ opacity: 1, transform: 'scale(1)' },
],
{ duration: 200, easing: 'ease-out', fill: 'forwards' }
);
The Web Animations API runs on the compositor thread, meaning animations do not block the main thread. jQuery animations run on the main thread and cause jank on slower devices.
DOM manipulation
// jQuery
$('<div class="notice">Hello</div>').appendTo('#container');
$('.old-element').replaceWith('<span>New</span>');
$('.item').remove();
$('.list').empty();
// Vanilla JS
const notice = document.createElement('div');
notice.className = 'notice';
notice.textContent = 'Hello';
container.append(notice);
// Or use insertAdjacentHTML for HTML strings
container.insertAdjacentHTML('beforeend', '<div class="notice">Hello</div>');
// Replace
oldElement.replaceWith(Object.assign(document.createElement('span'), { textContent: 'New' }));
// Remove
item.remove();
// Empty
list.replaceChildren();
Document ready
// jQuery
$(document).ready(function () { /* ... */ });
$(function () { /* shorthand */ });
// Vanilla JS
document.addEventListener('DOMContentLoaded', () => {
// DOM is ready
});
// Or simply place your <script> tag with type="module" at the end of <body>
// Modules are deferred by default, so the DOM is already ready
Web Components: the modern jQuery plugin replacement
jQuery plugins provided reusable UI components (sliders, modals, tabs, accordions). In 2026, Web Components offer a standards-based alternative with better encapsulation.
Example: a toggle panel component
class TogglePanel extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
const title = this.getAttribute('title') || 'Toggle';
this.shadowRoot.innerHTML = `
<style>
:host { display: block; margin: 1rem 0; }
button {
width: 100%; padding: 0.75rem 1rem;
background: #f5f5f5; border: 1px solid #ddd;
cursor: pointer; text-align: left;
font-size: 1rem; font-weight: 600;
}
.content {
display: none; padding: 1rem;
border: 1px solid #ddd; border-top: none;
}
:host([open]) .content { display: block; }
</style>
<button part="trigger">${title}</button>
<div class="content"><slot></slot></div>
`;
this.shadowRoot.querySelector('button').addEventListener('click', () => {
this.toggleAttribute('open');
});
}
}
customElements.define('toggle-panel', TogglePanel);
Usage in HTML:
<toggle-panel title="Shipping information">
<p>Free shipping on orders over $50.</p>
</toggle-panel>
Web Components provide Shadow DOM encapsulation (styles do not leak), slots for content projection, and lifecycle callbacks. They work in every modern browser without polyfills.
When to use Web Components vs a framework
| Scenario | Recommendation |
|---|---|
| Simple interactive widget (accordion, tabs, modal) | Web Component |
| Full SPA (single page application) | React / Vue / Svelte |
| WordPress block (Gutenberg) | React (WordPress standard) |
| Shared component across multiple sites | Web Component |
| Complex state management | Framework with state library |
Migration strategy for WordPress projects
Step 1: Audit your jQuery usage
Run this command in your theme directory to find all jQuery references:
grep -rn '\$(\|jQuery\.\|jQuery(' --include='*.js' --include='*.php' .
Categorize each usage:
- Your code (theme/custom plugin): migrate this
- Third-party plugin: leave this, the plugin manages its own dependencies
- WordPress admin: do not touch, WordPress core handles this
Step 2: Create a migration plan
Prioritize by impact:
- Front-end theme code (affects every visitor) - migrate first
- Custom plugin front-end - migrate second
- Admin-side customizations - migrate last (lower traffic)
Step 3: Replace patterns incrementally
Do not rewrite everything at once. Replace one file at a time:
- Remove
array('jquery')from the file’swp_enqueue_scriptdependency array - Replace all jQuery patterns with vanilla JS equivalents
- Test in Chrome, Firefox, Safari, and Edge
- Test with all active plugins enabled
- Run Lighthouse before and after to measure improvement
Step 4: Handle the WordPress AJAX pattern
WordPress legacy AJAX uses admin-ajax.php with jQuery:
// Old pattern (jQuery + admin-ajax)
jQuery.post(ajaxurl, {
action: 'my_custom_action',
nonce: myData.nonce,
post_id: 123,
}, function (response) {
console.log(response);
});
// Modern pattern (fetch + REST API)
async function myCustomAction(postId) {
const response = await fetch('/wp-json/myplugin/v1/action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': myData.nonce,
},
body: JSON.stringify({ post_id: postId }),
});
return response.json();
}
The REST API approach is faster (no admin-ajax.php overhead), more cacheable, and follows modern WordPress development standards.
Step 5: Enqueue scripts properly
// Before (with jQuery dependency)
wp_enqueue_script(
'my-theme-scripts',
get_template_directory_uri() . '/js/main.js',
array('jquery'),
'1.0.0',
true
);
// After (no jQuery, with module support)
wp_enqueue_script_module(
'my-theme-scripts',
get_template_directory_uri() . '/js/main.js',
array(),
'2.0.0'
);
WordPress 6.5+ supports wp_enqueue_script_module() which loads scripts as ES modules with type="module", enabling native import/export syntax.
When keeping jQuery still makes sense
jQuery may still be justified if:
- Legacy codebase with 50+ jQuery plugin dependencies: The migration cost exceeds the performance benefit. Plan a gradual phase-out over 6-12 months.
- WordPress admin customizations: The admin area already loads jQuery. Adding your own admin scripts with jQuery dependency costs nothing extra.
- Third-party plugin requirements: Some popular plugins (certain form builders, page builders) require jQuery. Do not fight the dependency if you cannot control it.
- Team skill gap: If your development team is not comfortable with modern JS, invest in training before forcing a migration. Understanding what a WordPress developer does helps set realistic expectations for the skills needed during migration.
The goal is pragmatic improvement, not ideological purity. Remove jQuery where it costs you performance and adds no value. Keep it where removing it would break things or cost more than it saves.
Common jQuery deprecation errors in WordPress and how to fix them
If you are debugging a WordPress site in 2026, you will almost certainly run into warnings from jquery-migrate. These are the ones that show up most often, along with the concrete fix.
JQMIGRATE: jQuery.fn.on() event shorthand is deprecated
Full warning text in the browser console:
JQMIGRATE: jQuery.fn.on() event shorthand is deprecated
This fires whenever a script uses an event-binding shortcut that the migrate shim has flagged for removal (the classic pattern is code that relies on the old .bind(), .live(), or an event name used as a method). The fix is to rewrite the handler with an explicit .on() call or plain addEventListener. For example $el.click(handler) becomes $el.on('click', handler), and $(document).on('click', '.selector', handler) is the modern delegated form. Once every caller uses the explicit form, you can deregister jquery-migrate from WordPress with wp_deregister_script('jquery-migrate') or let WordPress drop it automatically once no enqueued script declares it as a dependency.
JQMIGRATE: jQuery.fn.on is deprecated
A slightly different wording of the same family. It usually points to a plugin or theme that monkey-patches jQuery.fn.on, or to a build of jQuery Migrate that flags a specific signature you are calling. The fix is the same: audit the callers, replace shorthand event methods with explicit .on() or vanilla addEventListener, and remove the shim once the console is quiet.
jQuery is not defined (WP Rocket “Delay JavaScript Execution”)
Error text:
Uncaught ReferenceError: jQuery is not defined
Most common cause in WordPress: WP Rocket (or a similar optimizer) delays JavaScript execution, so jQuery has not loaded yet when an inline script tries to use it. Three ways to fix it:
- Add the specific script that triggers the error to WP Rocket’s “Excluded JavaScript Files” list, so it is not delayed.
- Exclude
jquery-coreandjquery-migratefrom the delay list entirely (WP Rocket has presets for this). - Better long term: refactor the inline script to pure vanilla JS. Once nothing depends on
window.jQuery, the race condition disappears and you can keep delay-JS turned on for everything.
jQuery functions are deprecated and no longer supported in Slider Revolution 6.5+
This is a theme/plugin compatibility warning, not a WordPress core issue. Slider Revolution 6.5 removed support for a set of legacy jQuery helper functions that older add-on templates depend on. The fix is to update the Slider Revolution plugin and every third-party template to the current version. If you maintain a custom template for Slider Revolution, replace the deprecated helpers with the current API documented in the plugin’s changelog. If the author of the template is no longer maintaining it, rebuild the slider using native CSS animations or a lightweight vanilla JS carousel. That is usually the better long-term path anyway.
jquery-ui-slide.js / node/minify.js build errors
Seen in Etherpad-style WordPress builds and older gulp/npm pipelines. It usually means your build chain is trying to minify a jQuery UI file that is already minified or is using a minifier that does not understand a newer syntax in the bundle. The fix is to pin jQuery UI to the version your build supports, or to switch the minifier to terser (which handles ES2020+ without tripping on modern syntax). If you control the pipeline, the cleaner move is to drop jQuery UI entirely and replace the component (slider, datepicker, autocomplete) with a native HTML input or a small vanilla JS library.
Use latest jquery/jquery-ui, lower required moment version
This appears in older WordPress admin pages and composer scripts that pin versions. Update jquery and jquery-ui to the versions WordPress core currently bundles (check wp-includes/script-loader.php), and downgrade moment only if an admin screen specifically depends on an older API. For new code, replace moment with Temporal or with Intl.DateTimeFormat, which are built into the browser and much lighter.
If the warnings persist after these fixes, the usual cause is a single stubborn plugin that still ships its own jQuery calls. The fastest way to find it is to disable plugins one by one with the console open. Once the noise is gone, you can also remove jquery-migrate from the page entirely, which shaves around 13KB of JavaScript from every request.
ES2024+ features that replace common jQuery utilities
Structured clone (deep copy)
// jQuery
const copy = $.extend(true, {}, original);
// ES2024+
const copy = structuredClone(original);
Array-like iteration
// jQuery
$.each(items, function (index, item) { /* ... */ });
// ES2024+
items.forEach((item, index) => { /* ... */ });
// Or with Array.from for NodeLists
Array.from(document.querySelectorAll('.item')).map(item => item.textContent);
// Or spread operator
[...document.querySelectorAll('.item')].filter(item => item.dataset.active);
Deferred/Promise patterns
// jQuery
const deferred = $.Deferred();
deferred.resolve('done');
deferred.promise().then(val => console.log(val));
// ES2024+
const promise = new Promise((resolve) => resolve('done'));
promise.then(val => console.log(val));
// Promise.withResolvers() - ES2024 feature
const { promise, resolve, reject } = Promise.withResolvers();
IntersectionObserver (replaces jQuery scroll handlers)
// jQuery (expensive scroll handler)
$(window).scroll(function () {
$('.lazy-image').each(function () {
if ($(this).offset().top < $(window).scrollTop() + $(window).height()) {
$(this).attr('src', $(this).data('src'));
}
});
});
// Vanilla JS (performant, off-main-thread)
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('.lazy-image').forEach(img => observer.observe(img));
Utility library alternatives (if you need a helper)
If you find yourself writing the same vanilla JS patterns repeatedly, consider a micro-library instead of jQuery:
| Library | Size | Purpose |
|---|---|---|
| Alpine.js | 15KB | Declarative reactivity (x-data, x-on) |
| htmx | 14KB | AJAX, WebSocket, SSE via HTML attributes |
| Petite-Vue | 6KB | Vue-compatible template syntax |
| None (vanilla) | 0KB | Best performance, full control |
For WordPress themes in 2026, the recommendation is vanilla JS for simple interactions and Alpine.js or htmx if you need declarative behavior without a full framework.
jQuery best practices in 2026
If you still maintain jQuery code — whether by choice or because a legacy codebase demands it — these practices minimize the damage:
- Load jQuery from WordPress core, never from a CDN. WordPress bundles a compatible version. Loading a second copy from
cdnjsorcode.jquery.commeans double the weight and potential version conflicts. - Use
jQueryinstead of$in WordPress. WordPress runs jQuery in no-conflict mode. Wrapping code injQuery(function($) { ... })prevents collisions with other libraries. - Avoid
.ready()nesting. A singlejQuery(function($) { ... })is enough. Nested.ready()calls create callback pyramids and confuse execution order. - Cache selectors. Every
$('.my-class')call traverses the DOM. Store results in a variable when you use the same selector more than once. - Delegate events instead of binding to individual elements. Use
$(parent).on('click', '.child', handler)instead of$('.child').click(handler). This handles dynamically added elements and uses fewer event listeners. - Do not use jQuery for CSS animations. Use CSS transitions or the Web Animations API. jQuery’s
.animate()runs on the main thread and causes jank. - Set
jqueryas a dependency only when needed. If a script file does not use jQuery, remove it from thewp_enqueue_scriptdependency array to avoid loading jQuery unnecessarily.
jQuery latest version and what changed
As of 2026, jQuery 3.7.1 is the latest stable release (shipped August 2023). jQuery 4.0.0-beta.2 has been in beta since February 2024, with no stable release date announced.
jQuery 4.0 changes that matter for WordPress:
| Change | Impact |
|---|---|
| Drops IE 11 support | No effect — WordPress 6.6+ already dropped IE 11 |
Removes deprecated APIs (.click(), .bind(), .delegate()) | Plugins using these will break |
| Smaller bundle (~68KB vs 87KB) | Modest improvement, still heavier than no jQuery |
FormData-based $.ajax() for file uploads | Nicer API, but fetch() does this natively |
WordPress core currently ships jQuery 3.7.1 and has not committed to bundling 4.0. The practical takeaway: do not wait for jQuery 4 to improve performance. Migrate to vanilla JS where possible, and use jQuery 3.7.1 best practices where migration is not yet feasible.
Who still uses jQuery in 2026
jQuery remains loaded on an estimated 77% of all websites (per W3Techs), largely because WordPress, Shopify, and legacy enterprise sites include it by default. But “loaded” does not mean “needed.”
The breakdown:
- WordPress sites: jQuery loads on virtually every WordPress page because the admin bar and many popular plugins depend on it. On the frontend, the actual dependency is often just one or two scripts.
- Shopify themes: Most Shopify themes bundle jQuery for cart interactions. Shopify has not moved to deprecate it.
- Enterprise legacy systems: Banks, government portals, and large e-commerce platforms often have jQuery embedded in codebases dating back to 2010-2015. Migration is expensive and low-priority.
- New projects: Almost none. React, Vue, Svelte, and vanilla JS dominate greenfield development. No modern framework or starter template includes jQuery.
If you are starting a new WordPress theme or plugin in 2026, there is no reason to add jQuery as a dependency. The browser APIs are sufficient for every common pattern. For more on building plugins the right way, see the WordPress plugin best practices section in our plugin stack guide.
Conclusion: the migration checklist
- Audit all jQuery usage in your theme and custom plugins
- Measure current Core Web Vitals as a baseline
- Replace front-end theme code first (highest visitor impact)
- Use
querySelector,addEventListener,fetch,classList, and Web Animations API - Consider Web Components for reusable UI elements
- Use
wp_enqueue_script_module()for ES module support - Test in all major browsers after each file migration
- Measure Core Web Vitals again and document the improvement
- Keep jQuery only for admin scripts and third-party plugin dependencies
- Train your team on modern JavaScript patterns
The web platform in 2026 provides everything jQuery offered, and more. Every kilobyte of unnecessary JavaScript you remove makes your WordPress site faster, more accessible, and easier to maintain.
If you need help auditing a WordPress codebase, replacing legacy jQuery patterns, or hardening the site after removing outdated scripts, our WordPress developer team and WordPress security audit service can plan and execute the migration with you.

