A Year Without jQuery

Dropping the trusted workhorse from our front-end back in 2014 has led to a faster, leaner platform

March 1st 2016

I joined Colony back in Summer 2014. Six months into the job, we came to a point in our product development requiring the addition of several large features, and the rethinking of some key pieces of our platform design.

Faced by the decision of hacking on top of the code I had inherited when I started, or starting from scratch, I took the decision of going for the latter, which presented the opportunity to make some big changes to the front-end stack and its dependencies - one of which was to drop jQuery, which we did in late 2014.

While I had already completed a few smaller projects using only “vanilla” JavaScript, this was the first large-scale UI-heavy application I had considered building without jQuery. As someone who cut my teeth on jQuery and authored numerous plugins for the ubiquitous library, I’d now come to a point (like many other developers I’d talked to) where I felt a little bit guilty every time I called the fabled $() function. For a long time, I had already been looking for opportunities to use vanilla JS over jQuery wherever I could do so safely in all browsers. I now felt that the time had come both in my personal development, and also in the landscape of the wider front-end world, to say goodbye to my old friend altogether.

18 months later, the lessons I’ve learned from the process of building a UI without jQuery have been extremely valuable, and I hope to share a few of them in this article. What prompted me to write this however, was actually a talk I attended recently at front-end London, entitled How not to use jQuery. While it was a great and informative talk, it highlighted a misconception that I’ve come across recently from several people - that ES6 will save us from jQuery (right after it cures cancer and ends world poverty). I also remember talking to a developer friend recently who told me that his team were looking forward to moving away from jQuery "just as soon as ES6 is more widely supported".

It highlighted a misconception that I’ve come across recently … that ES6 will save us from jQuery

I don’t completely understand where this idea comes from, and hopefully it’s not widespread, but I thought it worth addressing anyway. In my mind, ES6 is for the most part a much-needed syntactical progression of the JavaScript language and jQuery is for the most part, a DOM manipulation library with a beautifully designed API. These two things actually have very little in common so I wanted to write this article primarily to prove that you can stop using jQuery today - and you don’t need ES6 or Babel to do it.

So why drop jQuery at all you may ask? Firstly, application overhead and load time (especially on slower devices and connections); secondly UI performance and responsiveness (again particularly on slower devices); and lastly, the removal of unnecessary abstraction — giving you the opportunity to better understand the DOM, the browser and its APIs.

If there was one thing holding all of us back from dropping jQuery it was arguably having to support IE8, but I hope we can agree those days are now soundly behind us (and you have my sympathies if that’s not the case). Missing from IE8 were the browser DOM APIs which could have saved us from jQuery; things such as Element.querySelectorAll(), Element.matches(), Element.nextElementSibling, and Element.addEventListener() — things which now exist consistently across all browsers.

While IE9 and up (including the latest version of Edge) still have problems, they are more or less consistent in terms of what I would consider the “essential” DOM APIs (with the exception of Element.classList in IE9 unfortunately) needed to write a UI-heavy application without jQuery and without the overhead of numerous polyfills and libraries.

There’s no denying however, that jQuery also comes packed with a bunch of useful utility functions, as well as tools for things like Ajax and animation, which is where things get interesting in terms of deciding what and what not to to include in your front-end toolkit.

Helper Functions

I found that as far as utility and helper functions, dropping jQuery provided a great opportunity to write a few helper functions of my own and learn a bit more about browsers and the DOM in the process, which was in some ways the most valuable part of the process for me. This static class of helper methods (which i called “h”) covered basic things like querying child or parent elements, extending objects, and even Ajax, as well as lots of things unrelated to the DOM.

This might sound like attempting to rewrite jQuery, but that was absolutely not the goal. This small collection of convenience helper methods equates to just a tiny fraction of jQuery’s overall functionality, without wrapping Elements or providing any additional abstraction. The native browser APIs mentioned above are what really enable us to interact with the DOM without jQuery, with these functions filling some small gaps that existed when I embarked on the project.

Following are a few of those helper functions which I found myself needing, and which I also found to be interesting and educational to write. I’m not including these just so that anyone reading can copy and paste them in their project - you may not even have a need for them - but rather to illustrate how easily we can arrive at solutions to common DOM traversal problems, with the above APIs at our disposal.

Returns the index of a given element in relation to its siblings, optionally restricting siblings to those matching a provided selector

