Skip to content

第 23 章 · 浏览器原理 · 高频面试题

这一章是面试压轴。第 1 题"输入 URL 到页面渲染"几乎每场必问,必须背到能脱口而出 3 分钟


1. 从输入 URL 到页面渲染,发生了什么?(必背!)

:分 5 个阶段。

阶段一:URL 解析 + 缓存检查

  1. 检查输入是 URL 还是搜索词
  2. 检查 HSTS 列表,如果命中强制升级 HTTPS
  3. 查浏览器强缓存,命中直接渲染(不发请求)

阶段二:DNS 解析(域名 → IP)

浏览器缓存 → 系统缓存 → hosts → 本地 DNS → 根域名 → 顶级域名 → 权威域名

阶段三:建立连接

  • TCP 三次握手(SYN → SYN+ACK → ACK)
  • 如果是 HTTPS:再做 TLS 握手(ClientHello → ServerHello → 证书验证 → 密钥协商)
  • HTTP/3 用 QUIC,跳过 TCP 改 UDP,一个 RTT 就完成

阶段四:发送请求 / 接收响应

  • 发送 HTTP 请求(行 + Headers + Body)
  • 服务器处理后返回 Response
  • 浏览器边接收边解析(流式)

阶段五:浏览器渲染

  1. 解析 HTML:构建 DOM 树
  2. 解析 CSS:构建 CSSOM 树
  3. 合并:得到 Render Tree(去掉 display:none 节点)
  4. Layout:计算每个节点的几何信息
  5. Paint:把节点画到图层(绘制指令)
  6. Composite:GPU 合成图层,输出像素
  7. JS 执行(可能阻塞渲染)
  8. 触发 DOMContentLoadedload

完成后四次挥手关闭 TCP 连接(HTTP/1.1 keep-alive 时复用)。


2. 浏览器为什么要采用多进程架构?

优势说明
稳定性一个 tab 崩溃不影响其他 tab;插件挂了不影响浏览器
安全性沙箱隔离,渲染进程没有文件系统权限,避免恶意代码逃逸
性能多核 CPU 真并行;减少 GC 互相干扰

代价:内存开销大——每个进程有独立的 V8 实例、独立的内存。所以 Chrome 总被吐槽吃内存。

进一步的 Site Isolation(2018+):不同 site 的 iframe 也分到不同渲染进程,防御 Spectre 漏洞。


3. 回流(Reflow)和重绘(Repaint)的区别?

类型触发阶段代价
回流几何属性变化(宽高、位置、display)Layout → Paint → Composite最贵
重绘仅外观变化(color、bg、visibility)Paint → Composite中等
合成transform / opacity仅 Composite最便宜

避免回流的技巧

  1. 批量修改:用 cssText / 加 class
  2. 脱离文档流后修改:display:none → 改 → display:block
  3. DocumentFragment 批量插入
  4. 动画用 transform/opacity,走合成层
  5. 避免在循环里读 offsetXxx(强制同步布局)

4. 什么是合成层?怎么触发?有什么收益和代价?

合成层:浏览器把某些元素提为独立的图层,单独由 GPU 合成,避免动一下就回流整个页面。

触发方式

  • transform: translateZ(0) / translate3d(0,0,0)(hack,老兼容写法)
  • will-change: transform / opacity(推荐)
  • <video> / <canvas>
  • position: fixed(多数情况)
  • 动画进行中的 opacity

收益:动画跑在合成阶段,不进入 Layout/Paint,60fps 流畅。

代价

  • 吃显存(每层一个位图)
  • 创建合成层本身有开销

最佳实践:动画开始前will-change动画结束改回 auto,避免长期占显存。


5. 详细说说事件循环(Event Loop)。

JS 是单线程的,靠事件循环实现异步。

主循环每一圈

1. 取一个宏任务执行
2. 执行完后清空整个微任务队列(如果微任务里又产生微任务,继续清)
3. 执行 requestAnimationFrame 回调(如果到了渲染时机)
4. 渲染:style → layout → paint → composite
5. requestIdleCallback(如果还有空闲时间)
6. 回到 1

宏任务setTimeout / setInterval / MessageChannel / I/O / UI 事件 / setImmediate(Node)

微任务Promise.then / MutationObserver / queueMicrotask

经典题

js
console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
console.log(4);
// 输出:1, 4, 3, 2

为什么这样设计:微任务能够"在用户感知之前"清理副作用(比如 Vue 的 nextTick),而宏任务是渲染的时机点。


6. setTimeout(fn, 0) 真的会立刻执行吗?

不会

  1. HTML5 规范:嵌套层数 ≥ 5 时最小为 4ms。
  2. 即使是 0,也要等当前同步代码 + 所有微任务执行完。
  3. 后台 tab 中 setTimeout 会被节流到至少 1000ms 一次。
  4. 浏览器渲染优先级高于 setTimeout,可能被推迟。

更精确的"下一帧前执行":用 requestAnimationFrame。 更精确的"立即微任务":用 queueMicrotask


