Appearance
速查思路
立刻尝试“强行暂停”拿到调用栈
- 按 F12 打开 DevTools(它是独立进程,常常能打开)。
- 点击右上角的 “暂停脚本执行 (||)”。若是无限循环/长任务,会立即停在当前语句,直接看到 Call Stack 和责任文件行号。
- 在 Sources 面板点
{ }美化代码,展开 Call Stack 的最顶层几帧,基本就能指向肇事函数或第三方库入口。
DevTools 打不开时
- 打开 Chrome 任务管理器(
Shift + Esc)看哪个标签页 CPU/内存飙高,先确认“真凶”页。 - macOS:活动监视器 → 选中 “Google Chrome Helper (Renderer)”(对应卡死标签)→ 齿轮 → 采样进程。堆栈里若能看到你项目函数名/第三方库符号,即可反查。
- Windows:任务管理器 → 选中对应 Chrome 渲染进程 → 创建转储文件(或用 WPR/WPA 临时采样),用于随后分析。
- 打开 Chrome 任务管理器(
重载采样拿证据
- DevTools → Performance → 勾选 “Record during page load” → 点 ● 录制,然后刷新页面复现。看 Main 线程是否被 Long Task(>50ms)占满,展开 Bottom-Up/Call Tree 找出最重函数。
- Memory 面板做 Heap Snapshot 对比(前/后),判断是否对象激增、监听器泄漏等。
判因速表
- 无限
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 分批挂载子项操作清单(按优先级)
- 打开 DevTools → 点暂停 (||) → 看 Call Stack 定位行号/文件。
- DevTools → Performance:录制一次“刷新加载”的火焰图,找 Long Task 的最重函数。
- DevTools → Memory:快照前后对比是否有对象/监听器激增。
- 若 DevTools 无法打开:用 Shift+Esc(Chrome 任务管理器)+ 系统采样(macOS Sample / Windows Dump)确认“谁在忙”。
- 在代码中加入 Long Task 监控、错误收集、紧急刹车/分片。下次即使没开 DevTools,也能在控制台或日志里还原现场。
小结
- “暂停拿栈”是最快的突破口:很多看似“卡死”的问题,暂停一下就直指那行循环/递归/渲染环。
- 证据优先:Performance 火焰图 + Memory 快照,能在“主观卡顿”与“客观瓶颈”之间拉齐认知。
- 预防为主:把上面的“黑匣子”片段常驻到项目启动路径里,后续定位会轻松很多。
如果你告诉我当前项目是 Vue 还是 React/Next、以及卡住前的最近改动点,我可以按你的栈直接给出更贴合的守卫/分片插入位置与最小修改片段。