Intersection Observer and requestAnimationFrame — How to be less busy on the thread

Did you know that the naive implementation of Intersection Observer or requestAnimationFrame can be improved dramatically when performance and memory is a concern?

Lazy loading assets, detecting if an element is within the viewport boundaries, ad visibility or animating images are just a few examples of when you might want to reach for an Intersection Observer or requestAnimationFrame.

To do work, it takes work

What if you have a lot of elements on the page to observe? Like hundreds of images. In JavaScript, you wouldn’t create a new instance of a class hundreds or thousands of times if you could reuse the old one, right? We have all seen this module pattern that will create N number of foo and bar functions for every invocation of Klass2. If memory or performance is critical, the prototypal pattern would be a better design decision.

// module pattern
Klass2 = function() {
let foo = () => {
...
};
let bar = () => {
...
};

return {
foo: foo,
bar: bar
}
}

Why can’t we make the same optimizations with Intersection Observers or requestAnimationFrame? Introducing intersection-observer-admin and raf-pool. Let’s look at the Intersection Observer example first.

const admin = new IntersectionObserver();

admin.observe(element, enterCallback, exitCallback);

How does it work? Just pass it the element you want to observe, an enter and/or exit function, a hash of options, and lastly, if applicable, the scrollable container the element lives in. What we then do is create an administrator that adds an observed element to the same IntersectionObserver instance if it uses collectively the same root, hash of options, and scrollable container. Otherwise, we stamp out a new Intersection Observer instance. We also have to remember to release the memory and unobserve or disconnect. In Ember.js, we can take advantage of lifecycle hooks such as Service destroy.

destroy(...args) {    
this.admin.destroy(...args);
}

With raf-pool, it is pretty simple. Just provide a unique identifier and a callback for when the

const rafPool = new RafPool();

const callback = () => {
rafPool.add(element.id, callback);
};

callback();

Now do you really need either of these? Probably not, unless your pages are “heavy” with lots and lots of images to observe. This solution has been in use in ember-infinity and ember-in-viewport for quite some time without a hitch.

Let’s look at a more “strenuous” example. Intersection Observers are very light on memory and activity. You won’t see much on the main thread. However, with raf-pool, you can see quite a difference in “busyness of the thread”. If I have ~20 images on the page I need to observe, without raf-pool, there are lots of callbacks being fired every 16ms as shown while profiling an application.

Normal rAF behaviour

With raf-pool

Given Intersection Observer support isn’t available in all browsers, it is good to provide Intersection Observer AND requestAnimationFrame support in your application, defaulting to the former and switching to the latter if ‘IntersectionObserver’ is not in ‘window’.

Every app can benefit from reducing memory use and thread activity. Let me know your thoughts or submit an issue on either repository if you have any questions! Also thanks to the countless people I have interacted with to provide ideas and help integrate and test these features.

Scott Newcomer