Breaking Up (with jQuery) Is Hard To Do

Nora Brown
7 min readNov 30, 2018

--

Learning by rewriting a simple jQuery plugin using plain Javascript.

Bust out of your jQuery routine and write some plain-ass Javascript. Also, this is my cat.

While playing with the Intersection Observer API to build an image lazy-loader, I wanted to provide a simple, lightweight fallback for browsers that don’t support it (lookin’ at you, Safari). Most importantly, a fallback that didn’t depend on any other libraries.

My favorite lazy-loading plugin is Unveil, from Luis Almeida. It is extremely to the point! Providing a bare minimum of configuration, it’s fewer than 50 lines of code. But, it does depend on jQuery or Zepto. So, I embarked on an interesting detour of rewriting this plugin in “vanilla” Javascript.

jQuery, the gateway code

It turned out to be a gratifying exercise and an excellent learning experience, and I recommend it for anyone whose path to Javascript was paved with jQuery, as mine was. I will be forever grateful to John Resig for creating jQuery. It made sense to me in a way that pure Javascript did not at the time, and allowed me to be creative in code without worrying about browser differences and opaque APIs.

But today, writing plain ol’ Javascript that works across modern, and even modern-ish browsers isn’t so painful. If a site isn’t using a library like jQuery already, it’s a shame to introduce it just for the sake of a little piece of functionality — especially one like lazy-loading, which is aimed at improving performance.

Our starting point — the Unveil.js jQuery plugin

I had previously made a few small modifications to the original plugin:

  1. Instead of using a separate data-src-retina attribute, I added data-srcset, as srcset is widely supported now, and seems to be the way forward for responsive images.
  2. I removed the loaded variable, which was unnecessary.
  3. I added a debounce function, to throttle the rate at which the scroll event handler is called.
  4. I changed some variable names and added lots of comments.

You can view the original jquery.unveil.js on Github, and here is my revision:

Slightly revised jQuery plugin, unveil.js, for lazy-loading images

What’s jQuery, what’s vanilla?

I set about identifying which bits were reliant on jQuery and which weren’t. I picked out a few categories that needed ‘translating’ to plain JS:

  • Event binding and custom event triggering
  • DOM interaction, like fetching and manipulating elements
  • Measuring
  • Dealing with collections, like iterating and filtering
  • Plugin architecture

Un-jQuerying Unveil.js

The unveil() function

Let’s start with the heart of the plugin, the unveil() function:

And here is my rewrite:

This function takes the initial collection of images and filters it to only those that are “in view” — in other words, intersecting the viewport. The key functionality here, filter() is a native function on Array.prototype, so nothing to worry about there. In the original, images is a jQuery collection, while in the rewrite, it’s a plain array of DOM nodes. So, we replace the jQuery-wrapped $img with a DOM node element, img.

Next we come to:

if ($img.is(":hidden")) return;

:hidden is a jQuery-provided selector that looks at display value, ancestor display value, height and width of zero, and type=”hidden” for form elements. I think the important thing in this context is display value, so I approximated this line with:

if (img.style.display === "none") return;

Next, there are a few ways to retrieve all the measurements required. The first two, viewport top and bottom, are straightforward. For the last two, the distance from the top of the document to the top and bottom of the image, we can use getBoundingClientRect(). We only call it once and store the return value in a variable rect. Because its values are relative to the viewport, not the document, we add the vertical scroll distance. Then, the math is the math:

var rect = img.getBoundingClientRect(),
wt = window.scrollY, // window vertical scroll distance
wb = wt + w.innerHeight, // last point of doc visible in window
et = wt + rect.top, // top of document to top of element
eb = wt + rect.bottom; // top of document to bottom of element
// bottom of img is below top of browser (- threshold)
// && the top of img is above bottom of browser (+ threshold
return eb >= wt - th && et <= wb + th;

All that’s left is to trigger an event for each element in the new inview collection. Unfortunately, what jQuery can do in a single elegant line:

inview.trigger("unveil");

becomes much more verbose in our rewrite:

// create a custom event, with CustomEvent if available, else using createEventif (w.CustomEvent) {
var unveilEvent = new CustomEvent('unveil');
} else {
var unveilEvent = document.createEvent('CustomEvent');
unveilEvent.initCustomEvent('unveil', true, true);
}
// trigger the custom event on each element in the inview arrayinview.forEach(function(inviewImage){
inviewImage.dispatchEvent(unveilEvent);
});

