2026年了,React 后台上午还挺顺,下午进几趟详情页就卡,重启浏览器又好了,是监听和定时器没还回去吗?
先说结论: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、三方图表 / 地图 / 播放器
排查怎么落地:先清点,再拿数字说话
按资源类型分格,而不是按页面列,是因为这四类的解绑方式、出错方式和验证工具都不一样;按页面列容易漏,按类型列可以逐项核对。
- 清点:在组件文件里搜 addEventListener、setInterval、new Observer、new WebSocket、new 三方实例,把每个 useEffect 对外挂了什么写成一张清单。
- 配对:有挂必须有解,且挂与解必须是同一个函数引用,匿名函数解不掉。
- 反复进出验证:同一路径进出 5 到 10 次(经验区间),观察 JS 堆内存和 DOM 节点数是否阶梯式上涨。
- 定口径:返回列表页后堆内存大致回落到进入前加 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。
下次再遇到上午顺、下午卡,先按资源清单核对一遍,再用反复进出的数字说话,别急着上虚拟列表或换框架。此法适用于会反复挂载的组件;一次性挂载的静态页面,按功能交付即可。
-
2026年了,表格里点一下价格排序,10 块排到 2 块前面,是 sort 默认拿字符串在比吗?
日期:2026年10月31日 阅读:46
-
2026年了,弹窗里点“获取验证码”,整张表单也跟着提交了,是按钮默认 type 在作怪吗?
日期:2026年10月30日 阅读:32
-
2026年了,AI 把官网首页出得挺快,目标机型上一开就图片错位、按钮点不到,兼容和性能这摊还该前端收吗?
日期:2026年10月29日 阅读:66
-
2026年了,述职被问前端体验架构师和普通前端差在哪,我拿什么证明才算数?
日期:2026年10月28日 阅读:42
-
2026年了,活动页倒计时锁屏几分钟再打开就慢了一大截,用户截图问是不是我们少给了时间,问题出在哪?
日期:2026年10月27日 阅读:108




