因为专注所以专业
助力成长与创新,汇集前沿前端开发观点

2026年了,React 后台上午还挺顺,下午进几趟详情页就卡,重启浏览器又好了,是监听和定时器没还回去吗?

2026年11月1日 阅读:65

先说结论:2026 年在 React 项目里,页面进出几趟后越用越卡、重启浏览器又恢复正常,多数不是路由或框架变慢,而是组件之外的几类资源没随卸载一起回收:window 与 document 上的事件监听、定时器与动画帧、IntersectionObserver 和 ResizeObserver、WebSocket 与轮询订阅、三方图表地图实例。React 只负责把组件从树上摘下来,既不会替你 removeEventListener,也不会替你 clearInterval;回调里的闭包又把旧 state 和 DOM 拽住,进出一次叠一层。

所以排查入口不是「先换个虚拟列表试试」,而是数清楚每进出一次,全局上多留了什么。

为什么组件卸载了,卡顿还在累加

React 卸载组件时断开的是 React 内部的引用关系。挂在 window 上的监听仍在,定时器仍在跑,回调闭包把原来的数据一起留住,这块内存就回收不掉。单次泄漏通常感知不到,真正放大代价的是后台、管理端这类「列表进详情、详情再返回」的高频路径:同一个人一天进出几十趟,成本线性叠加,表现往往不是报错,而是滚动发涩、输入延迟、切页掉帧。这也是很多人先怀疑数据量大,最后却查出监听数量在涨的原因。

  • window/document 事件:resize、scroll、keydown、visibilitychange、postMessage
  • 定时器与动画:setInterval 轮询、递归 setTimeout、requestAnimationFrame 循环
  • Observer:IntersectionObserver、ResizeObserver、MutationObserver
  • 连接与实例:WebSocket、EventSource、三方图表 / 地图 / 播放器

排查怎么落地:先清点,再拿数字说话

按资源类型分格,而不是按页面列,是因为这四类的解绑方式、出错方式和验证工具都不一样;按页面列容易漏,按类型列可以逐项核对。

  1. 清点:在组件文件里搜 addEventListener、setInterval、new Observer、new WebSocket、new 三方实例,把每个 useEffect 对外挂了什么写成一张清单。
  2. 配对:有挂必须有解,且挂与解必须是同一个函数引用,匿名函数解不掉。
  3. 反复进出验证:同一路径进出 5 到 10 次(经验区间),观察 JS 堆内存和 DOM 节点数是否阶梯式上涨。
  4. 定口径:返回列表页后堆内存大致回落到进入前加 10% 上下(经验区间),getEventListeners(window) 的数量不随进出次数增加。

第 2 步最容易返工:代码里其实写了 return,但解绑时传的是另一个新写的箭头函数,浏览器只认引用,结果等同于没解绑。第 3 步常被省掉,却是唯一能证明改对的动作,建议直接写进自测清单。

几类高频坑,多半是同一类错

  • 解绑传了新函数:挂载时 addEventListener('resize', 箭头函数),卸载时又写了一个,二者不是同一引用。
  • 依赖数组放对象:依赖里放了每轮渲染新建的对象或内联函数,挂载与解绑反复交替,还容易夹带重复请求。
  • 只消警告不治根:用 mounted 标志位压下 setState 警告,监听和定时器照旧留着。
  • 把 StrictMode 当泄漏:开发环境挂载执行两遍,看到日志多打一次就以为漏了,生产构建并不如此。
  • 三方实例只清 DOM:容器内容清空了,却没调 destroy 或 dispose,内部监听和定时器还在跑。

这些坑的共同点是「看起来处理了」。判断是否真处理,不看有没有写 return,看进出验证的数字。

手写清理、抽 hook、用库自带 API,怎么取舍

  • 手写清理:每处 useEffect 直接 return,单页改动量常见半小时到两小时(经验区间);适合页面少、资源类型单一的项目,缺点是新人容易漏。
  • 抽自定义 hook:例如 useEventListener、useInterval,一次性成本大致 1 到 2 人日(经验区间);适合多人协作、同类页面有十来个以上的中后台,缺点是封装太厚会让调试栈变长。
  • 用库自带订阅 API:图表、地图、播放器优先用官方 destroy;适合三方依赖重的页面,代价是版本升级时要重新核对卸载接口。

边界很清楚:页面只有一两处、且一年不动,手写就够;同类资源反复出现,抽 hook 的收益才明显;凡涉及三方实例,优先用官方卸载接口,不要自己猜。

交付现场:一次典型的返工

常见约束是这样的:后台模块预算固定、周期两周、素材和接口并行给,前端只能先保功能自测通过。当时的做法是页面能正常进出、控制台不报错就算过,多次进出的内存曲线没人看。上线一周后客服反馈某数据列表越滚越卡,排查两天才定位到详情页的 ResizeObserver 与图表实例没有销毁。代价是多花约 1 到 2 人日返工(经验区间),还顺延了一次小版本。现在这类页面会在验收清单里加一条:同一路径进出 10 次,堆内存不呈阶梯上涨,监听数量不随次数增加。

适用场景与边界

值得优先处理的是:单页应用的后台与管理端、长会话停留页面、含图表地图播放器的页面、有轮询或 WebSocket 的数据页。它们的共同特征是同一组件会被反复挂载。

不必大动的情况也要说清:整站只有一两个页面、组件挂载后不再卸载的;纯静态展示页;一次性活动落地页。把 useEffect 全部重写一遍,收益通常抵不上改动风险。判断标准是「同一组件会不会被反复挂载」:会,就必须配对写清理;不会,先按功能交付。

常见问题

React 开发环境下 useEffect 跑了两遍,是清理函数写错了吗?

不是。StrictMode 在开发环境会刻意挂载、卸载、再挂载一次,用来暴露没写清理的代码,生产构建不会这样重复执行。

明明写了 return 解绑,监听数量还是在涨,问题常在哪?

多半是挂载和解绑不是同一个函数引用,两边各写了一个箭头函数;也可能是依赖数组里放了每轮新建的对象,导致挂载与解绑反复交替。

不切路由,只在同一个页面里开关弹窗,也会漏吗?

会。条件渲染、弹窗反复开关、给组件换 key 强制重建,都会触发卸载,挂在组件外的监听和定时器一样要清。

图表、地图这类组件,卸载时把容器清空还不够吗?

不够。清空容器只移了 DOM,实例内部的监听、定时器和绘图上下文还在,一般要调官方提供的 destroy 或 dispose。


下次再遇到上午顺、下午卡,先按资源清单核对一遍,再用反复进出的数字说话,别急着上虚拟列表或换框架。此法适用于会反复挂载的组件;一次性挂载的静态页面,按功能交付即可。

有类似的项目需求?
联系我们,获取一对一项目参考方案
获取方案
对这个话题感兴趣?
10 年技术团队,24 小时内出具参考方案
获取方案
准备好开始了吗,
那就与我们取得联系吧!
13370032918
了解更多服务,随时联系我们
请填写您的需求
您希望我们为您提供什么服务呢
您的预算
联
系
微信二维码
扫码添加客服微信
专业对接各类技术问题
联系电话
13370032918 (金经理)
电话若占线或未接到、就加下微信
联系邮箱
349077570@qq.com
提交成功
感谢您的信任,我们会尽快与您联系!
为您推荐以下案例