从输入URL到页面显示:现代浏览器到底在背地里忙什么?
从输入URL到页面显示:现代浏览器到底在背地里忙什么?
> 核心直觉:你敲下回车那0.1秒,浏览器已经干了几十件事——DNS查询、TCP握手、TLS协商、HTML解析、CSS解析、JavaScript编译、DOM构建、样式计算、布局、绘制、合成……但大多数开发者对这些"黑箱操作"一无所知,优化性能时只能靠猜。Addy Osmani——那个16岁写浏览器、后来领导Chrome Developer Experience的人——把这一切摊开了给你看。
---
一、一个被问烂了的面试题
"从输入URL到页面显示,发生了什么?"
标准答案是: 1. DNS解析 2. TCP三次握手 3. TLS协商(HTTPS) 4. 发送HTTP请求 5. 服务器响应 6. 浏览器解析HTML 7. 构建DOM树 8. 解析CSS构建CSSOM 9. 合并成渲染树 10. 布局(Layout) 11. 绘制(Paint) 12. 合成(Composite)
背完这12步,面试官点点头,你松了口气。
但这12步是1980年代的思维模型。 现代浏览器的复杂度远超这个线性流程。真正的问题不是"发生了什么",而是:
> 浏览器怎么在200毫秒内做完这一切?它用了哪些你根本想不到的优化?以及——你写的代码,有多少次无意中把这些优化全毁了?
---
二、Addy Osmani 是谁?为什么听他讲
Addy Osmani 的履历本身就像一部浏览器发展史:
- 16岁:在爱尔兰小镇的高中里,用C++写了一个叫 XWebs 的浏览器,支持下载管理、内置播放器、网页编辑器,甚至还有语音朗读助手。拿了全国青年科学竞赛一等奖。
- 2012年:加入Google,领导 Chrome Developer Experience 团队。
- Chrome DevTools:他 overseeing 的工具,4000万开发者每天用。
- Lighthouse:他参与创建的网站性能审计工具,每月运行数千万次。
- Core Web Vitals:他和同事定义的 LCP、CLS 等性能指标,直接影响Google搜索排名。
- Speedometer:他和Apple WebKit团队合作创建的基准测试,驱动了Apple、Intel优化芯片和引擎。
- 2023-2025年:领导把 Google Gemini AI 集成到 Chrome DevTools。
---
三、网络层:投机加载的艺术
3.1 你以为的加载 vs 实际发生的加载
你以为的流程:
请求HTML → 收到HTML → 解析HTML → 发现需要CSS → 请求CSS →
收到CSS → 解析CSS → 发现需要图片 → 请求图片 → ...
实际的流程(Chrome):
你还没敲完URL → 浏览器已经在猜你要访问什么
收到HTML第一个字节 → 预加载扫描器已经开始扫描
发现 <img src="hero.jpg"> → 还没解析完HTML就已经在下载图片
发现 <script> → 如果是async/defer,并行下载;如果是同步,阻塞解析
发现 <link rel="preload"> → 立即提升优先级
3.2 预加载扫描器(Preload Scanner)——浏览器的"偷看者"
这是现代浏览器最重要的隐藏优化之一。
当HTML解析器遇到 标签(尤其是同步脚本)时,它必须停下来等脚本下载和执行完才能继续。这段时间,如果浏览器什么都不做,就浪费了。
于是浏览器有一个独立的预加载扫描器,它在主解析器被阻塞时,偷看HTML源码中还没解析到的部分,提前发现需要下载的资源(图片、CSS、字体、JS),并发起请求。
效果:Chrome的实验数据显示,预加载扫描器能带来约 20% 的页面加载时间提升。
但你可能正在毁掉这个优化:
// ❌ 错误示范:把资源发现逻辑藏在JS里
document.addEventListener('DOMContentLoaded', () => {
const img = document.createElement('img');
img.src = '/hero.jpg'; // 预加载扫描器看不到这个!
document.body.appendChild(img);
});
预加载扫描器不能执行JavaScript。如果资源的URL是通过JS动态生成的,浏览器就看不到它,无法提前下载。结果就是:用户看到一片空白,等JS执行完才开始下载关键图片。
正确做法:
<!-- ✅ 让资源在HTML中可见 -->
<img src="hero.jpg" fetchpriority="high" alt="Hero image">
<!-- 或者用 preload 明确告诉浏览器 -->
<link rel="preload" href="hero.jpg" as="image" fetchpriority="high">
3.3 投机加载(Speculative Loading)——Chrome比你更了解你的网站
Chrome有一个叫 Speculation Rules API 的功能(Addy Osmani 是背后的主要推动者之一)。
原理很简单:如果用户当前在页面A,Chrome分析历史数据,预测用户接下来可能访问页面B,于是提前预渲染页面B。
<!-- 页面A的HTML中 -->
<script type="speculationrules">
{
"prerender": [{
"source": "list",
"urls": ["/next-page"]
}]
}
</script>
当用户真的点击"/next-page"时,页面已经是预渲染好的,体验像单页应用一样流畅。
这不是激进的实验性功能。 Google搜索已经在用:你看到的搜索结果,Chrome可能已经预渲染了排在前几位的页面。
---
四、解析层:HTML和CSS是怎么被"读懂"的
4.1 HTML解析不是"一行一行读"
浏览器的HTML解析器是一个状态机。它不是逐字符读,而是根据当前状态和下一个token,决定转移到哪个新状态。
Data state → 看到 '<' → Tag open state
Tag open state → 看到 '!' → Markup declaration open state
Markup declaration open state → 看到 '--' → Comment start state
...
HTML5规范明确定义了80多个解析状态。这个状态机的存在,是为了处理HTML的容错性——浏览器遇到 malformed HTML 时,不能崩溃,必须按规则恢复。
这就是为什么写HTML时不用太纠结语法完美——浏览器比你更能容错。但这也是为什么不同浏览器可能有细微差异——它们的错误恢复规则可能不同。
4.2 CSS解析:从字符到规则
CSS解析比HTML更复杂,因为CSS是一门上下文无关语言(context-free grammar)。
/* 浏览器看到的不是"文本",而是token序列 */
body { color: red; }
/* Token序列: */
/* IDENT(body) LBRACE IDENT(color) COLON IDENT(red) SEMICOLON RBRACE */
解析过程分两步: 1. 词法分析(Tokenization):把字符流变成token流 2. 语法分析(Parsing):把token流变成CSS规则树
CSS解析器遇到不认识的属性时,会跳过整条规则——不是忽略那个属性,而是整条声明都作废。这就是为什么写CSS时要注意兼容性前缀的顺序。
4.3 关键渲染路径:阻塞渲染的资源
浏览器构建渲染树需要两个东西:DOM和CSSOM。如果CSS还没下载完,浏览器会延迟渲染——因为不知道元素的样式,画出来也是错的。
关键洞察:
- 同步JS会阻塞DOM构建(因为JS可能修改DOM)
- CSS会阻塞渲染(因为需要样式信息)
- 同步JS也会阻塞CSSOM构建(因为JS可能查询计算样式)
<head>
<!-- ✅ CSS放在head,浏览器一收到就开始解析 -->
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- 页面内容 -->
<!-- ✅ JS放在body末尾,不阻塞渲染 -->
<script src="app.js"></script>
</body>
---
五、JavaScript引擎:V8的JIT编译
5.1 为什么JS需要JIT?
JavaScript是解释型语言。但解释执行太慢了——如果每次执行都要把源码翻译成机器码,复杂网页的JS执行时间会 unacceptable。
JIT(Just-In-Time)编译的 trick 是:先解释执行,同时监控哪些代码被频繁执行(热点代码),然后把热点代码编译成高效的机器码。
V8引擎有两层编译器:
| 编译器 | 策略 | 速度 | 优化程度 |
|---|---|---|---|
| Ignition | 字节码解释器 | 启动快 | 低 |
| TurboFan | 优化编译器 | 启动慢 | 高 |
5.2 隐藏类(Hidden Classes)——V8的"类型推断"
JS是动态类型语言,但CPU执行机器码时喜欢静态类型。V8的 trick 是隐藏类:
function Point(x, y) {
this.x = x;
this.y = y;
}
const p1 = new Point(1, 2);
const p2 = new Point(3, 4);
// V8发现p1和p2有相同的属性结构,给它们分配同一个隐藏类
const p3 = new Point(5, 6);
p3.z = 7; // ❌ 属性结构变了!V8必须创建新的隐藏类,p3和p1/p2不再共享优化
性能启示:
- 对象的属性创建顺序要一致
- 不要在运行时随意添加/删除属性
- 数组最好保持单一类型(全数字或全字符串)
5.3 垃圾回收:V8的"分代回收"
V8把内存分为两个区域:
| 区域 | 存放 | 回收频率 | 算法 |
|---|---|---|---|
| 新生代 | 新创建的对象 | 频繁 | Scavenge(复制算法) |
| 老生代 | 存活时间长的对象 | 较少 | Mark-Sweep-Compact |
V8的优化:增量标记(incremental marking)和并发清理——把垃圾回收的工作分散到多个小步骤中,减少单次暂停时间。
对你的影响:
- 避免创建大量临时对象(会导致频繁GC)
- 大对象直接进入老生代,减少Scavenge压力
- 内存泄漏通常是因为对象被意外引用,GC无法回收
六、渲染管线:从像素到屏幕的旅程
6.1 现代浏览器的多进程架构
Chrome不是单进程应用。它用了多进程架构:
| 进程 | 职责 | 数量 |
|---|---|---|
| Browser进程 | 主控、UI、网络请求管理 | 1 |
| Renderer进程 | 解析HTML/CSS/JS、渲染页面 | 每标签页1个(或每站点1个) |
| GPU进程 | 图形处理、合成 | 1 |
| Network Service | 网络请求 | 1 |
| Utility进程 | 各种辅助服务 | 多个 |
- 安全:每个Renderer是沙箱化的,一个页面崩溃不影响其他页面
- 性能:充分利用多核CPU
- 隔离:不同站点的Renderer进程隔离,防止恶意页面窃取数据
6.2 渲染管线的六个阶段
JavaScript → Style → Layout → Paint → Composite
1. JavaScript
- JS修改DOM或CSSOM
- 触发重新渲染
- 浏览器计算每个元素的最终样式(匹配CSS选择器、处理继承和层叠)
- 输出:每个元素的计算样式(computed style)
- 计算每个元素在视口中的几何信息(位置、大小)
- 输出:布局树(Layout Tree)
- 代价高:涉及整个文档或子树的计算
- 浏览器把页面分成多个图层(Layer)
- 某些CSS属性会创建新图层:transform、opacity、will-change 等
- 图层可以独立绘制和合成
- 把每个图层的内容绘制到内存中的位图(Bitmap)
- 绘制顺序:背景色 → 背景图 → 边框 → 子元素 → 轮廓
- GPU把所有图层合成最终图像
- 图层可以独立变换(平移、缩放、旋转),不需要重新绘制
6.3 重排(Reflow)vs 重绘(Repaint)vs 合成(Composite)
| 操作 | 触发 | 代价 | 例子 |
|---|---|---|---|
| 重排 | 改变几何属性 | 最高 | width、height、top、left |
| 重绘 | 改变外观但不改变几何 | 中等 | color、background-color、visibility |
| 合成 | 仅改变合成属性 | 最低 | transform、opacity |
// ❌ 触发重排(代价高)
element.style.width = '100px';
element.style.height = '200px';
element.style.left = '10px';
// 三次修改 = 三次重排!
// ✅ 批量修改,只触发一次重排
element.style.cssText = 'width: 100px; height: 200px; left: 10px;';
// ✅ 或者使用 transform(只触发合成)
element.style.transform = 'translate(10px, 0)';
强制同步布局(Forced Synchronous Layout)——性能杀手:
// ❌ 交替读写,强制浏览器立即重排
for (let i = 0; i < elements.length; i++) {
const height = elements[i].offsetHeight; // 读(需要最新布局)
elements[i].style.height = height + 10 + 'px'; // 写(触发重排)
}
// ✅ 先批量读,再批量写
const heights = elements.map(el => el.offsetHeight); // 批量读
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // 批量写
});
---
七、性能优化:顺应浏览器规则的心法
7.1 Core Web Vitals:Google定义的三项核心指标
Addy Osmani 参与定义的 Core Web Vitals,是衡量用户体验的三个关键指标:
| 指标 | 测量 | 目标值 | 意义 |
|---|---|---|---|
| LCP (Largest Contentful Paint) | 最大内容元素渲染时间 | ≤2.5s | "页面主要内容加载完成" |
| FID (First Input Delay) → INP (Interaction to Next Paint) | 首次交互到响应的延迟 | ≤200ms | "页面可交互" |
| CLS (Cumulative Layout Shift) | 累积布局偏移 | ≤0.1 | "视觉稳定性" |
- 让LCP元素(通常是首屏最大图片或文本块)在HTML中直接可见
- 用
fetchpriority="high"提升图片优先级 - 不要用JS动态插入LCP元素
- 优化服务器响应时间(TTFB)
- 图片和嵌入元素要设置明确的 width/height
- 不要动态插入内容到已有内容上方
- 字体加载策略(避免FOIT/FOUT导致布局跳动)
7.2 资源优先级:浏览器怎么决定先加载什么
浏览器给每个资源分配一个优先级:
| 优先级 | 资源类型 |
|---|---|
| Highest | HTML、同步CSS、字体 |
| High | 首屏图片、预加载资源 |
| Medium | async/defer JS、非首屏图片 |
| Low | 延迟加载的图片 |
| Lowest | 预获取(prefetch)的资源 |
fetchpriority 属性微调:<!-- 提升LCP图片优先级 -->
<img src="hero.jpg" fetchpriority="high" alt="Hero">
<!-- 降低非关键JS优先级 -->
<script src="analytics.js" fetchpriority="low"></script>
7.3 关键资源数量控制
浏览器对同一域名的并发连接数有限制(HTTP/1.x时通常是6-8个)。如果关键资源太多,就会排队。
HTTP/2 multiplexing 缓解了这个问题(一个连接上可以并发多个请求),但资源数量仍然影响性能。
Addy Osmani 的 checklist:
- ✅ 减少关键CSS(内联首屏所需CSS,其余异步加载)
- ✅ 延迟非关键JS(async/defer)
- ✅ 代码分割(Code Splitting),按需加载
- ✅ 图片懒加载(loading="lazy")
- ✅ 使用现代图片格式(WebP、AVIF)
- ✅ 字体子集化(只加载需要的字符)
八、Addy Osmani 的"70%问题"
Addy Osmani 在离开Google后写了很多关于AI辅助编程的文章,但他对前端性能的观点一直没有变:
> "大多数性能问题不是因为缺某个高级技术,而是因为基础没做对。"
他提出的"70%问题":
| 问题 | 占比 | 解决方案 |
|---|---|---|
| 图片未优化 | ~30% | 压缩、现代格式、响应式图片 |
| JS/CSS未压缩 | ~20% | Gzip/Brotli、代码分割、Tree Shaking |
| 阻塞渲染的资源 | ~15% | 内联关键CSS、延迟JS |
| 字体加载策略 | ~5% | font-display: swap、子集化 |
---
九、总结:为什么懂浏览器原理很重要
回到开头的问题:"从输入URL到页面显示,发生了什么?"
现在你知道了:
1. 网络层:浏览器在猜你要访问什么(投机加载),在解析器被阻塞时偷看后续资源(预加载扫描器) 2. 解析层:HTML用状态机容错解析,CSS用上下文无关语法解析,两者都可能被JS阻塞 3. JS引擎:V8用JIT编译(Ignition + TurboFan),用隐藏类优化动态类型,用分代GC管理内存 4. 渲染管线:JavaScript → Style → Layout → Paint → Composite,重排代价最高,合成代价最低 5. 性能优化:Core Web Vitals(LCP、INP、CLS)定义了用户体验标准,70%的问题靠基础优化解决
关键心法:
> 不要和浏览器对抗,要顺应它的规则。 > > - 让关键资源在HTML中可见(不要藏在JS里) > - 减少重排和重绘(用transform代替top/left) > - 控制关键资源数量(内联关键CSS,延迟非关键JS) > - 尊重资源优先级(fetchpriority微调)
浏览器是地球上最复杂的软件之一。理解它的工作原理,不是面试需要,而是写出高性能Web应用的必备能力。
正如 Addy Osmani 说的:
> "如果你不理解浏览器怎么加载、解析、渲染你的代码,你的优化就是凭感觉——而感觉通常是错的。"
---
参考来源:
- Addy Osmani. "How Modern Browsers Work." Medium, 2025.
- Addy Osmani biography: https://addyosmani.com/bio/
- High Performance Browser Networking by Ilya Grigorik (O'Reilly)
- Chrome Developers Documentation: developers.google.com/web
- V8 Blog: v8.dev/blog
🌟 智谱 GLM-5 已上线
我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。
🎁 领取 2000万 Tokens