Skip to content

浏览器原理

前端优化的答案大多写在浏览器内部机制里。本文按"一个页面如何被显示出来"的主线展开:进程架构 → 导航 → 解析渲染 → 合成;再补事件循环这个"运行时心脏",最后落到存储与安全——知道边界在哪,才能安全地存、正确地防。

一、进程与线程架构

现代浏览器(Chrome/Edge 系)以多进程隔离站点与职责:

text
浏览器进程(UI、网络、存储管理)
 ├─ 网络进程     网络栈(HTTP/缓存)
 ├─ GPU 进程     合成、位图上传
 └─ 渲染进程×N   每站点一个:主线程(JS/布局/绘制树)+ 合成线程
                  + 排版线程(text layout)+ IO 线程(事件捕获)

为什么隔离:一个标签页崩溃不影响其他页;渲染进程沙箱化,网页代码拿不到系统权限。代价是进程间通信(IPC)与内存开销——这也是"打开的标签页越多越吃内存"的机制原因。

注意区分:主线程跑 JS、解析、样式与布局的骨架;合成线程独立处理 transform/opacity 动画与滚动——这解释了为何合成器属性动画不阻塞 JS。

二、一次导航与渲染管线

2.1 从地址栏到首帧

text
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)
css
/* 各阶段改动的代价对照(从轻到重) */
/* 合成器层    → transform / opacity(最便宜) */
/* 绘制        → color / box-shadow / background */
/* 布局(重排)  → width / height / 位置 / 字体 / 添加 DOM(最贵) */

2.2 关键概念速记

  • 重排(reflow/layout):几何变了,整棵子树甚至整页重新算几何;
  • 重绘(repaint):几何没变只是颜色/阴影变,跳过 layout 直接 paint;
  • 渲染阻断资源:同步 <script> 与 CSS 会挡在首帧前(CSS 还阻断其后的 JS 执行);
  • 图层(layer):浏览器把复杂区域提升为独立层,滚动/动画只在合成线程处理。will-changetransformposition: fixed 等都可能触发分层。
html
<!-- 优化首帧加载顺序 -->
<link rel="stylesheet" href="/screen.css">   <!-- 首屏 CSS:同步,尽快 -->
<script src="/app.js" defer></script>        <!-- defer:DOM 解析完再执行 -->

三、事件循环:运行时的心脏

3.1 队列模型(浏览器侧简化版)

text
宏任务队列:script 整体、setTimeout/setInterval、I/O、UI 事件
微任务队列:Promise.then、queueMicrotask、MutationObserver、await 后续

每个宏任务执行完 → 清空全部微任务 → 渲染机会(浏览器决定是否重绘)
→ 下一个宏任务……
js
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 与任务时序

js
async function f() {
  console.log('a')
  await 0            // 等价于 Promise.resolve(0).then(...) → 交出控制权
  console.log('b')   // 作为微任务继续
}
f()
console.log('c')     // 输出:a c b

await 后面的代码永远以微任务方式续跑;循环里反复 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持久异步同源文件系统级高性能读写
js
// 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(跨站请求伪造)浏览器自动携带 CookieSameSite=Lax/Strict、CSRF Token、校验 Origin
点击劫持iframe 内层叠诱骗X-Frame-Options: DENY / CSP frame-ancestors
敏感数据外泄站内脚本窃读HttpOnly Cookie、最小权限、CSP 收紧
供应链脚本三方 CDN 被投毒SRI(integrity 校验)、锁版本、自托管
html
<!-- 内容安全策略:只放行自家域名与指定三方;禁止内联脚本/eval -->
<meta http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self'; img-src 'self' data: https:">
html
<!-- 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 一起用。

五、工程启发与坑位速查

  1. 动画卡顿先看是"JS 阻塞"还是"布局抖动":Performance 面板里长任务 vs 紫色 layout 块一目了然;
  2. 事件循环别造轮子:异步流程先想 Promise/async——比 callback 嵌套在微任务时序上更可预测;
  3. Storage 溢出:写入前 try/catch 并估算容量,localStorage 写满会静默失败或抛错;
  4. CSS 放在 <head>、脚本用 defer:这是"首帧不被渲染阻断"的最便宜手段;
  5. 虚拟 DOM 不改变渲染管线:diff 再快,最终 layout/paint 成本一样;优化看最终 DOM 变化量;
  6. 别在滚动事件里做重活(含创建新对象):IO 线程捕到事件,主线程逐帧处理,超帧预算就掉帧;
  7. Service Worker 是缓存不是离线银弹:缓存策略不当(过度缓存 HTML)会导致用户看到旧页面——用 network-first 兜底导航请求。

参考链接

基于 VitePress 构建 · 内容以知识共享方式沉淀