Makuhari Development Corporation
5 min read, 921 words, last updated: 2024/1/21
TwitterLinkedInFacebookEmail

Infinite Auto Scroll Carousel in Nextjs

By writing an auto-scroll carousel, we met a confusing and hard-to-spot issue with Nextjs CSR components inside SSR pages.

The frame rate of our auto-scroll is unexpectedly affecting resource loading which includes scripts, threejs glb files, etc.

We have to limit the animation down below 30fps to overcome this issue, and the reason remains unclear.

Here is the procedure leading us to the problem.

The Implementation

There are tons of methods to do a carousel, including the auto-scrolling stuff. Let's do it without any libs/dependencies at the beginning.

Feature:

  • Carousel with a limited amount of images, and, let it scroll repeatedly (infinitely).
  • Enable auto-scrolling with a given speed & frame rate.
  • Enable manual scrolling and do not affect the auto one when finished.

Auto-scroll with requestAnimationFrame

To let the scroll move by itself, we can add animation via requestAnimationFrame.

const scrollingDivRef = useRef<HTMLDivElement>(null);
const scrollingIdRef = useRef<number>(0);
 
useEffect(() => {
  const scrollingDiv = scrollingDivRef.current;
  if (scrollingDiv) {
    const slowScroll = () => {
      const scrollSpeed = 1;
      scrollingDiv.scrollLeft += scrollSpeed;
      scrollingIdRef.current = requestAnimationFrame(slowScroll);
    };
    slowScroll();
  }
  return () => cancelAnimationFrame(scrollingIdRef.current);
}, []);
 
return (
  <div
    className="scrollbar-none w-[100vw] overflow-x-scroll"
    ref={scrollingDivRef}
  >
    <div className="flex w-fit">
      {contentList.map((x, index) => (
        <img />
      ))}
    </div>
  </div>
);

If the list is long enough, it should continue to horizontally auto-scroll itself till the very end.

Infinite scroll

Now let's make it infinite.

We quickly came to the idea to make a copy of the list of the content, and join them to the start/end of the origin one when scrolled to the necessary position. This would cause some DOM operation.

Another option is to keep the two lists steady, and automatically helping the list re-scrolls to the necessary position.

It needs some calculation of:

  • Element.scrollWidth: a measurement of the width of an element's content, including content not visible on the screen due to overflow.
  • Element.clientWidth: the inner width of an element in pixels. It includes padding but excludes borders, margins, and vertical scrollbars.

So basically we start at the middle, once touch the border, scrolls back to the middle.

const handleScroll = () => {
  const scrollingDiv = scrollingDivRef.current;
  if (scrollingDiv) {
    // - 1 for the fix of none int width list
    if (scrollingDiv.scrollLeft > scrollingDiv.scrollWidth - scrollingDiv.clientWidth - 1) {
      scrollingDiv.scrollLeft -= scrollingDiv.scrollWidth;
    } else if (scrollingDiv.scrollLeft === 0) {
      scrollingDiv.scrollLeft += scrollingDiv.scrollWidth;
    }
  }
};
 
// two lists down there
return (
  <div
    className="scrollbar-none w-[100vw] overflow-x-scroll"
    ref={scrollingDivRef}
    onScroll={handleScroll}
  >
    <div className="flex w-fit">
      {[...contentList, ...contentList].map((x, index) => (
        <img />
      ))}
    </div>
  </div>
);

If the images have some gap between each other, it could need some additional calculation for the two conditions.

// for example, images have gap-8 (32px), after two list joined, calculate the scroll amount as below
scrollingDiv.scrollLeft -= (scrollingDiv.scrollWidth - 32) / 2 + 32;
scrollingDiv.scrollLeft += (scrollingDiv.scrollWidth - 32) / 2 + 32;

This just works like a charm, you can't feel anything strange when manually infinite scrolling crosses the content, and once you finish the scrolling, it continues to auto-scroll from the position you stopped.

The Issue with Resource Loading

As time went by, we faced a resource loading issue with nextjs scripts:

The scripts loaded without any problem after the first-time page-refreshing followed by a local build, but couldn't be loaded(used), or function normally as long as we do not proceed one of the following stuff:

  • change the viewport size (like open the Chrome developer tools, or drag the screen size).
  • switch Mac screens away to another one then come back (three fingers drag on the control pad).
  • make another local build (by doing another save operation followed by a hot update).

This is so frustrating, without being enable to solve it, lucky we limited the problematic place:

  • threejs glb file resource loaded by a hook also affected in a same way.
  • pages without the auto-scroll carousel, all work normally.

So the carousel caused this problem!

Seems the animation to blame

Since we can actually replace the requestAnimationFrame using setInterval, we quickly made the changes.

const scrollingDivRef = useRef<HTMLDivElement>(null);
 
useEffect(() => {
  const scrollingDiv = scrollingDivRef.current;
  if (scrollingDiv) {
    const intervalId = setInterval(() => {
      const scrollSpeed = 1;
      scrollingDiv.scrollLeft += scrollSpeed;
    }, 50);
    return () => clearInterval(intervalId);
  }
}, []);
 
// other places remain the same

It's solved! Seems the requestAnimationFrame to blame?

Actually the key is the frame rate

Next, we tried to set the interval time from 50ms to 20ms, and suddenly, the same problem came back!

Is the frame rate related? Well, 30fps is in the middle of those numbers. So after a quick test, we found: 33ms NO, 34ms OK.

There is a 30fps limitation!

The requestAnimationFrame refresh rate is actually following the display refresh rate so it was 60fps, which was problematic.

The frequency of calls to the callback function will generally match the display refresh rate

It affects all the parent components

The carousel itself is a CSR child component inside a SSR page, as a leaf on a component tree, but it could affect distance SSR components' resource loading, how?

What causes this issue?

  • Next
  • React

Found nothing special by doing a quick go through of the source code of Nextjs and React using the keyword "30".

The question remains.

Generally, if we auto-scroll the list below 30fps, there would be no longer a problem.

Makuhari Development Corporation
法人番号: 6040001134259
サイトマップ
ご利用にあたって
個人情報保護方針
個人情報取扱に関する同意事項
お問い合わせ
Copyright© Makuhari Development Corporation. All Rights Reserved.