JavaScript 究竟怎么跑

那天深夜,我把一段看上去平平无奇的订单状态更新函数丢进预生产,结果线上 CPU 一路飙红、页面直接卡死。 事后回溯,我们才意识到:看似同步几毫秒就能跑完 的代码,在事件循环的显微镜下其实是一条彻底堵住主线程的巨蟒。

下面我就用最贴近业务的例子,带你走完这段"JavaScript 究竟怎么跑"的完整流水线,并给出我们后来能秒级止血的三板斧。


问题场景:一条把 10 万条订单一次性算完的"自杀"代码

js 复制代码
// 🔍 这段循环在测试环境只有 200 条订单,表现良好
export function bulkCalcFee(orders) {
  return orders.map(o => {             // ①
    let fee = 0;
    for (let sku of o.skus) {          // ②
      fee += sku.price * sku.qty * complexTax(sku.category);
    }
    return { id: o.id, fee };
  });
}
  • 行①:Array.map 是同步的,它会把 10 万次complexTax 一股脑压进调用栈 ,主线程寸步难行[1]。
  • 行②:complexTax() 本身又包含 4 层if-else,栈帧越叠越高,堆内存跟着膨胀。

解决方案:把重计算切成"宏任务"+"空闲切片"

我们做的第一件事,就是把每 256 条订单拆成一个 chunk,然后借助浏览器/Node 里的事件循环让出控制权。

js 复制代码
// 🔍 chunkSize 根据经验值在浏览器里 16~256 效果最佳
export async function bulkCalcFeeAsync(orders, chunkSize = 256) {
  const results = [];
  for (let i = 0; i < orders.length; i += chunkSize) {
    const slice = orders.slice(i, i + chunkSize);
    results.push(...slice.map(calcSingle));   // 同步计算一小块
    await new Promise(setTimeout);            // 把余下推到下一轮宏任务
  }
  return results;
}
  • 解释第 8 行:await new Promise(setTimeout) 不是"延迟 0 ms",而是在宏任务队列里插入一个空任务 ,让浏览器/Node 有机会去清空微任务、更新 UI,随后再拉取下一轮 chunk,从而把一次 200 ms 的卡顿拆成 50 次 4 ms 的微抖动

原理剖析:同步海啸→宏任务大坝→微任务分洪

把上面的修复逻辑画成文字版时序图,就能看清事件循环的三层闸门:

lua 复制代码
        ┌-----------┐
同步代码 │ calcSingle│
一次性    │ (4 ms)    │
跑完      └-----┬-----┘
                ▼      <-- 调用栈瞬间拉满
        ┌---------------┐
宏任务    │ setTimeout(fn,0)│
缓冲      └-----┬-----------┘
                ▼      <-- 让出主线程,界面可滑动
        ┌---------------┐
微任务    │ 更新进度条    │
插队      └-----┬-----------┘
                ▼
        浏览器重渲染
  • 微任务队列总是在当前宏任务末尾一次性清空 ,因此进度条动画不会因为多次宏任务而闪烁;这也是我们把进度回调 放到 queueMicrotask 而不直接丢 setTimeout 的原因。

应用扩展:三种可落地的"任务分片"战术

场景 战术 关键 API / 配置
浏览器端列表渲染 切片 + requestIdleCallback await new Promise(requestIdleCallback)
Node 端批量查询 使用 Node 自带 setImmediate setImmediate(() => nextChunk())setTimeout 少一次 timer 阶段
Web Worker 大计算 真·多线程 new Worker('./heavy.js')整块计算挪走,主线程 0 ms

举一反三:三个变体,照着就能抄

  1. 搜索联想 每次用户按键盘就触发 200 ms 的拼音匹配? → 用 AbortController 在上一个宏任务未完成时直接取消,减少重复计算。

  2. Excel-like 表格公式自动重算 公式链可能深度 20 层? → 把依赖拓扑转成 DAG,每个节点用微任务更新,让 UI 先刷出绿色"计算中"图标。

  3. Canvas 实时渲染热力图 10 万个坐标点? → ImageData 拆成 1 k 点一组,利用 requestAnimationFrame浏览器每次重绘前只刷一部分像素,肉眼根本看不出分段。


最后我们给那段"肇事函数"加了道保险:在编译阶段接入 types/bundlesize 把循环次数写进类型注解,一旦测试用例出现 >5000 条数据直接 Type Error,提前把性能雷埋掉------这比上线后回滚便宜太多

JavaScript 不是"跑得慢",而是"把异步写成同步就会堵"。事件循环把一切都准备好了,缺的是我们给主线程留的那口喘息。

相关推荐
心.c3 分钟前
深拷贝浅拷贝
开发语言·前端·javascript·ecmascript
IT_陈寒32 分钟前
Vue 3.4性能优化实战:5个鲜为人知的Composition API技巧让打包体积减少40%
前端·人工智能·后端
前端九哥37 分钟前
💻【急招!27届前端实习生】广州4399实习太幸福了!江景+三餐+健身房全都有😭
前端·面试·招聘
咖啡の猫43 分钟前
Vue全局事件总线
前端·javascript·vue.js
Lovereo1 小时前
我的目标检测性能优化之路:预算不够、GPU 没有、但性能我得要
前端
T___T1 小时前
JavaScript 变量声明详解:var、let、const 的核心差异
javascript·面试
蒙娜丽宁1 小时前
Rust 与 WebAssembly:构建高效前端应用的全流程复盘
前端·rust·wasm
这儿有一堆花1 小时前
使用 Actix-web 开发高性能 Web 服务
前端·数据库
豆苗学前端1 小时前
10分钟带你入门websocket,并实现一个在线多人聊天室
前端·javascript·后端
白水清风1 小时前
Vue3之渲染器
前端·vue.js·面试