You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React滚动位置Hook导致滚动时组件持续重渲染如何优化

问题解答

你的认知确实有偏差,当前的持续重渲染是完全符合React运行规则的预期行为

根本原因很简单:你写的useScrollPosition内部,每次scroll事件触发都会执行setScrollPosition更新Hook自身的state。只要调用了这个Hook的组件,不管你后续拿这个返回值做什么判断,只要Hook里的state变了,组件就会先触发重渲染。
你后面写的useEffect阈值判断,是组件已经因为scrollPosition变化完成重渲染之后才会执行的逻辑,根本拦不住前面的重渲染流程——实际运行逻辑是:滚动触发→Hook更新state→组件重渲染→effect执行判断阈值,发现没跨10px就不更新scrolled,不会触发第二次渲染而已,但是第一次无意义的重渲染已经发生了。

问题根源

你把连续变化的滚动位置存在了通用Hook的state里,所有用这个Hook的组件,都会被迫接收每一次滚动的位置更新,哪怕组件根本不需要知道精确的滚动距离,只关心「有没有超过10px」这种二值状态。

可落地的优化方案

  • 方案一:把阈值判断下沉到Hook内部,从源头挡住无意义的state更新
    如果你的常用场景就是判断滚动是否跨过固定阈值,可以直接改造Hook支持传入阈值参数,只有状态真的发生变化的时候才更新state,不要把每次滚动的精确位置都抛给组件。
    改造后的Hook代码:
import { useLayoutEffect, useState } from 'react';

const useScrollPosition = (threshold) => {
  // 传了阈值就直接存布尔值,没传就存原始滚动距离
  const [scrollValue, setScrollValue] = useState(() => {
    const yOffset = window.pageYOffset;
    return typeof threshold === 'number' ? yOffset > threshold : yOffset;
  });

  useLayoutEffect(() => {
    const handleScroll = () => {
      const yOffset = window.pageYOffset;
      const newVal = typeof threshold === 'number' ? yOffset > threshold : yOffset;
      // 只有值和之前不一样才调用setState,完全一样就不触发更新
      setScrollValue(prev => prev === newVal ? prev : newVal);
    };

    // 加passive: true提升滚动性能,告诉浏览器不会阻止默认滚动行为
    window.addEventListener('scroll', handleScroll, { passive: true });
    return () => window.removeEventListener('scroll', handleScroll);
  }, [threshold]);

  return scrollValue;
};

export default useScrollPosition;

对应业务组件可以直接简化,删掉多余的scrolled状态和对应的effect:

const Component = () => {
  // 直接拿到是否滚动超过10px的布尔值,只有跨阈值的瞬间才会触发重渲染
  const scrolled = useScrollPosition(10);

  return (
    <div
      className={clsx(style.element, {
        [style.elementScrolled]: scrolled,
      })}
    >
      {children}
    </div>
  );
};
  • 方案二:如果要保留Hook返回全量滚动位置的通用能力,改成单例订阅模式
    如果有的场景需要拿精确滚动位置,有的场景只需要阈值判断,可以把scroll监听做成全局单例,整个应用只注册一次scroll事件,组件按需订阅自己需要的派生值,只有派生值变化的时候才触发组件更新:
import { useLayoutEffect, useState, useRef } from 'react';

// 全局单例相关变量,整个应用共用一个scroll监听
const listeners = new Set();
let isBound = false;
const triggerListeners = () => {
  const yOffset = window.pageYOffset;
  listeners.forEach(listener => listener(yOffset));
};
const bindGlobalScroll = () => {
  if (isBound) return;
  window.addEventListener('scroll', triggerListeners, { passive: true });
  isBound = true;
};
const unbindGlobalScroll = () => {
  if (listeners.size > 0) return;
  window.removeEventListener('scroll', triggerListeners);
  isBound = false;
};

const useScrollPosition = (selector = val => val) => {
  const [value, setValue] = useState(() => selector(window.pageYOffset));
  // 用ref存最新的选择器函数,避免因为选择器变化重新绑定监听
  const latestSelector = useRef(selector);
  latestSelector.current = selector;

  useLayoutEffect(() => {
    bindGlobalScroll();
    const listener = (yOffset) => {
      const newVal = latestSelector.current(yOffset);
      setValue(prev => prev === newVal ? prev : newVal);
    };
    listeners.add(listener);
    
    return () => {
      listeners.delete(listener);
      unbindGlobalScroll();
    };
  }, []);

  return value;
};

export default useScrollPosition;

使用的时候灵活度很高:需要全量滚动位置就直接const y = useScrollPosition(),需要判断阈值就传选择器const isScrolled = useScrollPosition(y => y > 10),需要判断是否触底就传const isBottom = useScrollPosition(y => window.innerHeight + y >= document.body.scrollHeight - 20),所有场景下都只有返回值真的变化的时候才会触发组件重渲染,而且全局只有一个scroll监听,性能比每个组件自己绑监听好很多。


内容的提问来源于stack exchange,提问作者Alex

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 08:18:28