For one thing, it becomes a two-step process: creating the event, then dispatching it. Secondly, there is still a ‘modern’ and fallback way of creating the event. And third, we have to handle iteration ourselves. So, it gains a few kilobytes.

The last step in the unveil() function is to revise the images array to only include the images that aren’t yet in view. jQuery has the brilliant .not() method which can take a selector, a function, or, as in this case, another selection! In plain JS, I’ve decided to just re-fetch the collection. With a line added to remove the the unveil class once an image comes into view, the end result is the same. Performance-wise, probably not ideal. Other options would be to manually divide the images array into inview and notInview arrays, or possibly to initially make images a live set of elements instead of a static array.

The unveil event handler

The other major chunk of code to contend with is:

images.one("unveil", function() {
var $img = $(this);
var source = $img.attr("data-src");
var sourceset = $img.attr("data-srcset");

if (source) {
$img.attr("src", source);
}
if (sourceset) {
$img.attr('srcset', sourceset);
}
});

In the jQuery version, images is a jQuery collection, and .one() conveniently attaches an event handler to each element in the collection, and unbinds after it’s been executed one time.

In our rewrite, images is a simple array, and we have to handle iteration ourselves. As for firing only once, there is, I learned, a native Javascript way to do this. You can pass an options object to addEventListener(), which includes an option once: true. However, this isn’t supported in IE. MDN has a clear explanation of the problem and solution for this, which is to test for support thusly:

var onceSupported = false;
try {
var options = {
get once() {
onceSupported = true;
}
};
w.addEventListener("test", options, options);
w.removeEventListener("test", options, options);
} catch(err) {
onceSupported = false;
}

With that check in place, we can pass the option, or false, depending on whether support was detected:

image.addEventListener('unveil', function(){
// do stuff...
}, onceSupported ? { once: true } : false);

Inside our unveil event handler, things look pretty much as expected. Whereas jQuery uses the same method, attr(), to both get and set attribute values, native Javascript uses getAttribute() and setAttribute().

Kicking it off

An important piece we still haven’t handled is how to kick off the whole process of unveiling our images. In the jQuery plugin we have:

$w.on("scroll.unveil resize.unveil lookup.unveil", debouncedUnveil);

What’s up with all the .unveils? jQuery provides a way to namespace your event bindings, which allows end-users of plugins to unbind specific event handlers (without needing to keep a reference to the handler function), instead of clobbering all handlers for a given event. With the namespaces in place, I can remove this scroll event handler with:

$w.off("scroll.unveil")

Meanwhile, any other handlers attached to the scroll event would stay in place. While there are a few viable solutions to namespacing events in Vanilla javascript, I talked myself out of this requirement because I couldn’t imagine a scenario where it would be needed for this particular plugin.

Plugin infrastructure

The jQuery plugin follows a pretty common pattern:

// plugin structure
;(function($) {
$.fn.unveil = function(threshold, callback) {
...
return this;
};
})(window.jQuery || window.Zepto);
// to use the plugin:
$('.unveil').unveil(100, function(){
...
});

Because it has very simple configuration, it accepts options as individual arguments, rather than a large options object, like more complex plugins. It extends the jQuery prototype, and returns this, which is the jQuery collection, so that other methods can be chained after it.

For our purposes, we can use this simple pattern:

// plugin structure
;(function() {
this.unveil = function(threshold, callback){
...
}
}());
//to use the plugin:
window.unveil(100, function(){
...
});

We’re adding the unveil method to window, the global scope. Ken Wheeler has an excellent post on writing a more complex plain javascript plugin with options, methods, etc.

Wrap-up

Here is the completely re-written plugin:

We now have a simple image lazy-loader which is a tiny bit longer than the original, but that’s far outweighed by not requiring any external libraries.

Conclusion: a worthwhile exercise

If you are comfortable using and writing jQuery and jQuery plugins, and want to improve your baseline Javascript knowledge, I recommend taking a favorite (preferably simple) plugin and rebuilding it without jQuery. It’s a fun way to learn new things about native Javascript, and reinforce your jQuery knowledge.

Here are some helpful resources:

--

--

Nora Brown

I design, build, and optimize websites. Currently loving CSS Grid and Vue.js.