7. requestAnimationFramerequestIdleCallback 区别?

API何时执行用途
requestAnimationFrame下一帧渲染流畅动画、统一 DOM 测量
requestIdleCallback一帧渲染完后剩余空闲时间(最多约 50ms 一次)非紧急任务(埋点、空闲预取)
js
// 60fps 动画
function tick() {
  el.style.transform = `translateX(${x++}px)`;
  requestAnimationFrame(tick);
}
requestAnimationFrame(tick);

// 空闲时分批上报
requestIdleCallback(deadline => {
  while (deadline.timeRemaining() > 0 && q.length) {
    sendOne(q.shift());
  }
});

注意

  • requestAnimationFrame 在后台 tab 会停。
  • requestIdleCallback 不可靠(Safari 一直没有),生产慎用,或用 scheduler.postTask 替代。
  • React Fiber 的"时间切片"早期用过 requestIdleCallback,后来改用 MessageChannel

8. deferasync 的区别?

html
<script src="a.js"></script>           <!-- 默认:阻塞 HTML 解析 -->
<script src="a.js" async></script>     <!-- 并行下载,下完立即执行(仍可能打断 HTML 解析) -->
<script src="a.js" defer></script>     <!-- 并行下载,DOM 解析完后按顺序执行 -->
<script type="module" src="a.js"></script> <!-- 默认 defer,且类型 module -->
HTML 解析 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━► DOMContentLoaded
普通      [下载][暂停 HTML][执行]
async        [   下载   ][执行]      (执行时仍可能阻塞 HTML)
defer        [    下载    ]              [按序执行]→ DOMContentLoaded

经验法则:

  • 首屏关键 JS → 内联或 defer
  • 第三方统计/广告async
  • 现代项目入口<script type="module" defer>(默认就是 defer)

9. 跨标签页通信有哪些方式?分别适用什么场景?

方式同源限制持久适用
localStorage 事件同源持久古老兼容方案
BroadcastChannel同源临时现代首选,多 tab 群聊
postMessage跨源 ✅临时iframe / 弹出窗口
SharedWorker同源临时多 tab 共享后台任务/状态
Service Worker同源持久离线 / 推送 / 消息中转

注意

  • localStorage 事件同 tab 不触发,只在其他 tab 触发。
  • postMessage 必须校验 e.origin,不然存在 XSS 风险。
  • BroadcastChannel 简单到爆,新项目首选。

10. 浏览器渲染中,CSS 会阻塞 DOM 解析吗?JS 呢?

资源阻塞 HTML 解析?阻塞渲染?
HTML——自身才能渲染
CSS不阻塞 DOM 构建阻塞渲染(要等 CSSOM 完)
JS(普通)阻塞(边下边停)阻塞
JS(async)不阻塞下载,执行可能阻塞同上
JS(defer)不阻塞不阻塞,DOM 完成后执行

关键细节:JS 执行可能要读 CSSOM(如 getComputedStyle),所以 CSS 也会间接阻塞 JS 执行

最佳实践:CSS 放 <head>,JS 放 </body> 前 或加 defer


11. (进阶)GPU 加速的本质是什么?什么时候反而会更慢?

本质:把元素提升为独立合成层,由 GPU 用纹理(texture)合成,跳过 CPU 的 Layout/Paint 过程。transformopacity 动画因此能 60fps。

反而更慢的情况

  1. 大量元素都加 will-change:每层都要分配显存,移动端显存有限直接崩。
  2. 图层频繁创建/销毁:层的创建本身要付出 Paint 成本。
  3. 超大尺寸合成层:手机上一个全屏 4K 视频层 = 几十 MB 显存。
  4. 改了"能阻塞合成"的属性:例如改 border-radius 仍然要触发 Paint。

结论will-change 是手术刀不是大锤,只在真正动画的元素上、动画期间使用。


12. (进阶)什么是强制同步布局(Layout Thrashing)?

读取布局信息(如 offsetWidth)会立即触发 Layout,因为浏览器要给你最新的值。 如果你"读 → 写 → 读 → 写"循环交替,每次读都强制 Layout,性能直接崩溃。

js
// ❌ 错误:循环里每次读 offsetHeight 都触发 Layout
for (let i = 0; i < items.length; i++) {
  items[i].style.height = items[i].offsetHeight + 10 + 'px';
}

// ✅ 正确:先全部读,再全部写
const heights = items.map(i => i.offsetHeight);
items.forEach((it, i) => it.style.height = heights[i] + 10 + 'px');

强制同步触发布局的属性/方法(节选): offsetTop/Left/Width/Height / clientTop/... / scrollTop/... / getBoundingClientRect() / getComputedStyle() / window.innerWidth(部分情况)


一句话总结

浏览器面试套路:输入 URL(5 阶段)→ 多进程架构 → 渲染流水线(DOM/CSSOM/Render Tree/Layout/Paint/Composite)→ 回流重绘合成 → 事件循环(宏微任务 + rAF + idle)——每一题都能从这条主线上找到位置。