Skip to content

速查思路

  1. 立刻尝试“强行暂停”拿到调用栈

    • F12 打开 DevTools(它是独立进程,常常能打开)。
    • 点击右上角的 “暂停脚本执行 (||)”。若是无限循环/长任务,会立即停在当前语句,直接看到 Call Stack 和责任文件行号。
    • Sources 面板点 { } 美化代码,展开 Call Stack 的最顶层几帧,基本就能指向肇事函数或第三方库入口。
  2. DevTools 打不开时

    • 打开 Chrome 任务管理器Shift + Esc)看哪个标签页 CPU/内存飙高,先确认“真凶”页。
    • macOS:活动监视器 → 选中 “Google Chrome Helper (Renderer)”(对应卡死标签)→ 齿轮 → 采样进程。堆栈里若能看到你项目函数名/第三方库符号,即可反查。
    • Windows:任务管理器 → 选中对应 Chrome 渲染进程 → 创建转储文件(或用 WPR/WPA 临时采样),用于随后分析。
  3. 重载采样拿证据

    • DevTools → Performance → 勾选 “Record during page load” → 点 ● 录制,然后刷新页面复现。看 Main 线程是否被 Long Task(>50ms)占满,展开 Bottom-Up/Call Tree 找出最重函数。
    • Memory 面板做 Heap Snapshot 对比(前/后),判断是否对象激增、监听器泄漏等。
  4. 判因速表

    • 无限 for/while / 递归:暂停后停在循环体;变量无变化。
    • 同步重计算(大数组排序、JSON.parse、大量正则):Performance 里长紫块(Evaluate Script/Task)持续。
    • 同步布局抖动(读写交替导致反复 Recalculate Style / Layout):火焰图出现密集 Layout/Style
    • 大量 DOM 插入/重排重绘:Paint/Composite 占比巨大。
    • 状态环(React/Vue)反复 setState/assign:调用栈在同一组件/响应式回调来回出现。
    • 同步 XHR/阻塞 API:Network 里可见 (synchronous) 或主线程长时间等待。

事后加“黑匣子”——下次不用预开 DevTools 也有线索

下面都是可直接粘贴的小片段;涉及复杂逻辑的地方已在上一行写中文注释。

① 监控长任务(Long Task)并记录可归因信息

js
// 页面初始化时插入:记录长任务,复现场景时在控制台查看 window.__longTasks
const __longTasks = [];
const __po = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // 复杂:采集 longtask 的归因信息(可能包含脚本/容器来源)
    const attr = (entry.attribution || []).map((a) => ({
      name: a.name,
      entryType: a.entryType,
      containerType: a.containerType,
      containerName: a.containerName,
      containerSrc: a.containerSrc,
    }));
    __longTasks.push({ duration: entry.duration, attr, ts: performance.now() });
  }
});
__po.observe({ entryTypes: ["longtask"] });
window.__longTasks = __longTasks;

② 捕获未处理错误与 Promise 拒绝

js
// 复杂:把致命错误集中打印/上报,卡死前常能留下一条线索
window.addEventListener("error", (e) => {
  console.warn("[onerror]", e.message, e.filename, e.lineno, e.colno);
});
window.addEventListener("unhandledrejection", (e) => {
  console.warn("[unhandledrejection]", e.reason);
});

③ “紧急刹车”与让步点:防止循环把主线程锁死

js
// 复杂:三击 Shift 触发 __panic,循环里检测后可中断
let __panic = false,
  __hits = 0;
window.addEventListener("keydown", (e) => {
  if (e.key === "Shift") {
    __hits++;
    setTimeout(() => {
      __hits = 0;
    }, 800);
    if (__hits >= 3) {
      __panic = true;
      console.warn("[panic] ON");
    }
  }
});

// 复杂:在重循环中加入“让步点”和保险丝(async 场景)
async function heavyLoop(N) {
  let fuse = 0;
  for (let i = 0; i < N; i++) {
    if (__panic) break; // 紧急刹车
    if (++fuse % 1000 === 0) {
      // 每 1000 次让出主线程
      await Promise.resolve(); // 微任务间隙,避免长任务
    }
    // ... 原有循环体 ...
  }
}

④ 非 async 场景的分片执行(避免一次性处理巨量数据)

js
// 复杂:把超大任务按帧切片执行,避免长时间阻塞
function runChunked(items, fn, chunk = 1000) {
  let i = 0;
  function step() {
    const end = Math.min(i + chunk, items.length);
    while (i < end) {
      if (__panic) return; // 紧急刹车
      fn(items[i++]);
    }
    if (i < items.length) requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
}

⑤ DOM 批量插入(降低布局与重绘压力)

js
// 复杂:使用 DocumentFragment 批处理 DOM,再一次性挂载
function appendBatch(container, nodes) {
  const frag = document.createDocumentFragment();
  for (const n of nodes) frag.appendChild(n);
  container.appendChild(frag);
}

框架级常见“自卡死”与快速止血

React

jsx
// 复杂:避免 effect 初始化里 setState 触发无限渲染
const initedRef = useRef(false);
useEffect(() => {
  if (initedRef.current) return;
  initedRef.current = true;
  // setState(...) 仅首轮执行
}, []);
jsx
// 复杂:事件里频繁 setState 的列表操作,使用批量或节流
const onScroll = useMemo(() => {
  let ticking = false;
  return () => {
    if (!ticking) {
      ticking = true;
      requestAnimationFrame(() => {
        // setState(计算结果)
        ticking = false;
      });
    }
  };
}, []);

Vue 3

ts
// 复杂:watch 中写回同源数据引发环,增加更新守卫
const updating = ref(false);
watch(source, (v) => {
  if (updating.value) return;
  updating.value = true;
  target.value = compute(v);
  requestAnimationFrame(() => {
    updating.value = false;
  });
});
ts
// 复杂:大列表渲染建议启用虚拟列表或分片渲染
// 示例:在 nextTick 后用 requestAnimationFrame 分批挂载子项

操作清单(按优先级)

  1. 打开 DevTools → 点暂停 (||) → 看 Call Stack 定位行号/文件。
  2. DevTools → Performance:录制一次“刷新加载”的火焰图,找 Long Task 的最重函数。
  3. DevTools → Memory:快照前后对比是否有对象/监听器激增。
  4. 若 DevTools 无法打开:用 Shift+Esc(Chrome 任务管理器)+ 系统采样(macOS Sample / Windows Dump)确认“谁在忙”。
  5. 在代码中加入 Long Task 监控错误收集紧急刹车/分片。下次即使没开 DevTools,也能在控制台或日志里还原现场。

小结

  • “暂停拿栈”是最快的突破口:很多看似“卡死”的问题,暂停一下就直指那行循环/递归/渲染环。
  • 证据优先:Performance 火焰图 + Memory 快照,能在“主观卡顿”与“客观瓶颈”之间拉齐认知。
  • 预防为主:把上面的“黑匣子”片段常驻到项目启动路径里,后续定位会轻松很多。

如果你告诉我当前项目是 Vue 还是 React/Next、以及卡住前的最近改动点,我可以按你的栈直接给出更贴合的守卫/分片插入位置与最小修改片段

本站总访问