主题
第 23 章 · 浏览器原理 · 高频面试题
这一章是面试压轴。第 1 题"输入 URL 到页面渲染"几乎每场必问,必须背到能脱口而出 3 分钟。
1. 从输入 URL 到页面渲染,发生了什么?(必背!)
答:分 5 个阶段。
阶段一:URL 解析 + 缓存检查
- 检查输入是 URL 还是搜索词
- 检查 HSTS 列表,如果命中强制升级 HTTPS
- 查浏览器强缓存,命中直接渲染(不发请求)
阶段二:DNS 解析(域名 → IP)
浏览器缓存 → 系统缓存 → hosts → 本地 DNS → 根域名 → 顶级域名 → 权威域名
阶段三:建立连接
- TCP 三次握手(SYN → SYN+ACK → ACK)
- 如果是 HTTPS:再做 TLS 握手(ClientHello → ServerHello → 证书验证 → 密钥协商)
- HTTP/3 用 QUIC,跳过 TCP 改 UDP,一个 RTT 就完成
阶段四:发送请求 / 接收响应
- 发送 HTTP 请求(行 + Headers + Body)
- 服务器处理后返回 Response
- 浏览器边接收边解析(流式)
阶段五:浏览器渲染
- 解析 HTML:构建 DOM 树
- 解析 CSS:构建 CSSOM 树
- 合并:得到 Render Tree(去掉
display:none节点) - Layout:计算每个节点的几何信息
- Paint:把节点画到图层(绘制指令)
- Composite:GPU 合成图层,输出像素
- JS 执行(可能阻塞渲染)
- 触发
DOMContentLoaded→load
完成后四次挥手关闭 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 | 最便宜 |
避免回流的技巧:
- 批量修改:用
cssText/ 加 class - 脱离文档流后修改:
display:none→ 改 →display:block - 用
DocumentFragment批量插入 - 动画用
transform/opacity,走合成层 - 避免在循环里读
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) 真的会立刻执行吗?
答:不会。
- HTML5 规范:嵌套层数 ≥ 5 时最小为 4ms。
- 即使是 0,也要等当前同步代码 + 所有微任务执行完。
- 后台 tab 中
setTimeout会被节流到至少 1000ms 一次。 - 浏览器渲染优先级高于
setTimeout,可能被推迟。
更精确的"下一帧前执行":用 requestAnimationFrame。 更精确的"立即微任务":用 queueMicrotask。
7. requestAnimationFrame 和 requestIdleCallback 区别?
答:
| 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. defer 和 async 的区别?
答:
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 过程。transform 和 opacity 动画因此能 60fps。
反而更慢的情况:
- 大量元素都加
will-change:每层都要分配显存,移动端显存有限直接崩。 - 图层频繁创建/销毁:层的创建本身要付出 Paint 成本。
- 超大尺寸合成层:手机上一个全屏 4K 视频层 = 几十 MB 显存。
- 改了"能阻塞合成"的属性:例如改
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)——每一题都能从这条主线上找到位置。