jQuery for one selector
I got my first dev job in August at a place that has about 80 active clients which means I have spent the last couple months opening other people’s work for the first time. It is the fastest education I have had, and most of it is not flattering to anybody (very much including me!)
Last week I worked on a site where jQuery was loaded on every page and used in only one place. Its sole purpose was to add a class to an element when a button was clicked.
~30 KB, gzipped, parsed and executed on every page load, just to avoid typing classList.toggle.
I’m not writing this to dunk because I have done exactly this on my own projects. That’s the pattern. jQuery goes into the boilerplate on day one because it’s always in the boilerplate, and then the project turns out not to need much JavaScript, and nobody goes back and checks.
Earning its place in the boilerplate
It’s worth being clear-eyed about this because there’s a strain of jQuery bashing that’s just ahistorical.
In 2006 the DOM API was genuinely awful and, worse, it was differently awful in each browser. getElementById existed but selecting by class meant walking the tree yourself. Attaching an event was addEventListener in most browsers and attachEvent in IE, with a different this and a different event object. Ajax was XMLHttpRequest or an ActiveX object depending. Animations meant setInterval and doing some math.
jQuery made all of that one consistent thing that worked everywhere, and it did it so well that its selector engine got standardized into the platform. That’s about the highest compliment a library can receive: the browsers implemented it.
Which is also exactly why the case for it has weakened. It won.
The direct translations
The stuff I actually use jQuery for on a day-to-day basis on a WordPress site is a very short list and all of it is now native.
$('.item') document.querySelectorAll('.item')
$('#thing') document.querySelector('#thing')
$el.addClass('is-open') el.classList.add('is-open')
$el.toggleClass('is-open') el.classList.toggle('is-open')
$el.hasClass('is-open') el.classList.contains('is-open')
$el.on('click', fn) el.addEventListener('click', fn)
$el.attr('href') el.getAttribute('href')
$el.text('hi') el.textContent = 'hi'
$el.closest('.card') el.closest('.card')
$(document).ready(fn)
classList and closest and querySelectorAll are all supported back to IE9 or so, with closest being the newest and needing a small polyfill for older IE. Which for most of the projects here is fine now, though it wouldn’t have been when a lot of them were built.
The one that trips people up is that querySelectorAll returns a NodeList, not an array, so .forEach on it is not reliable everywhere yet. You end up doing Array.prototype.slice.call(...) or the spread operator if you’re transpiling, which is clumsier than $('.item').each(), and I’ll concede that one.
Still has its uses
Ajax, mostly, if I’m supporting older browsers. $.ajax handles a pile of inconsistencies and gives you a promise-ish thing. There’s a native fetch now which is much nicer to use. But it’s new enough this year that it needs a polyfill almost everywhere, so you’re loading something either way.
Animation, if it’s anything beyond a simple state change. Though honestly most of what I used .animate() for is now a CSS transition and a class toggle, which performs better anyway because it can run on the compositor.
And to be clear: any project where jQuery is already loaded and already used throughout, which brings me to the actual point.
I’m not ripping it out
This is where I’d push back on my own post a bit.
Removing jQuery from an existing project that uses it properly is a rewrite of every interactive thing on the site in exchange for maybe 30 KB. That is a terrible trade. You’d be introducing risk across a codebase to save less than one of the images on the homepage.
I did the math on the site that started this.
jQuery: about 30 KB over the network The hero image: 190 KB after I’d already optimized it
There is no version of this where the library is the thing to attack first. I’d been about to make the case for it because the library felt like my problem and the image felt like somebody else’s.
Trimming the boilerplate
I’ve actually changed what goes into a new project on day one, and that’s nothing.
I start with no JavaScript at all. Then, when I need something, I write it in vanilla JS first, and if the vanilla version is painful enough for the browsers in scope then I’ll pull in a library and at least then I’ll know why. On the last three projects that decision came out as “no jQuery” twice and “yes, for the Ajax” once, and in all of those cases it was a decision instead of an inheritance.
Which is the part that generalizes, I think. The problem was never jQuery. It’s that the boilerplate accumulates things that solved problems that no longer exist, and nothing in the process ever prompts you to re-examine an assumption that isn’t currently hurting.