浏览器原理
进程架构
Chrome 是多进程架构:
| 进程 | 职责 |
|---|---|
| 浏览器主进程 | 界面显示、用户交互、子进程管理 |
| 渲染进程 | 把 HTML/CSS/JS 变成页面。每个标签页通常一个(站点隔离) |
| GPU 进程 | 3D 绘制、合成 |
| 网络进程 | 网络请求 |
| 插件进程 | 插件运行 |
多进程的好处:一个标签页崩溃不影响其他;安全沙箱隔离;充分利用多核。代价是内存占用高。
渲染进程里的线程(这个更常考):
- GUI 渲染线程 —— 解析 HTML/CSS、布局、绘制
- JS 引擎线程 —— 执行 JS
- 事件触发线程 —— 管理事件队列
- 定时器线程 ——
setTimeout的计时(计时不由 JS 线程做,所以不会被同步代码阻塞计时,但到期后的回调要排队) - 异步 HTTP 请求线程
关键结论:GUI 渲染线程和 JS 引擎线程互斥。 JS 执行时渲染会暂停,这是长任务导致页面卡顿的根本原因。
🔥 渲染流程
HTML → 解析 → DOM 树
↓
CSS → 解析 → CSSOM 树 ──→ 合并 → 渲染树(Render Tree)
↓
Layout(布局/回流)—— 计算几何位置
↓
Paint(绘制)—— 生成绘制指令,分层
↓
Composite(合成)—— GPU 合成图层
几个关键细节:
CSS 阻塞渲染但不阻塞 DOM 解析 —— CSSOM 没构建完不会渲染(避免白屏闪烁),但 HTML 解析可以继续。所以 CSS 应放 <head>。
JS 阻塞 DOM 解析 —— 因为 JS 可能修改 DOM,浏览器必须等它执行完。所以 <script> 应放 </body> 前,或者用:
| 属性 | 下载 | 执行时机 |
|---|---|---|
| 无 | 阻塞解析 | 立即,阻塞 |
async | 并行 | 下载完立即执行(可能阻塞解析),顺序不保证 |
defer | 并行 | DOM 解析完、DOMContentLoaded 之前,按顺序执行 |
type="module" | 并行 | 等同 defer |
选型:有依赖关系或要操作 DOM 用 defer;完全独立的第三方脚本(统计、广告)用 async。
JS 执行前要等 CSSOM 构建完 —— 因为 JS 可能读取样式(getComputedStyle)。所以 <link> 在 <script> 之前时,脚本执行会被 CSS 下载阻塞。
🔥 重排与重绘
| 触发条件 | 代价 | |
|---|---|---|
| 重排 Reflow | 几何属性变化(宽高、位置、字体、显示隐藏、增删 DOM、窗口 resize) | 大 —— 要重新计算布局,且可能影响父子兄弟节点,之后必然重绘 |
| 重绘 Repaint | 只有外观变化(颜色、背景、阴影、visibility) | 中 |
| 只合成 | transform、opacity、filter | 小 —— 跳过布局和绘制,直接在合成线程做,不占主线程 |
所以动画要用 transform 和 opacity,不要用 left/top/width。这是前端动画性能的第一原则。
:::warning 强制同步布局(layout thrashing)
// ❌ 读写交替,每次读都强制浏览器立即重排
for (let i = 0; i < els.length; i++) {
els[i].style.width = els[i].offsetWidth + 10 + 'px'
}
// ✅ 先批量读,再批量写
const widths = els.map(el => el.offsetWidth)
els.forEach((el, i) => el.style.width = widths[i] + 10 + 'px')
原理:浏览器会把样式修改攒起来批量处理。但一旦读取 offsetWidth、offsetTop、scrollTop、getComputedStyle() 这类属性,浏览器必须立即重排才能给出准确值。读写交替就导致每次循环都重排一次。
这是「DOM 操作慢」最常见的真实原因,比「DOM 操作本身慢」更准确。 :::
减少重排的手段:用 class 一次性改多个样式而不是逐条改 style、用 documentFragment 批量插入、display: none 后批量操作再显示、用 transform 代替几何属性、对频繁变动的元素用 will-change 或提升为独立图层。
事件机制
三个阶段:捕获(window → target)→ 目标 → 冒泡(target → window)。
el.addEventListener('click', fn) // 冒泡阶段(默认)
el.addEventListener('click', fn, true) // 捕获阶段
el.addEventListener('click', fn, {once: true, passive: true, capture: false})
stopPropagation vs stopImmediatePropagation —— 前者阻止继续传播但同一元素上的其他监听器仍会执行;后者连同元素上的其他监听器也阻止。
preventDefault 阻止默认行为(链接跳转、表单提交),和传播无关。
passive: true 告诉浏览器「这个监听器不会调 preventDefault」,于是浏览器可以立即开始滚动而不必等监听器执行完。对 touchstart/wheel 事件能显著改善滚动流畅度。Chrome 对这些事件已默认 passive: true。
事件委托:
list.addEventListener('click', e => {
const item = e.target.closest('.item')
if (!item) return
// 处理
})
好处:减少监听器数量(性能和内存)、动态添加的子元素自动生效、不用手动解绑。
e.target vs e.currentTarget —— target 是实际触发事件的元素,currentTarget 是绑定监听器的元素。事件委托里用 target 找真实来源。
哪些事件不冒泡 —— focus/blur(用冒泡版的 focusin/focusout)、mouseenter/mouseleave(用 mouseover/mouseout)、load/error(资源加载错误要在捕获阶段监听)。
存储
| 容量 | 生命周期 | 随请求发送 | 用途 | |
|---|---|---|---|---|
| Cookie | 4KB | 可设过期 | ✅ 每次请求都带 | 会话标识、服务端需要读的状态 |
| localStorage | 5~10MB | 永久,需手动清 | ❌ | 长期本地数据 |
| sessionStorage | 5~10MB | 标签页关闭即清 | ❌ | 单次会话的临时数据 |
| IndexedDB | 几百 MB+ | 永久 | ❌ | 大量结构化数据、离线应用 |
| Cache API | 受配额限制 | 永久 | ❌ | Service Worker 缓存响应 |
Cookie 的安全属性(高频):
HttpOnly—— JS 读不到(document.cookie拿不到),防 XSS 窃取Secure—— 只在 HTTPS 下发送SameSite——Strict(完全禁止跨站发送)/Lax(默认,顶级导航的 GET 可以带)/None(允许跨站但必须配Secure)。防 CSRF 的主要手段Domain/Path—— 限定作用范围
localStorage 是同步 API,大量读写会阻塞主线程。而且只能存字符串,存对象要 JSON.stringify。
垃圾回收
V8 用分代回收:
新生代(Scavenge 算法) —— 空间小(几 MB),对象存活时间短。分成 From 和 To 两个半区,复制存活对象到 To,然后交换。存活超过两次回收的对象晋升到老生代。
老生代(标记-清除 + 标记-整理) —— 从根(GC Roots:全局对象、执行栈上的变量)出发标记所有可达对象,清除不可达的。清除会产生内存碎片,所以配合标记-整理移动对象。
增量标记 / 并发标记 —— 把标记过程切片,避免长时间停顿(STW)。
常见内存泄漏:
- 意外的全局变量 —— 忘写
const导致挂到window(严格模式可避免) - 未清理的定时器 ——
setInterval不clearInterval,回调闭包持有的对象永不释放 - 未解绑的事件监听器 —— 尤其是绑在
window/document上的,组件卸载后仍存在 - 闭包持有大对象 —— 见JS 语言核心
- 游离的 DOM 引用 —— JS 变量还指向已从文档移除的 DOM 节点
- 缓存无限增长 —— 用
WeakMap/WeakSet让键可被回收
排查工具:Chrome DevTools 的 Memory 面板 —— 用 Heap Snapshot 对比(操作前后各拍一次,看哪类对象数量持续增长)或 Allocation instrumentation on timeline(看分配时间线上哪些没被释放)。
跨标签页通信
| 方案 | 说明 |
|---|---|
BroadcastChannel | 同源页面间广播,最直接(现代浏览器) |
localStorage + storage 事件 | 一个页面写,其他页面收到 storage 事件。写入的页面自己收不到 |
SharedWorker | 共享的 Worker 作为中转 |
postMessage | 用于 iframe / 窗口间(需要持有 window 引用) |
| Service Worker | 作为中转站 |
其他高频点
浏览器输入 URL 后发生了什么(经典综合题):URL 解析 → DNS 查询(浏览器缓存 → 系统缓存 → hosts → 本地 DNS → 递归查询)→ TCP 三次握手 → TLS 握手(HTTPS)→ 发送 HTTP 请求 → 服务器响应 → 浏览器解析渲染(见上文)→ 断开连接。详见网络与缓存。
load vs DOMContentLoaded —— DOMContentLoaded 在 DOM 解析完成时触发(不等图片、样式表等资源);load 等所有资源加载完。性能监控用前者衡量「可交互时间」。
重定向的性能代价 —— 每次 301/302 都是一次完整的往返(DNS + TCP + 请求)。所以要避免重定向链,尤其是在关键资源上。
为什么 <img> 不阻塞渲染 —— 图片是异步加载的,布局时如果不知道尺寸就先占 0 高度(这是 CLS 的主要来源,所以要给 width/height 或 aspect-ratio)。