You need to enable JavaScript to run this app. React useEffect耗时计算阻塞渲染,select切换时loading不生效
优惠活动
大模型
产品
解决方案
定价
更多

React useEffect耗时计算阻塞渲染,select切换时loading不生效

问题成因
  • 核心本质:代码里的sleep是靠同步死循环写的长任务,只要跑起来就把JS主线程占得死死的,循环结束前啥别的事都干不了。浏览器渲染和JS执行共用一条主线程,主线程被堵的时候,所有待更新的DOM内容都堆着画不上去,用户自然看不到加载提示。
  • 按钮交互可正常显示加载提示的原因:点按钮触发click事件的时候,回调里的setIsLoading(true)和setA(a+1)会被React合并成一次调度。回调跑完之后,在进入useEffect跑sleep长任务之前,主线程是有空隙做完整渲染的,先把loading的文本画到屏幕上,之后才会被2秒的死循环卡住。用户自然能看到持续2秒的加载提示。
  • select交互无法正常显示加载提示的原因:切select选项触发change事件的时候,浏览器本身得先把原生控件的交互闭环走完:下拉面板收起来、把选中项标高亮、把控件的值更新成新选项。这整套原生流程,和咱们写的onChange回调、React计算DOM的过程、useEffect里的2秒sleep长任务,会被浏览器串成一整段连续的主线程任务,中间根本留不出渲染loading的空隙。等2秒死循环跑完,主线程终于空了,浏览器要把攒了两次更新的DOM一次性画上去:第一次是要显示loading,第二次是sleep跑完要把loading关掉。这俩更新挨得太近,刚画出来就被覆盖,用户根本察觉不到loading的存在。
修复方案
  • 最通用的改法就是给长任务的执行留个渲染的空隙,先把loading渲染完,再跑阻塞的长任务。可以在useEffect里用宏任务(比如setTimeout)把同步的长任务包一层,把长任务从当前的渲染周期里拆出去,对应修改后的代码:
useEffect(() => {
  // 延后到下一个宏任务再跑长任务,给浏览器留够画loading的时间
  const timer = setTimeout(() => {
    sleep(2000);
    console.log("finish");
    setIsLoading(false);
  }, 0);
  // 组件卸载的时候清掉定时器,避免内存泄漏
  return () => clearTimeout(timer);
}, [a]);

这么改完之后,不管是按钮还是select触发更新,都会先把loading的UI画上去,再进入长任务的阻塞阶段,加载提示就能稳定显示了。

  • 如果不想动长任务的执行逻辑,也可以在select的onChange回调里,把触发长任务的状态更新(也就是setA(a+1))延后到浏览器画完loading的下一帧,用requestAnimationFrame把两个逻辑的执行周期拆开:
onChange={() => {
  setIsLoading(true);
  // 等浏览器把loading渲染完,再触发依赖a更新的长任务
  requestAnimationFrame(() => {
    setA(a => a + 1);
  })
}}
  • 如果长任务本身计算量很大,更推荐把同步阻塞的计算丢到Web Worker里跑,完全不占主线程,从根上解决UI被卡住的问题。

小提醒:原代码里loading的拼写错成loding了,可以顺手改对。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.11.07 01:54:03