jQuery, You’re Great But…It’s Just Not Going to Work Out
How I refactored jQuery out of my plugin
Project Background
When I first began my journey into the wonderful (yet sometimes wacky) world of web-development a few years ago, I remember being faced with a number of questions.
- How do I select DOM elements in JavaScript?
- How do I easily bind event handlers to these elements?
- How can I remove and apply class tags?
These questions led me to a single solution…jQuery!
With its dead-simple interface and vast documentation, how could I not use this incredibly convenient tool? And so began a long lasting relationship with the library.
Selecting an element?
$(‘#myId’)
Applying a click handler?
$(‘#myId’).on(‘click’, function() {…})
Need a class tag?
$(‘#myId’).addClass(‘my-new-class’)
“It just cannot be this easy!” I thought. App after app, webpage after webpage, I would find reasons to harness this seemingly-perfect tool.
As my knowledge of best practices and native JavaScript APIs grew overtime, I began to realize that it was time to end this unhealthy relationship. Why, you ask, after years of happy matrimony? Well, for a number of reasons:
- jQuery was just getting way too big! The library has grown dramatically over time and even in its minified version is actually quite large (81.7 KB as of version 2.1.0).
- When I stepped back and took a look at apps I was creating, I realized that I only utilized a small subset of the jQuery functionality (selecting elements, toggling class tags, binding event handlers). Why load in the entire library when all I needed it for was to select some elements?
- Messy code! The convenience of JQuery can easily lead to poorly written code and bad practices such as very long and obscure selectors.
I decided to identify a jQuery-based project that I could refactor into a non-jQuery version. What better project than bootstrap-slider, the custom slider component for Twitter Bootstrap that I help co-manage/maintain with my open-source friend seiyria? The plugin allows you create simple UI sliders such as the following:
Process
I first began by identifying where in my codebase I was using jQuery methods. As you can see from this Github post, I compiled together a fairly lengthy list of jQuery methods my code was dependent on.
Next, I wrote alternate implementations of these methods in vanilla JS. Some of the methods were relatively easy to rewrite (such as $.data() for binding HTML5 data attributes). Others proved to be much more difficult, such as $.removeClass(), which involved a fair amount of regular-expressions.
I decided to attach these alternate implementations directly to the prototype of the Slider function (used as the constructor for an instance of Slider) and designated them with a _functionName like syntax so that I could avoid always passing the this context reference to call() or apply() anytime I wanted to invoke them.
Line by line, section by section, I meticulously went through and refactored the code. After a few quick bug fixes, I had a version of the plugin that did not rely on jQuery!
Pros
Cleaner Interface
One aspect of the new slider I really enjoy is the significantly cleaner interface! For comparison sake, lets take a look at how we interface with the slider:
$(“#mySliderElem”).slider(); // Instantiation with jQuery$(“#mySliderElem”).slider(‘getValue’) // Calling methodvar slider = new Slider(“#mySliderElem”) // No jQueryslider.getValue() // Calling method w/o jQuery
Instead of always having to call the the slider() method on the jQuery wrapped element reference, I can just create an instance of the slider, and then interact directly with the returned object!
But wait! What if I need to support existing jQuery users of the plugin, you ask? Does this mean that the versions with and without jQuery must become separate projects?
Thankfully not! I was fortunate to find the jQuery-bridget project by David DeSandro that allows me to easily adapt the Slider interface to still support the original jQuery interface!
All I have to do is include a copy of the bridget code within the slider code, and then detect whether jQuery is present in the browser (by checking if the $ object is defined). If it is, then I can go ahead and call bridget(), which will proxy any calls to $.slider(), and delegate to the appropriate Slider instance. Users can then choose which interface they want to interact with! Super cool!
Less Code to Load
One of the major reasons I initially took on this project was to have the Slider be operational in web-pages that otherwise did not rely on jQuery.
I ran some quick tests to see how the file load time had improved. For these tests, I assumed the code would be run in a production environment, and that all files would be minified. Take a look at the results:
With jQuery:
81.7 KB for JQ + 13.8 KB for bootstrap-slider = 95.5 KB
Without jQuery:
17.8 KB for bootstrap-slider = 17.8 KB
Difference:
95.5 KB (w/ JQ) — 17.8 KB (w/o JQ) = 77.7 KB savings
Better Performance
Going into the project, I had a hunch that certain native implementations of the methods would perform better than their jQuery counterparts, and I was correct. For example, the refactored version of the $.offset() method is far more performant.
Check out the following performance test:
Note: The bars represent operations per second (higher is better)
Cons
Performance Decline for Certain Methods
Not everything is without a bad side! Some native implementations did not perform as well as the corresponding jQuery versions.
For instance, for element selection, the refactored code relies heavily on document.querySelector(), which allows you to select elements in a similar fashion to jQuery’s Sizzle syntax. I thought that the native method would perform better then element selection via jQuery, when in fact, it is actually worse!
Take a quick glance at the results from this performance test:
Note: The bars represent operations per second (higher is better)
Thankfully, this is not very noticeable on most modern browsers, especially if you cache the references to these elements after selecting them.
Update: In hindsight I realize that I could have used getElementsByClassName(), which has pretty good support in modern browsers and better performance than querySelector().
Larger Code Base
One downside of the refactoring process was that the codebase grew substantially. As this growth occurred, I tried my best to maintain best coding practices and keep everything as clean and organized as possible. I now use large comment blocks to designate different sections of the codebase.
Debugging!
After the initial refactor, there was still some extensive debugging to do, which turned out to be a real pain! Fortunately, the project has a very robust test suite which made things much easier.
The test suite provided validation for refactoring so I could ensure these changes did not break any existing functionality. I did make some significant implementation changes that will still require minor changes to the some of the tests, but I do not anticipate this taking too much time.
Random aside: I came to discover that PhantomJS currently does not have support for the native JavaScript bind() method. I had to load a polyfill for it into my test runner. See this link for more info.
And as always, Chrome Developer Tools proved to be highly useful and reliable!
Conclusion
While jQuery provides a ton of useful features and functionality, it should be used lightly and only when really needed. In this case, I decided that it was best to part ways with the library, yet still provide fallbacks to support users still planning to use it as an interface for the Slider plugin.
Although the pros may not overwhelmingly outweigh the cons, the fact of the matter is, why load something that is only needed to support a single plugin? It was fun while it lasted jQuery, but it was time for us to end this relationship.
I still have a few more changes to make such as updating the documentation, but be on the lookout for a new release of bootstrap-slider without jQuery!
Useful Resources:
- http://youmightnotneedjquery.com/ — Vanilla JS implementations of popular jQuery methods
- https://github.com/ariya/phantomjs/issues/10522 — Discussion surrounding the lack of bind() support in PhantomJS.
- https://github.com/desandro/jquery-bridget — jQuery Bridget: Project for adapting non-jQuery plugins to use å jQuery-plugin style interface.
- http://jsperf.com/ — jsPerf: Allows you to compare and contrast the performance of various JavaScript code snippets across multiple browser types/versions