在地址栏里敲下 example.com,按下回车,几百毫秒后页面出现了。这段时间里浏览器做的事情,可以分成两大段:

  • 前半段:把需要的文件要过来,这是网络部分。
  • 后半段:把这些文件变成屏幕上的像素,这是渲染部分。

先记住一件有点反直觉的事:浏览器要的从来不是“一个页面”,而是 好几个文件。它先拿到一个 HTML 文件,读完这个文件,才知道页面还需要哪些 CSS、JavaScript 和图片,然后再去要这些文件。所以“打开一个网页”,实际是几十次文件的往返,不是一次。

下面按顺序走一遍,整体路线是:

绘制中

第一步:把地址拆成几块

你输入的那串字符,浏览器先要判断它是网址还是搜索词。像 example.com 这样看起来像地址的,才按地址处理;否则它会交给默认搜索引擎。

按地址处理时,浏览器把字符串拆成几部分:协议(https)、域名(example.com)、路径(比如 /hello)。这几部分的用途很明确:域名决定“去找哪台机器”,协议决定“用什么规矩说话”,路径决定“要那台机器上的哪个东西”。

第二步:DNS 把域名换成 IP

机器之间互相找,靠的是 IP 地址,也就是类似 103.102.166.224 的一串数字。域名是给人看的,所以中间需要一个翻译:DNS(Domain Name System,域名系统),它相当于互联网的电话簿,负责把域名对应到 IP 地址。

这个翻译并不会每次都从头查。顺序是逐层找缓存:先看浏览器自己的缓存,再看操作系统的缓存,再看路由器或网络服务商的缓存。都没有,才由 DNS 解析器去问别人:先问根服务器,再问 .com 这类顶级域的服务器,最后问管着这个域名的权威服务器,拿到答案 1。

答案会带一个 TTL(存活时间),告诉你能缓存多少秒。所以第一次访问一个新站点,这一步要多花几十毫秒;下次再访问,基本是瞬间完成。

第三步:TCP 握手,先确认双方能通话

有了 IP 地址,浏览器要跟这台机器的 443 端口建立一条 TCP 连接(HTTPS 默认用 443)。TCP 的职责是提供一条 可靠、有序 的字节流:丢了的包会重发,顺序乱了会重新排好。

建立连接需要三次握手。客户端先发 SYN,意思是“我想连,我的字节从编号 X 开始”;服务器回 SYN-ACK,意思是“收到你的 X 了,我的字节从编号 Y 开始”;客户端再回一个 ACK,确认收到 Y。三次看起来麻烦,但双方各自都要确认“我能发出去、也能收到”,并且约定从哪个字节开始编号,所以这一步不能省 4。

代价是时间:这三次消息要走一个完整的往返(去一趟、再回来一趟)。数据在这之前一个字节也发不出去。

第四步:TLS 握手,把通道加密

裸的 TCP 通道上,路径中间的节点既能看也能改。HTTPS 里的那个 S,来自 TLS(Transport Layer Security),它负责给这条通道加密。握手主要做三件事:服务器出示证书,浏览器检查这张证书是不是由自己内置信任的机构签发的、证书上的名字是否包含你访问的域名,以此确认对面确实是这个网站;双方各自算出一份谁也没在网络上传输过的会话密钥;此后往来的每个字节都加密。

用 TLS 1.3 时,这一步通常只需要一个往返。之后 HTTP 请求就在这条加密通道里跑。

第五步:发请求,收响应

通道建好,浏览器才正式开口。HTTP 请求本身就是一段有固定格式的文本:

1GET /hello HTTP/1.1
2Host: example.com
3Accept: text/html

第一行说明“做什么、要哪个资源、用哪个版本的 HTTP”,后面的头信息补充细节,例如目标域名、能接受什么格式、有没有该网站的 Cookie。

响应的结构是对称的:状态行、响应头、响应体。状态码只需先记住几个:200 表示成功,301 和 302 表示资源搬了家,浏览器会按响应里给出的新地址再请求一次,404 表示找不到 1。

响应体里就是那个 HTML 文本。也就是说,这一轮往返拿到的只是页面的骨架,页面还远没有显示出来。

第六步:把 HTML 变成屏幕上的像素

浏览器读 HTML 的方式,是边读边建一棵树。HTML 里的每个标签、属性和文本,都会变成这棵树上的一个节点,这棵树叫 DOM(Document Object Model)。它代表页面的结构,存在内存里。

读的过程中,只要发现 <link>、<script>、<img> 这类引用,浏览器就会顺手再发起请求去要那些文件。这也是为什么一次打开网页会对应几十次请求。

CSS 到了以后,浏览器同样把它解析成一棵树,叫 CSSOM,记录“哪条样式规则作用在哪些节点上”。接着,DOM 和 CSSOM 合并成一棵 render tree。注意它只保留要显示的内容:display: none 的元素根本不会进这棵树。

render tree 有了,但每个东西放在哪、多大,还不知道。layout(布局)这一步负责算出每个可见节点在屏幕上的精确位置和尺寸,把相对单位换算成真实像素。算完之后进入 paint,把每个节点的颜色、文字、边框、阴影真正画成像素;如果页面被分成多层,还要经过合成再送进屏幕 3。

这里有两个容易绊人的现象,原因都在渲染顺序里。CSS 会 阻塞渲染:因为后面的规则可能覆盖前面的,浏览器必须等 CSS 全部到齐,才能确定最终样式,否则显示出来的样子会是错的。另外,写在 HTML 里的同步 <script> 会 阻塞解析:因为 JavaScript 可能修改 DOM,浏览器必须先把它执行完,才知道后面的 HTML 该建成什么样 2。所以常见做法是把脚本放到 <body> 末尾,或者用 defer 让它延后执行。

还要知道,同一个域名上的后续文件可以复用已经建好的连接,不必每个文件重新握手一次;HTTP/2 还能在一条连接上同时跑多个请求,进一步省掉排队时间 4。

这篇的边界

以上描述的是 第一次访问一个没有缓存的站点。如果你之前来过,浏览器可能直接从本地缓存里取文件,连请求都不发;DNS 也可能直接命中缓存。另外,服务器收到请求之后内部还做了什么,属于服务器那一侧的事,这一篇没有展开。

一句话总结这条主线:网络阶段负责把需要的文件送到浏览器,渲染阶段负责把文件里的描述变成屏幕上的像素。以后遇到白屏、样式闪烁、图片加载后内容被挤下去这类问题,你就可以在脑子里按这两大段、六个步骤去定位它卡在哪一环。