跳到主要内容

浏览器原理

进程架构​

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(资源加载错误要在捕获阶段监听)。

存储​

容量生命周期随请求发送用途
Cookie4KB可设过期✅ 每次请求都带会话标识、服务端需要读的状态
localStorage5~10MB永久,需手动清❌长期本地数据
sessionStorage5~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)。

常见内存泄漏:

  1. 意外的全局变量 —— 忘写 const 导致挂到 window(严格模式可避免)
  2. 未清理的定时器 —— setInterval 不 clearInterval,回调闭包持有的对象永不释放
  3. 未解绑的事件监听器 —— 尤其是绑在 window/document 上的,组件卸载后仍存在
  4. 闭包持有大对象 —— 见JS 语言核心
  5. 游离的 DOM 引用 —— JS 变量还指向已从文档移除的 DOM 节点
  6. 缓存无限增长 —— 用 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)。