Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance issue when using scroll timeline (chrome), costly layerize when scrolling #705

Open
c99pjn opened this issue Feb 2, 2025 · 5 comments

Comments

@c99pjn
Copy link

c99pjn commented Feb 2, 2025

Describe the bug
I have been debugging a scrolling performance issue in our app. When taking performance snapshots I noticed a lot of time is spent in "layerize" as a result of using OverlayScrollbars.

Image

After trying some things locally I think the problem is related to how that --os-scroll-percent is the property that is animated here. I think animating a custom property here, instead of the resulting transform, the animation isn't done nearly as efficiently as it could be by the browser.

Probably requires some refactoring but instead of animating the custom property animate the transform directly

{
  // dummy keyframe which fixes bug where the scrollbar handle is reverted to origin position when it should be at its max position
  clear: ['left'],
  transform: [0, "translateY(calc(100cqh - 100%))"],
}

Environment

  • OverlayScrollbars version: 2.10.1
  • Used Operating System(s): Mac
  • Used Browser(s) (with version): Chrome 132.0.6834.160

Additional context
Add any other context about the problem here.

@tryggvigy
Copy link
Contributor

Good catch. I think when a css custom property is used in a transform the browser has to de-optimize and can not run the transform on the compositor. It has to run it on the main thread because there is no guarantee that changes to the custom property would not cause layout changes elsewhere in the DOM.

@KingSora
Copy link
Owner

KingSora commented Feb 3, 2025

@c99pjn Good day :)

Good catch! I'm honestly a bit dissapointed that this happening because I've (not so long ago) refactored the whole "position the scrollbar handle" code and based everything on this custom css property logic in order to gracefully bridge the gap between browsers which support the ScrollAnimationTimeline API and browsers which don't.

I'll look into this issue in the near future, in case @c99pjn @tryggvigy or anyone else knows a workaround where I can keep the custom css property - I would prefer this solution over setting the transformation directly in the animation.

@c99pjn
Copy link
Author

c99pjn commented Feb 3, 2025

Yeah, I understand why it was built like this but I'm afraid I don't think there is a way to get good performance when animating a custom property.

I don't know if it will actually work but could you keep the existing implementation and still change the scroll timeline animation to transforms. I think the animation transform will override the default one. I guess another complication is that this only works in the container query version where top and left aren't changed.

@tryggvigy
Copy link
Contributor

tryggvigy commented Feb 3, 2025

Yeah I completely get that, it's very unfortunate css custom properties can cause so much work for the browser. I guess with the power of their flexibility come some downsides.

Let's have a think about if there is cheap-ish way to fix this without adding much maintenance cost.

Sidetrack: and sadly things are not getting better with registered custom properties either (at least for now) https://www.bram.us/2023/02/01/the-gotcha-with-animating-custom-properties/ since registered custom properties substitute as their computed value and the compositor can't do variable substitution

@c99pjn
Copy link
Author

c99pjn commented Feb 4, 2025

I wrote a quick patch which replaces the animation after initialising the scrollbars. Not sure we will end up using this and we have a rather special situation where we bundle a chromium based browsers so we know features are available.

/**
 * If there is a scroll timeline animation on the scrollbar that animates --os-scroll-percent
 * from 0 to 1, replace it with an animation on the scrollbar handle that instead animates
 * a transform directly to avoid the performance overhead of animating a custom property.
 *
 * See https://github.com/KingSora/OverlayScrollbars/issues/705
 */
const patchScrollbar = (
  container: Element,
  direction: 'vertical' | 'horizontal',
): Cancellable => {
  const viewport = container.querySelector(
    ':scope > [data-overlayscrollbars-viewport]',
  );
  const scrollbar = container.querySelector(
    `:scope > .os-scrollbar-${direction}`,
  );
  const handle = scrollbar?.querySelector('.os-scrollbar-handle');
  const animation = scrollbar?.getAnimations?.()?.at(0);

  const isScrollTimelineAnimation =
    animation &&
    'ScrollTimeline' in window &&
    // @ts-ignore
    animation.timeline instanceof ScrollTimeline;

  if (!isScrollTimelineAnimation) return () => {};
  if (!isKeyframeEffect(animation.effect)) return () => {};

  const keyframes = animation.effect.getKeyframes();
  const animatesScrollPercent =
    keyframes.at(0)?.['--os-scroll-percent'] === '0' &&
    keyframes.at(1)?.['--os-scroll-percent'] === '1';

  if (viewport && handle && animatesScrollPercent) {
    animation.cancel();
    // @ts-ignore
    const timeline = new ScrollTimeline({
      source: viewport,
      axis: direction === 'vertical' ? 'block' : 'inline',
    });
    // See https://github.com/KingSora/OverlayScrollbars/blob/master/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.elements.ts#L125
    const newAnimation = handle.animate(
      {
        clear: ['left'],
        transform:
          direction === 'vertical'
            ? ['translateY(0)', 'translateY(calc(100cqh - 100%))']
            : ['translateX(0)', 'translateX(calc(100cqw - 100%))'],
      },
      {
        timeline,
      },
    );
    return () => {
      newAnimation.cancel();
    };
  }
  return () => {};
};

const patchScrollAnimation = (container: Element): Cancellable => {
  const cancelVertical = patchScrollbar(container, 'vertical');
  const cancelHorizontal = patchScrollbar(container, 'horizontal');
  return (): void => {
    cancelVertical();
    cancelHorizontal();
  };
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants