Choppy parallax in React. What's best practice?
Asked Answered
V

1

6

This is a general question about how to best produce parallax effects in React!

I want to directly update the position of an element based on the scroll position. The goal is to have the element appear fixed. I've tried some different methods but always having issues with choppy/flickering behavior in Safari on Mac, sometimes Chrome, and on some devices.

I've tried making the body fixed and have the root div scroll (to get rid of the elastic effect some browsers) but same issues. I've also tried adding translateZ(0) to active 3d rendering but also same issues.

Is this caused by some mechanics in how often React updates the component or is this a browser issue?

Is there a better way to create this effect?

import React, { useState, useEffect } from "react";

const useScroll = () => {
  const [scroll, setScroll] = useState(0);
  useEffect(() => {
    window.addEventListener("scroll", scrollHandler);
    return () => {
      window.removeEventListener("scroll", scrollHandler);
    };
  }, []);
  const scrollHandler = () => {
    setScroll(window.scrollY);
  };
  return scroll;
};

export default function App() {
  const scrollPos = useScroll();

  return (
    <main style={{ height: "300vh" }}>
      <div>Normal Div</div>
      <div style={{ transform: `translateY(${scrollPos}px)` }}>Fixed Div</div>
    </main>
  );
}

This is a simple reproduction of the problem. It produces the same issue for me (choppy in some browsers, esp. Safari on Mac).

Vinavinaceous answered 3/8, 2019 at 19:59 Comment(0)
T
0

Keeping the scroll position in a component state does indeed require to re-render said component every time a scroll event is triggered. That firing rate is huge, which is why DOM mutations should be avoided.

In your case, React not only has to update the state and reconcile, it must also mutate the DOM to replace that last div, because one of its props, style, is different every render. And the choppy effect is going to get worse as your components get bigger.

=> Even though React is highly performant, scroll events are a specific beast, and thus qualify for an imperative handle:

const useScrollHandler = (handler) => {
  useEffect(() => {
    window.addEventListener('scroll', handler)
    return () => {
      window.removeEventListener('scroll', handler)
    }
  }, [])
}

const FixedDiv = (props) => {
  const ref = useRef()
  const handler = () => { ref.current.style.transform = `translateY(${window.scrollY}px)` }
  useScrollHandler(handler)
  return <div ref={ref} {...props} />
}

function App() {
  return (
    <main style={{ height: '300vh' }}>
      <div>Normal Div</div>
      <FixedDiv>Fixed Div</FixedDiv>
    </main>
  )
}
Tattered answered 3/8, 2019 at 22:55 Comment(3)
Thank for you reply! But your example gives me the exact same choppy behavior? I'm no under-the-hook expert and didn't really understand what the big difference is here. As for firing rate on scroll events, I've tried using requestAnimationFrame to throttle but that had no effect.Vinavinaceous
Actually, I now do understand that in your example both App and FixedDiv is not re-rendering on every new scroll position. But why am I getting the same choppy behavior then?Vinavinaceous
That's just down to pure CSS if you still see a glitch, React doesn't get in the way. If you want a fixed element, go with position: fixed; to avoid it. For your real use case, I would have to seeTattered

© 2022 - 2024 — McMap. All rights reserved.