Home > Back-end >  Looking for a better (more efficient) way to test for scroll position
Looking for a better (more efficient) way to test for scroll position

Time:06-24

This is a Javascript project, using the jQuery library.

I have a sticky bar I would like to show whenever the browser window is below a certain page position, and otherwise hide it. Its ID is #sticky-atc-bar.

The page position is determined by the top of an element with class .product-section.

Based on research into how to do this, I originally came up with the following JS.

$( document ).ready(function() {
    $('#sticky-atc-bar').hide();

    var section = $(".product-section");
    var offsetSection = section.offset().top; //check for top property
    $(function() {
        $(window).scroll(function() {
        console.log('Scroll Event');

            if ($(window).scrollTop() >= offsetSection) { // reached product section
                console.log('True:');
                $('#sticky-atc-bar').show();
            } else { // not reached product section
                console.log('False');
                $('#sticky-atc-bar').hide();
            }
        });
    }); 
});

This works. Yet, I am aware it generates a lot of scroll events, the entire time the user is engaged with the page. Knowing that's very inefficient, I went looking for alternative approaches. Along the way I read this article called "Learning from Twitter", which made total sense.

I also came across this solution to a similar requirement. My version of that code is:

var target = $(".product-section").offset().top,
    timeout = null;

$('#sticky-atc-bar').hide();

$(window).scroll(function () {
    if (!timeout) {
        timeout = setTimeout(function () {
            console.log('scroll');            
            clearTimeout(timeout);
            timeout = null;
            if ($(window).scrollTop() >= target) {
                 $('#sticky-atc-bar').show();            
            } else {
                 $('#sticky-atc-bar').hide();
            }
        }, 250);
    }
});

I set it up on this jsFiddle, and it works.

However ... It still generates a significant number of events. They have simply been reduced to one event every 250 ms, so 4 every second.

What I would like to know is if there's a better way to go about this? Put another way, aside from changing the timeout on this second piece of code, can I reduce this operation to even fewer events? And is that even necessary at this point, or is one event every 250ms not a significant impact?

CodePudding user response:

Here you go: https://jsfiddle.net/son0azfj/

var productEl = document.querySelector('.product-section')
var stickyEl = document.querySelector('#sticky-atc-bar')

// check if we have elements
if (productEl && stickyEl) {
  // create an observer object, binding toggleDisplayFactory to it
  var observer = new IntersectionObserver(toggleDisplayFactory(stickyEl))
  
  // observe the product element
  observer.observe(productEl)
}

// [1] create a named function which accepts an element...
function toggleDisplayFactory(element) {
  // and returns a function - the handler that IntersectionObserver expects
  return function handler(entries) {
      // determine if the observed element is in the viewport or not
      var isInViewPort = Boolean(entries.find(entry => entry.intersectionRatio > 0))
      /**
       * if in the viewport:
       *   we want the element passed in at [1] to use it's own styles, i.e. remove the inline style property
       * else:
       *   we want the element to inherit its default display property (e.g. block, inline, inline-block, etc.) and thus be visible
       */
      var display = isInViewPort ? null : 'inherit'
    
      // set the display property of the element passed in at [1]
      element.style.display = display
  }
}
  1. use InterSectionObserver to watch for the position of an element with respect to the viewport. Use that element's position in the viewport to "do something else", i.e. show / hide another element
  2. avoid setTimeout for manipulating the DOM - use requestAnimationFrame instead (not required here)
  3. avoid querying DOM elements repeatedly - get them once, and reference them where they need to be manipulated
  4. prefer setting defaults in CSS, and then in JS override a specific property inline, removing the property when you no longer need it

Reasoning:

IntersectionObserver allows you to observe an arbitrary element with respect to its position in the viewport, instead of a scroll event which fires rapidly as one scrolls. The events an observed element trigger are only triggered when its position relative to the viewport changes

setTimeout - If you were to use a timeout, you would need to think of some number which is appropriate to use when using setTimeout. How do you do this? Well, you can only guess... you'd have to use a magic number. requestAnimationFrame instead knows when the DOM is ready for re-rendering and calculating layout - no magic numbers required


Links:


EDIT: Feel free to replace document.querySelector with $(selector) - the rest of the code will work as-is.

  • Related