2
\$\begingroup\$

The following code animates an item's Y position based on how much of its parent is currently visible. This is the first time I ever animated something on JavaScript based on the window scroll position and the current performance is extremely bad. I am looking for ways to get this running as fast as possible for a smooth animation.

The code I use to get a percentage between 0 and 100 based on how much an element is currently visible is the following:

    const calcVisibilityForElem = (elem) => {
      const windowHeight = window.innerHeight;
      const docScroll = window.pageYOffset || document.documentElement.scrollTop;
      const divPosition = elem.offsetTop;
      const divHeight = elem.offsetHeight;
      const hiddenBefore = docScroll - divPosition;
      const hiddenAfter = divPosition + divHeight - (docScroll + windowHeight);

      if (
        docScroll > divPosition + divHeight ||
        divPosition > docScroll + windowHeight
      ) {
        return 0;
      } else {
        let result = 100;

        if (hiddenBefore > 0) {
          result -= hiddenBefore * 100 / divHeight;
        }

        if (hiddenAfter > 0) {
          result -= hiddenAfter * 100 / divHeight;
        }

        return result;
      }
    };

The code I use to animate:

const calcVisibilityForAllArticles = () => {
  document.querySelectorAll('article').forEach((item) => {
    const top = calcVisibilityForElem(item);
    if (top !== 0) {
      item.querySelector('span').style.transform = `translateY(${Math.floor(
        top * 1.5
      )}px)`;
    }
    if (top >= 80) {
      const navItem = document.querySelector(`[href="#${item.id}"]`);
      document.querySelectorAll('nav a').forEach((navItem) => {
        navItem.classList.remove('highlight');
      });
      navItem.classList.add('highlight');
      navItem.focus();
    }
  });
};

In my process trying to boost its performance I made 2 changes, first of all, I animate using translate in the place of the top position, from what I understand changing any value other than transform causes a window repaint. The other change I made is to only animate elements that are inside my viewport. (top !== 0) === Only animate elements that are at least 1% inside the viewport.

I don't think that the code itself is very slow, however, the calcVisibilityForAllArticles function is run by:

document.onscroll = () => {
  calcVisibilityForAllArticles();
};

With means that the function is run back to back extremely fast. (With is needed for a smooth animation.)

Any help is more than welcome.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Faster does not mean smoother.

Some very quick notes.

Queries to the DOM are very slow compared too what they do. You should only query the DOM for the elements once and then just use the references you have.

When page is ready

// query the DOM a stash the elements in an array of objects so you can add 
// And more DOM related info as you find it.
const articles = [...document.querySelectorAll('article')].map(element=> ({element}));

Then in the code

const calcVisibilityForAllArticles = () => {
  // Now you can loop through the element without needing to traverse the DOM each time.
  for(const item of articles){ // item holds the element as element
    const top = calcVisibilityForElem(item.element);

    // for the first pass 
    if (top !== 0) {
       // for the first pass  of visible do the query and save the element
       if(!item.span) { item.span = item.querySelector('span') }
       // now you can use it directly
      item.span.style.transform = `translateY(${Math.floor(top * 1.5)}px)`;
    }
    // do the same for all the other elements you search the DOM for each animation cycle.

Timing

As you have not supplied a working example there is no way to know if you are overloading the page or not.

The important thing to remember is that all browsers have a fixed display rate of 60FPS. Making changes to the DOM at a rate higher than this will only result in shearing and flickering. It can also have a pull down effect on the browser as you block it from doing what it needs to do.

You must particularly be careful with events that are driven by the mouse. Some mice can trigger 500+ events a second. You should never update the DOM from move events that can come from the mouse.

RequestAnimationFrame

When ever you make any regular changes to the DOM use the timer callback via the function requestAnimationFrame. It is designed to present the smoothest possible animations. It is synced to the browsers frame rate, and will not present changes until the display device is not scanning out the pixels.

To use

var scrollPos;  // this holds the current scroll position

// In the scroll event only get the scroll position and make no changes to the
// DOM
function yourScrollEvent() {  ... scrollPos = foo ...}

You make the DOM updates via the frame request

 var lastScrollPos; // used to check if there is a need to animate

 function scrollLoop(){
      if(lastScrollPos !== scrollPos){ // only render if needed
          lastScrollPos = scrollPos;

          // do your DOM stuff here

       }

      requestAnimationFrame(scrollLoop); // request next frame

  }


   // start it off via the frame request.
   requestAnimationFrame(scrollLoop); 

This will produce the smoothest possible animation. If it can not keep up and drops frames then you need to reduce the complexity of the animation.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.