A Year Without jQuery
Dropping the trusted workhorse from our front-end back in 2014 has led to a faster, leaner platform
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.
$() 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
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.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.
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.
Since writing these in 2014, I’ve since learned that
h.closestParent() now has a native equivalent in the form of
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).
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.
Event Handling and Delegation
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.
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
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.
A typical behavior definition in our application looks something like this:
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:
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.root (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 (
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.
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
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.
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).
It pleases me that there is apparently still a place for simple API design in the worldJohn Resig
Element object — shortcomings which Resig solved so incredibly elequently with the jQuery API.
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.