浏览器原理
前端优化的答案大多写在浏览器内部机制里。本文按"一个页面如何被显示出来"的主线展开:进程架构 → 导航 → 解析渲染 → 合成;再补事件循环这个"运行时心脏",最后落到存储与安全——知道边界在哪,才能安全地存、正确地防。
一、进程与线程架构
现代浏览器(Chrome/Edge 系)以多进程隔离站点与职责:
浏览器进程(UI、网络、存储管理)
├─ 网络进程 网络栈(HTTP/缓存)
├─ GPU 进程 合成、位图上传
└─ 渲染进程×N 每站点一个:主线程(JS/布局/绘制树)+ 合成线程
+ 排版线程(text layout)+ IO 线程(事件捕获)为什么隔离:一个标签页崩溃不影响其他页;渲染进程沙箱化,网页代码拿不到系统权限。代价是进程间通信(IPC)与内存开销——这也是"打开的标签页越多越吃内存"的机制原因。
注意区分:主线程跑 JS、解析、样式与布局的骨架;合成线程独立处理 transform/opacity 动画与滚动——这解释了为何合成器属性动画不阻塞 JS。
二、一次导航与渲染管线
2.1 从地址栏到首帧
1. 输入 URL → 浏览器进程查缓存/发请求(DNS、TCP、TLS)
2. 拿到 HTML → 分配渲染进程
3. 渲染进程主线程解析:
HTML 词法 → DOM 树
CSS 解析 → CSSOM 树
JS 遇到即执行(可阻塞解析;defer/async 改变时序)
4. DOM + CSSOM → 渲染树(合并:去掉 display:none、补上可见样式)
5. Layout:计算每个可见元素的几何(宽高/位置)——整棵树算
6. Paint:把布局结果绘制为位图(按层)
7. Composite:合成线程把各层合成最终画面(GPU)/* 各阶段改动的代价对照(从轻到重) */
/* 合成器层 → transform / opacity(最便宜) */
/* 绘制 → color / box-shadow / background */
/* 布局(重排) → width / height / 位置 / 字体 / 添加 DOM(最贵) */2.2 关键概念速记
- 重排(reflow/layout):几何变了,整棵子树甚至整页重新算几何;
- 重绘(repaint):几何没变只是颜色/阴影变,跳过 layout 直接 paint;
- 渲染阻断资源:同步
<script>与 CSS 会挡在首帧前(CSS 还阻断其后的 JS 执行); - 图层(layer):浏览器把复杂区域提升为独立层,滚动/动画只在合成线程处理。
will-change、transform、position: fixed等都可能触发分层。
<!-- 优化首帧加载顺序 -->
<link rel="stylesheet" href="/screen.css"> <!-- 首屏 CSS:同步,尽快 -->
<script src="/app.js" defer></script> <!-- defer:DOM 解析完再执行 -->三、事件循环:运行时的心脏
3.1 队列模型(浏览器侧简化版)
宏任务队列:script 整体、setTimeout/setInterval、I/O、UI 事件
微任务队列:Promise.then、queueMicrotask、MutationObserver、await 后续
每个宏任务执行完 → 清空全部微任务 → 渲染机会(浏览器决定是否重绘)
→ 下一个宏任务……console.log('1')
setTimeout(() => console.log('2'), 0) // 宏任务
Promise.resolve().then(() => console.log('3')) // 微任务
queueMicrotask(() => console.log('4')) // 微任务
console.log('5')
// 输出顺序:1 5 3 4 2
// 同步 → 微任务(先注册先执行)→ 宏任务渲染发生在微任务之后、下一个宏任务之前——在微任务里连续改动 DOM 不会触发多次布局,浏览器攒到一帧统一绘制。所以"帧对齐"的操作该用
requestAnimationFrame(rAF 在渲染前回调),而不要依赖 setTimeout。
3.2 async/await 与任务时序
async function f() {
console.log('a')
await 0 // 等价于 Promise.resolve(0).then(...) → 交出控制权
console.log('b') // 作为微任务继续
}
f()
console.log('c') // 输出:a c bawait 后面的代码永远以微任务方式续跑;循环里反复 await 也只在宏任务间隙穿插,不会让出渲染。要让出渲染:主动 await new Promise(r => setTimeout(r)) 或 rAF。
3.3 常见时序问答
| 问题 | 答案 |
|---|---|
setTimeout(fn, 0) 能保证 0ms 执行吗 | 不能,还有 4ms 嵌套下限与宏任务排队 |
rAF 与 setTimeout 谁先 | rAF 紧跟渲染前;连续改样式动画用 rAF 才不会丢帧 |
requestIdleCallback 何时跑 | 帧间空闲时;适合低优先级后台任务(分析、上报) |
| 死循环为何卡 UI | 主线程是唯一的,长任务超过 50ms 就阻塞交互(INP 恶化) |
四、渲染、布局与安全都涉及的存储
4.1 存储机制对比
| 机制 | 容量 | 持久性 | 同步/异步 | 同源 | 典型场景 |
|---|---|---|---|---|---|
| localStorage | ~5MB/源 | 持久,手动清 | 同步 | 同源(不跨 tab 强一致) | 主题、不敏感配置 |
| sessionStorage | ~5MB/源 | 标签页会话 | 同步 | 同源 | 表单草稿、页内状态 |
| Cookie | ~4KB/个 | 按 Expires | 同步,随请求发送 | 按 Domain/Path | 会话标识(HttpOnly) |
| IndexedDB | 大(GB 级) | 持久 | 异步 | 同源 | 离线数据、缓存文件元数据 |
| Cache Storage | 大 | 持久 | 异步 | 同源(SW 作用域) | Service Worker 请求缓存 |
| OPFS | 大 | 持久 | 异步 | 同源 | 文件系统级高性能读写 |
// IndexedDB 最小用法(原生 API 冗长,可配合 idb 库)
const req = indexedDB.open('my-db', 1)
req.onupgradeneeded = () => req.result.createObjectStore('kv')
req.onsuccess = () => {
const db = req.result
db.transaction('kv', 'readwrite').objectStore('kv').put('value', 'key')
}坑 1:localStorage 同步阻塞主线程 + 所有同源标签页共享——别把大数据或高频读写放进去;容量上限接近时写入直接抛异常。 坑 2:Cookie 别存敏感业务数据(每次请求都会带上它,放大攻击面与体积);登录态 token 请用
HttpOnly + Secure + SameSite。
4.2 安全防线(浏览器侧能做的)
| 威胁 | 机制 | 前端对策 |
|---|---|---|
| XSS(脚本注入) | 浏览器执行了非预期脚本 | 输出转义、CSP、不 eval、sanitize 用户 HTML |
| CSRF(跨站请求伪造) | 浏览器自动携带 Cookie | SameSite=Lax/Strict、CSRF Token、校验 Origin |
| 点击劫持 | iframe 内层叠诱骗 | X-Frame-Options: DENY / CSP frame-ancestors |
| 敏感数据外泄 | 站内脚本窃读 | HttpOnly Cookie、最小权限、CSP 收紧 |
| 供应链脚本 | 三方 CDN 被投毒 | SRI(integrity 校验)、锁版本、自托管 |
<!-- 内容安全策略:只放行自家域名与指定三方;禁止内联脚本/eval -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; img-src 'self' data: https:"><!-- SRI:脚本内容与摘要不符则不执行,防 CDN 劫持 -->
<script src="https://cdn.example.com/lib.js"
integrity="sha384-xxxx" crossorigin="anonymous"></script>SameSite 三值速记:
Strict(任何跨站都不带 Cookie,最安全但影响第三方登录回跳)、Lax(GET 顶层导航带,默认值)、None(必须配 Secure,允许跨站发送——显式开放才用)。CORS 不是安全机制是策略机制:它挡的是"浏览器读取响应",挡不住"请求已发出"。配合 Cookie 的 SameSite 与 CSRF Token 一起用。
五、工程启发与坑位速查
- 动画卡顿先看是"JS 阻塞"还是"布局抖动":Performance 面板里长任务 vs 紫色 layout 块一目了然;
- 事件循环别造轮子:异步流程先想 Promise/async——比 callback 嵌套在微任务时序上更可预测;
- Storage 溢出:写入前 try/catch 并估算容量,localStorage 写满会静默失败或抛错;
- CSS 放在
<head>、脚本用defer:这是"首帧不被渲染阻断"的最便宜手段; - 虚拟 DOM 不改变渲染管线:diff 再快,最终 layout/paint 成本一样;优化看最终 DOM 变化量;
- 别在滚动事件里做重活(含创建新对象):IO 线程捕到事件,主线程逐帧处理,超帧预算就掉帧;
- Service Worker 是缓存不是离线银弹:缓存策略不当(过度缓存 HTML)会导致用户看到旧页面——用
network-first兜底导航请求。