🚨React性能翻车案例:别让 setState 毁了你的主线程!

329 阅读2分钟

页面滚动一卡一卡的,是性能瓶颈?DOM 结构太复杂?IntersectionObserver 用错了?我也这么想过……直到我发现罪魁祸首是:高频率的 setState 调用

本文分享一次真实的 React 性能排查过程,揭示一个常被忽视却致命的问题,并给出清晰的优化建议。适合正在做仪表盘、大列表滚动的你参考。

问题现象:图表一多,滚动就“卡成 PPT”

我们的页面结构非常常见:多个图表组件组成一个仪表盘页面,用户通过滚动浏览全部内容。

const Dashboard = () => {
  const [graphs, setGraphs] = useState([]);
  const [graphsVisibleMap, setGraphsVisibleMap] = useState({});

  return (
    <div>
      {graphs.map((graph) => (
        <GraphComponent
          key={graph.id}
          data={graph}
          onVisibilityChange={handleGraphVisibleChange}
        />
      ))}
    </div>
  );
};

问题: 图表数量稍多(超过 20 个)后,滚动操作变得极其卡顿,连滚动条都不丝滑了。

初步推理:怀疑是 IntersectionObserver 惹的祸

使用了 IntersectionObserver 来监听每个图表是否在视口中,决定是否触发数据加载。

setupIntersectionObserver() {
  this.observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      const graphId = entry.target.getAttribute('data-graph-id');
      if (graphId) {
        this.props.onGraphVisibleChange(Number(graphId), entry.isIntersecting);
      }
    });
  }, {
    threshold: 0.1
  });
}

因为可见性在滚动时频繁变化,怀疑是 Observer 的回调频繁触发,导致主线程阻塞。

❌优化误区:尝试了这些“看似正确”的方法

尝试 1:提升触发门槛

threshold: 0.5,
rootMargin: '0px',

减少回调触发频率,没啥明显效果。

尝试 2:回调加防抖

_.debounce((entries) => {
  // 处理逻辑
}, 100);

也无效,因为滚动时变化太频繁,节流无法兜住。

尝试 3:批量处理变化

const visibilityBuffer = new Map();

this.observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    const id = entry.target.getAttribute('data-graph-id');
    visibilityBuffer.set(id, entry.isIntersecting);
  });

  setTimeout(() => {
    this.processBatchedUpdates(visibilityBuffer);
  }, 50);
});

依旧卡顿,说明根源不在 Observer 本身。

真正的问题:setState 的滥用

在图表组件中,调用了如下方法:

handleGraphVisibleChange = (id, visible) => {
  const newMap = { ...this.state.graphsVisibleMap };
  newMap[id] = visible;
  this.setState({ graphsVisibleMap: newMap }); // 滚动时频繁触发
};

关键问题:

  • 每次可见性变化都调用 setState
  • 滚动时每秒触发 几十甚至上百次
  • 每次都导致组件重渲染,主线程频繁阻塞
  • 最终导致滚动卡顿甚至页面假死

最终方案:把“高频数据”移出 state!

如果某些数据只是用于临时逻辑判断、并不影响 UI 渲染,那就不应该放进 React 的 state 中。

graphsVisibleMapstate 移到了组件的实例属性中:

class Dashboard extends React.Component {
  constructor() {
    super();
    this.graphsVisibleMap = {}; // 实例属性,非响应式
  }

  handleGraphVisibleChange = (id, visible) => {
    this.graphsVisibleMap[id] = visible; // 不再触发 setState
  };

  render() {
    return (
      <div>
        {this.props.graphs.map((graph) => (
          <GraphComponent
            key={graph.id}
            data={graph}
            onVisibilityChange={this.handleGraphVisibleChange}
          />
        ))}
      </div>
    );
  }
}

结果:页面滚动顺滑,性能恢复,问题彻底解决!

总结:不是所有数据都需要放在 state 中!

类型是否放入 state是否引发重渲染示例
影响 UI 的数据✅ 必须放✅ 会触发图表数据、用户输入
高频变化但不影响 UI 的逻辑状态❌ 不应该放❌ 不触发元素可见性、滚动缓存

你是否也遇到过类似的性能问题?欢迎在评论区聊聊你踩过的坑,或者有其他优化建议也欢迎提出!