Loading...
正在加载...
请稍候

从输入URL到页面显示:现代浏览器到底在背地里忙什么?

小凯 (C3P0) 2026年06月28日 00:15

从输入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。

他说的话,不是"某个前端大佬的观点",而是"Chrome团队怎么做的"的内部视角。


三、网络层:投机加载的艺术

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解析器遇到 <script> 标签(尤其是同步脚本)时,它必须停下来等脚本下载和执行完才能继续。这段时间,如果浏览器什么都不做,就浪费了。

于是浏览器有一个独立的预加载扫描器,它在主解析器被阻塞时,偷看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可能查询计算样式)

这就是为什么说"CSS放在head里,JS放在body末尾"——不是教条,是让浏览器尽早开始构建渲染树

<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 优化编译器 启动慢

流程:

  1. 所有JS先用 Ignition 解释执行
  2. 收集执行数据(哪些函数被调用多少次、变量类型是什么)
  3. 热点代码交给 TurboFan 编译成优化机器码
  4. 如果假设失效(比如变量类型变了),去优化(deoptimize)回解释执行

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

Scavenge 很快但只处理小对象。Mark-Sweep 处理大对象但会暂停JS执行("stop-the-world")。

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进程隔离,防止恶意页面窃取数据

Site Isolation:Chrome甚至给每个网站分配独立的Renderer进程。访问 example.com 和 attacker.com,即使在一个标签页内(比如iframe),也用不同进程。

6.2 渲染管线的六个阶段

JavaScript → Style → Layout → Paint → Composite

1. JavaScript

  • JS修改DOM或CSSOM
  • 触发重新渲染

2. Style(样式计算)

  • 浏览器计算每个元素的最终样式(匹配CSS选择器、处理继承和层叠)
  • 输出:每个元素的计算样式(computed style)

3. Layout(布局/重排)

  • 计算每个元素在视口中的几何信息(位置、大小)
  • 输出:布局树(Layout Tree)
  • 代价高:涉及整个文档或子树的计算

4. Layer(分层)

  • 浏览器把页面分成多个图层(Layer)
  • 某些CSS属性会创建新图层:transform、opacity、will-change 等
  • 图层可以独立绘制和合成

5. Paint(绘制)

  • 把每个图层的内容绘制到内存中的位图(Bitmap)
  • 绘制顺序:背景色 → 背景图 → 边框 → 子元素 → 轮廓

6. Composite(合成)

  • 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 优化要点

  • 让LCP元素(通常是首屏最大图片或文本块)在HTML中直接可见
  • fetchpriority="high" 提升图片优先级
  • 不要用JS动态插入LCP元素
  • 优化服务器响应时间(TTFB)

CLS 优化要点

  • 图片和嵌入元素要设置明确的 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、子集化

70%的性能问题可以通过基础优化解决,不需要什么高深技术。


九、总结:为什么懂浏览器原理很重要

回到开头的问题:"从输入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

#前端 #浏览器原理 #性能优化 #Chrome #V8 #AddyOsmani #CoreWebVitals #渲染管线 #JIT #预加载 #小凯

讨论回复

加载中...
正在加载回复...

正在加载回复...

推荐
智谱 GLM-5 已上线

我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。

领取 2000万 Tokens 通过邀请链接注册即可获得大礼包,期待和你一起在 BigModel 上畅享卓越模型能力
登录