目录一、先建立认知框架二、浏览器的线程架构浏览器进程模型三、为什么必须互斥DOM 的线程安全问题核心矛盾四、JS 对 HTML 解析内容的依赖document.write 的极端案例JS 读取 DOM 的即时性要求五、CSSOM 的额外影响六、Web Worker真正的并行 JS七、这个设计的工程影响长任务Long Task问题预解析器Preload Scanner的补偿八、两个面试回答模板 高分模板展现系统性 工程深度 简答模板30 秒快速作答版九、面试官常见追问这道题考察的是浏览器底层线程模型能答好的关键在于不只说因为 JS 可以操作 DOM而是要讲清楚浏览器线程架构、渲染线程和 JS 引擎线程为什么互斥、互斥的根本原因是什么、以及这个设计带来的工程影响这才是让面试官眼前一亮的回答方式。一、先建立认知框架JS 阻塞 HTML 解析根本原因不是技术限制而是设计选择——浏览器让渲染线程和 JS 引擎线程互斥运行两者永远不会同时工作这是为了保证 DOM 操作的线程安全。浏览器主线程的工作 HTML 解析构建 DOM 样式计算 布局 绘制 JS 执行 这些任务都跑在同一个主线程上天然串行 JS 执行时其他任务自然暂停二、浏览器的线程架构浏览器进程模型Browser 进程主进程 ↓ Renderer 进程每个 Tab 一个核心 ├── 主线程Main Thread │ ├── HTML 解析 │ ├── CSS 解析 │ ├── JS 执行V8 引擎跑在这里 │ ├── 样式计算 │ ├── 布局Layout │ └── 绘制Paint ├── 合成线程Compositor Thread ├── 光栅化线程Raster Thread └── Worker 线程Web Worker 运行在这里关键点是HTML 解析和 JS 执行都在同一个主线程上这不是偶然是浏览器的架构设计。两者共享同一个执行上下文天然串行JS 执行时主线程被占用HTML 解析自然暂停。三、为什么必须互斥DOM 的线程安全问题核心矛盾HTML 解析器在构建 DOM 树 正在创建节点、建立父子关系、更新树结构 JS 引擎同时想修改 DOM 删除节点、插入节点、修改属性 → 两者同时操作同一棵 DOM 树 → 数据竞争Race Condition → DOM 树状态不一致结果不可预测一个具体的竞争场景// 假设 HTML 解析器正在处理 // ul // liitem 1/li ← 解析器刚创建这个节点 // JS 同时执行 const ul document.querySelector(ul); ul.innerHTML ; // 清空了 ul // 解析器接着要把 liitem 1/li 插入 ul // 但 ul 已经被清空了 // 接下来应该怎么办→ 状态不一致行为未定义DOM 不是线程安全的数据结构没有锁机制。如果允许并发访问需要给每个 DOM 操作加锁复杂度会急剧上升性能反而更差。浏览器选择了更简单、更高效的方案让 JS 和 HTML 解析互斥从根本上避免竞争。四、JS 对 HTML 解析内容的依赖document.write 的极端案例// JS 可以通过 document.write 向当前解析位置插入 HTML document.write(div动态插入的内容/div);如果允许并行 解析器解析到 script 时已经解析了后面的内容 JS 执行 document.write 插入了新 HTML 插入位置在哪已解析的内容和新内容如何合并 解析器的内部状态已经无效了必须重新处理 → 浏览器根本无法确定当前解析位置在哪 → 暂停解析是唯一合理的选择JS 读取 DOM 的即时性要求// 这段 JS 必须读到它之前的完整 DOM const count document.querySelectorAll(p).length; console.log(count); // 开发者期望这是一个确定的值如果解析和 JS 并行 解析器还在继续创建 p 节点 JS 执行 querySelectorAll 时 DOM 还在变化 count 的值是不确定的每次可能不同 → JS 对 DOM 的同步读取必须建立在 DOM 静止的基础上 → 暂停解析才能保证 DOM 在 JS 执行期间是静止的五、CSSOM 的额外影响JS 阻塞 HTML 解析CSS 还会进一步阻塞 JS 执行叠加起来影响更大。解析器遇到 script ↓ 先检查前面是否有未完成的 CSS 加载 ↓ 如果有 → 等待 CSS 下载并解析完成构建 CSSOM ↓ CSS 就绪后才执行 JS ↓ JS 执行完才恢复 HTML 解析为什么要等 CSS// JS 可能读取元素的计算样式 const height getComputedStyle(element).height; // 如果 CSS 还没加载完计算出来的样式是错的 // 浏览器必须等 CSS 就绪才能保证 getComputedStyle 的结果正确六、Web Worker真正的并行 JS浏览器不是不能并行执行 JSWeb Worker 就是并行的。但 Worker 线程不能访问 DOM这正好印证了互斥的根本原因是 DOM 的线程安全。// Worker 线程可以执行耗时计算不阻塞主线程 const worker new Worker(heavy-task.js); worker.postMessage({ data: largeData }); worker.onmessage (e) { // 计算完成把结果拿回主线程处理 document.getElementById(result).textContent e.data; }; // heavy-task.jsWorker 线程 self.onmessage (e) { // ✅ 可以做复杂计算 const result heavyComputation(e.data); // ❌ 不能访问 DOM // document.querySelector(...) → 报错document is not defined self.postMessage(result); };Worker 可以并行 ← 因为它不碰 DOM 主线程 JS 必须串行 ← 因为它要操作 DOM → 互斥的根本原因就是 DOM 的线程安全问题七、这个设计的工程影响长任务Long Task问题JS 执行时间过长超过 50ms 被定义为长任务 ↓ 主线程被占用HTML 解析暂停 ↓ 用户交互事件点击、滚动无法响应 ↓ 页面卡顿用户体验差解决方案// ① 任务切片用 setTimeout 把长任务拆成小块 function processLargeArray(array) { const chunk array.splice(0, 100); // 每次处理 100 条 processChunk(chunk); if (array.length 0) { setTimeout(() processLargeArray(array), 0); // 让出主线程 } } // ② 使用 requestIdleCallback在主线程空闲时执行 requestIdleCallback((deadline) { while (deadline.timeRemaining() 0 tasks.length 0) { processTask(tasks.shift()); } }); // ③ 使用 Web Worker把计算移出主线程预解析器Preload Scanner的补偿浏览器的优化 主线程被 JS 阻塞时 预解析器Preload Scanner同时扫描后续 HTML 提前发现需要下载的资源CSS、JS、图片 提前发起网络请求并行下载 效果 JS 执行完恢复解析时很多资源已经在下载了 减少了串行等待的时间八、两个面试回答模板 高分模板展现系统性 工程深度JS 阻塞 HTML 解析根本原因是浏览器的架构设计HTML 解析和 JS 执行都运行在同一个主线程上而且这个设计是故意的目的是保证 DOM 操作的线程安全。先说线程架构。浏览器的 Renderer 进程有一个主线程HTML 解析、CSS 解析、JS 执行、样式计算、布局、绘制都跑在这个主线程上。V8 引擎也运行在这个主线程里所以 JS 执行时主线程被占用HTML 解析自然暂停这是同一个线程内任务串行执行的天然结果。为什么不能并行因为 DOM 不是线程安全的数据结构。假设 HTML 解析器正在创建节点、建立 DOM 树JS 同时在删除节点、修改属性两者同时操作同一棵树就会产生数据竞争DOM 状态不一致结果不可预测。要支持并发就需要给每个 DOM 操作加锁复杂度急剧上升性能反而更差。浏览器选择了更简单的方案让 JS 和 HTML 解析互斥从根本上避免竞争。document.write 是另一个必须互斥的原因。JS 可以通过 document.write 向当前解析位置插入 HTML如果允许并行解析器根本无法确定当前解析位置在哪插入的内容该放哪也无法确定只能暂停解析才能保证 document.write 的行为有意义。CSS 还会进一步放大这个问题。JS 执行前浏览器要等前面的 CSS 加载并解析完因为 JS 可能调用 getComputedStyle 读取元素样式CSS 没就绪计算出来的样式值是错的。所以阻塞链是CSS 慢 → JS 等 CSS → HTML 解析等 JS → 渲染等 DOM层层叠加。Web Worker 印证了这个设计的本质。Worker 线程可以并行执行 JS不阻塞主线程但 Worker 完全不能访问 DOM。这正好说明阻塞的根本原因就是 DOM 的线程安全能访问 DOM 就必须串行不访问 DOM 就可以并行。工程影响上JS 执行时间超过 50ms 就会被定义为长任务导致用户交互事件无法响应页面卡顿。解决方案是任务切片用 setTimeout 或 requestIdleCallback 把长任务拆成小块让主线程定期有机会处理其他任务或者把纯计算移到 Web Worker主线程只处理 DOM 操作。浏览器也有预解析器作为补偿主线程被 JS 阻塞时预解析器提前扫描后续 HTML提前发起资源下载请求减少串行等待时间。 简答模板30 秒快速作答版JS 阻塞 HTML 解析根本原因是两者都运行在浏览器的同一个主线程上且这是故意设计的目的是保证 DOM 的线程安全。HTML 解析器构建 DOM 树JS 引擎同时可以增删改 DOM如果并行执行就会产生数据竞争DOM 状态不一致。DOM 没有锁机制浏览器选择互斥运行来从根本上避免竞争比加锁更简单高效。另外 JS 可以用 document.write 在当前解析位置插入 HTML并行的话解析器根本无法确定位置也必须暂停。Web Worker 可以验证这个结论Worker 线程可以并行执行 JS 不阻塞主线程但完全不能访问 DOM。能访问 DOM 就必须串行不访问 DOM 就可以并行这就是互斥的本质原因。工程上JS 执行超过 50ms 就是长任务会导致页面卡顿。解决办法是任务切片、requestIdleCallback或者把纯计算放到 Web Worker 里主线程只做 DOM 相关操作。九、面试官常见追问追问答题方向为什么 DOM 不加锁支持并发加锁复杂度高、性能差、容易死锁互斥更简单可靠Web Worker 为什么不能访问 DOMDOM 不是线程安全的允许 Worker 访问就会引发数据竞争document.write 现在还能用吗强烈不推荐会清空整个文档阻塞解析现代开发基本不用预解析器是什么主线程被 JS 阻塞时提前扫描后续 HTML 发起资源下载的优化机制什么是长任务如何优化超过 50ms 的 JS 任务用任务切片、requestIdleCallback、Web Worker 解决主线程上除了 JS 执行还有什么HTML 解析、CSS 解析、样式计算、Layout、Paint 都在主线程requestIdleCallback 和 setTimeout 有什么区别requestIdleCallback 在主线程空闲时执行setTimeout 是定时执行不感知主线程状态CSS 如何加剧 JS 对解析的阻塞JS 执行前要等 CSS 就绪CSS 慢则 JS 延迟执行进一步延迟 HTML 解析恢复