Since writing these in 2014, I’ve since learned that h.closestParent() now has a native equivalent in the form of Element.closest(), and h.children() in the form of ':scope' psuedo-class allowing us to reference the element itself in queries (e.g. .querySelectorAll(':scope > .child'). While both of these features are fairly new and not universally supported yet, it’s exciting to see how fast browser APIs are catching up (often following jQuery’s influence), and I’m excited to refactor both of these helpers out of our application very soon.

It’s worth noting that one function I’ve omitted from this list due to its length and complexity is h.extend() which I use frequently to extend, merge and clone objects (analogous to jQuery’s $.extend). We don’t use any additional utility libraries such as Underscore or Lodash, so a home-baked extend helper was critical for our application. There are numerous Stack Overflow posts explaining how to achieve such functionality, but I still find myself tinkering with this function regularly as features are added with increasingly complex needs (e.g. the copying of getters and setters, and deep cloning of nested objects and arrays).

Over the last few years, one resource I’ve always found particularly helpful in my efforts to use vanilla JavaScript is the excellent You Might Not Need jQuery.

Loops

One thing that I do miss from jQuery however, is the array-like nature of its collection objects which makes performing operations on multiple elements effortless. Without jQuery, you will find yourself relying heavily on loops to achieve the same functionality. Rest assured however, that this is where you will gain the most performance benefits — as I learned a long time ago when trying to optimise execution time in MixItUp. Those expensive internal $.each function calls made by jQuery when methods are called on a collection, can be sacrificed in favour of lean native loops without any function invocation.

jQuery

var $items = $container.children('.item');
$items.hide();

Vanilla JavaScript

Event Handling and Delegation

Similarly, when dealing with event delegation, jQuery provides some niceties under the hood in terms of returning the bound element rather than the event target, requiring a little extra code in vanilla JavaScript (both examples for illustration only):

Without jQuery, we soon find out the clicked element may not necessarily be what we’re expecting

In the second example, the clicked element or event “target” (e.target), could be the button, an element nested inside the button, or a completely unrelated element. A closestParent() helper function (see above) becomes invaluable in these sort of situations.

Necessary Abstraction

As you can see, the brevity afforded by jQuery is often sacrificed when moving to vanilla JavaScript, but it doesn’t have to be this way. Just as jQuery abstracts various verbose and repetitive pieces of functionality away in a simple API, we can do the same, albeit with APIs more tailored to our application.

The drawback of an extremely versatile API like jQuery’s is the accompanying overhead needed in order for it to function in every possible situation. For example allowing arguments to be passed to methods in any order, or not at all and fail silently. When we know the limitations of our application, we can write more efficient abstractions without this sort of overhead. As seen in the above example however, event binding and handling is easy enough without jQuery, but it’s not necessarily beautiful.

The low-level nature of working with the DOM directly is more likely to inspire efficiency in other parts of your code.

Of course there’s nothing stopping you from using jQuery and also writing great APIs for your application, but the low-level nature of working with the DOM directly is more likely to inspire efficiency in other parts of your code. Take for example, our solution for reusable UI components:

UI Component Example

In our application, every distinct UI component is expressed as a discreet class which we call a “behavior” (an approach originally shown to me almost two years ago by fellow Colony developer Sam Loomes, and one that we continue to use today). We strongly believed, and still do, in the concept of “unobtrusive” JavaScript. So while we loved the idea of discreet, self-contained components, the blurring of HTML and JavaScript in Angular’s templates for example (and now React’s JSX), didn’t feel quite right and was therefore something we wanted to avoid. We also realised that trying to retrofit an opinionated framework on to our unique platform architecture would result in significant hacking and a fair amount of redundancy in the framework.

We therefore decided to build our own solution, with the principle that JavaScript UI code should not be tightly coupled to specific markup. Our UI behaviors effectively “progressively enhance” arbitrary pieces of markup, which could be anything as long as they contains the essential “key” DOM elements defined in the behavior.

To declare these behaviors, I designed a function, Behavior.extend(), with a simple public interface to extend a “base” behavior prototype and abstract away the monotony of things like prototypal inheritance, element reference caching, and event binding whenever a behavior is found in the DOM and instantiated.

When our app starts up, we crawl the DOM for any elements marked with a data-behavior attribute using querySelectorAll(). When an element’s behavior is instantiated, references to any elements needed are automatically cached, and any events are automatically bound. Take for example the following piece of HTML:

The data-ref attribute indicates that a reference to that element is to be automatically cached by the enclosing behavior by using a localised querySelector() call on the “dash-cased” version of that property name. For example, the element with attribute data-ref="button-prev" is referenced as this.dom.buttonPrev in our JavaScript, with the root element assigned to this.dom.context (a property inherited from the base behavior).

When a DOM reference is defined with an array default (as slides is in the example), the behavior knows to cache a NodeList rather than a single element by using the singular version of the property name (slide) and querySelectorAll().

Kudos to Zone developer Mike Simmonds for the idea of using data-ref to query nested elements, rather than class names — again keeping styling and functionality nicely separate.

Additionally, using prototypal inheritance under the hood, we can easily extend our behaviors with additional properties and methods, using the same syntax:

This became extremely useful for things like form fields where would create a base “input” behavior containing methods for things like validation, then extend it into distinct classes for specific field types with different UI (for example, a radio button group or a text input).

When we redraw a section of the DOM, (e.g. during an application state change of some kind), any behaviors contained are “destroyed” for efficient garbage collection by calling a method to remove their references and unbind their events.

Again, the lack of something like jQuery’s .off() method with its optional namespaces, lead us to a more hands-off solution, where we barely have to think about event binding at all. jQuery already gives us an powerful and expressive syntax (albeit non-standard), but it doesn’t (and shouldn’t) solve the larger problem of event binding as a whole. In the context of a specific application however, if something can be completely automated, then it probably should be.

Our UI component approach is just one example of how dropping jQuery doesn’t necessarily mean having to deal with tedious DOM manipulation and low-level APIs, and moreover, inspired us to be creative in the pursuit of DRYness. By writing only the abstractions needed for your application, you can keep both overhead and code repetition to a minimum.

Syntax

Regarding syntax and JavaScript itself, we haven’t felt the need to move to ES6 and a transpiler yet. In the interest of performance, I tend to prefer writing code that’s as close to the production code as possible, so the added abstraction of Babel or Traceur is something I’d rather avoid until native ES6 support is more widespread. Moreover, I feel that there are some great features in ES5 that deserve wider attention — particularly getters and setters, and static Object methods like Object.seal(), and Object.freeze(). There are all sorts of great uses for these features, but I find them most useful in enforcing stricter and safer data structures on constructors.

For example, in my previous example of our UI "behaviors", we call Object.seal() on both the State and Dom constructors behind the scenes to ensure that all properties must be defined in the constructor. This also helps us to catch things like property name typos during development.

With IE9 and up, almost all ES5 features are available natively, so there’s no reason not to take advantage of them today.

While ES6 is a huge step forward for JavaScript and is transforming it into a grown-up and respectable programming language, its current lack of support should not be what’s holding you back from dropping jQuery.

A Place for jQuery?

While I had the luxury of being able to devote all of our resources to a single product over a large timespan, the average agency or freelance client project won’t have a budget for such a high degree of experimentation. jQuery still enables developers to write extremely robust code quickly and succinctly, and the average website needs nothing more than this (particularly when development time is restricted).

On top of that, jQuery’s API design should forever remain an inspiration to us all. Its simplicity and flexibility is something that we should all strive for in software design, which is in no small part responsible for its triumph over other libraries such as Mootools and YUI back in the day, as well as enabling countless rookie developers (myself included) to get into JavaScript. As John Resig recently stated in his post marking the 10th anniversary of jQuery

It pleases me that there is apparently still a place for simple API design in the world

John Resig

And I think that’s one lesson we can all take away from jQuery, whether it continues to be used or not. In many ways, moving to vanilla JavaScript highlights the ugliness of working with the DOM directly, and the shortcomings of native Element object — shortcomings which Resig solved so incredibly elequently with the jQuery API.

Having said that, the lessons I’ve learned over the last year have made me a better developer, and the tools built in the process have opened my eyes and given me enough confidence and understanding of vanilla JavaScript that the only scenario where I would personally consider using jQuery again would be a project needing IE7 support. This isn’t a criticism of jQuery at all, just a sign that our technology and browsers have come along way since the fragmented, non-standard world of jQuery v1.0.0.

So if you’re working on a project with some freedom for experimentation, and one which doesn’t need archaic browser support, I strongly recommend you take the leap and say goodbye to jQuery today. You’ll create a lighter, faster application and learn a considerable amount in the process.