TCP/IP 与前端性能:从数据包到首次渲染的底层逻辑

我以前排首屏慢,第一反应总是看包体、看 JS 执行、看图片有没有压。后来有次页面本身不大,Performance 里主线程也不算忙,但用户就是等很久才看到第一屏。最后打开 Network 才发现,时间主要耗在连接和等待首字节上。

这是“从 URL 到页面展示”系列的第三篇。前面两篇聊完了浏览器导航、DNS 和传输层细节,这一篇继续往下钻一点:一个数据包究竟怎么跑完整条链路?TCP/IP 为什么会直接影响前端最关心的首次渲染?

前端性能优化不能只盯着 JS、CSS 和图片。用户从输入 URL 到看到页面,中间有一大段时间消耗在网络链路上。理解这条链路,很多性能优化手段才不只是“背结论”。

性能起点:TTFB 和 FP

排查页面加载慢时,我会先把两个时间点拆开看。

TTFB,也就是 Time To First Byte,表示从浏览器发起请求,到收到服务器第一个字节的时间。

它大致包含:

  • DNS 解析
  • TCP 连接
  • TLS 握手
  • 服务器处理
  • 响应第一个字节传回浏览器

FP,也就是 First Paint,表示浏览器第一次把像素绘制到屏幕上的时间。

它大致可以理解为:

1FP = TTFB + HTML 解析 + CSSOM 构建 + 渲染树构建 + 布局 + 首次绘制

从这个链条能看出来,TTFB 是 FP 的前置成本。网络层越慢,浏览器越晚拿到 HTML,后面的解析和渲染也就越晚开始。

这就是为什么前端做性能优化时,不能只看包体积和代码执行时间。网络链路本身就是首屏体验的一部分。

数据包的旅程

互联网不是把一个完整文件一次性塞进网线里。

一个 HTML、JS、图片文件,都会被拆成一个个数据包,在网络中传输。到达对端后,再按规则重新组装。

为什么要拆包?

第一,单个文件可能很大。如果一次性占住整条链路,其他请求就只能等。

第二,拆成小包后,传输更灵活。某个包丢了,只需要重传这个包,不需要把整个文件重传。

第三,网络路径复杂,不同包可能经过不同节点。小包传输更适合互联网这种分布式网络。

这些数据最终会变成二进制帧,在物理介质上传输。前端看到的是一个请求,底层实际是很多包在网络里流动。

IP 层:尽力送到

IP,也就是 Internet Protocol,工作在网络层。

它的职责很单纯:根据 IP 地址,把数据包从源主机送到目标主机。

但 IP 提供的是“尽力而为”的服务。它不保证:

  • 一定送达
  • 一定按顺序送达
  • 一定不重复
  • 一定不出错

所以 IP 本身是不可靠的。

这对 Web 页面来说显然不够。HTML、CSS、JS 都需要完整、正确、有序地到达浏览器。一个关键字节出错,都可能导致资源解析失败。

可靠性要交给传输层处理。

UDP 和 TCP:两种传输方式

传输层运行在 IP 之上,负责把数据交付给目标主机上的具体应用。端口号就是用来定位应用的。

常见的两个传输层协议是 UDP 和 TCP。

UDP:只管快

UDP 不建立连接,直接发包。

它不保证顺序,不保证到达,也不会自动重传丢失的数据。

它的优势是头部开销小、速度快、延迟低。

适合场景包括:

  • 音视频直播
  • 视频通话
  • 在线游戏
  • 对实时性要求高、能容忍少量丢包的业务

对于这些场景,晚到的数据可能已经没有意义。与其等重传,不如继续处理新数据。

TCP:保证可靠

Web 页面资源更适合 TCP。

HTML、CSS、JS、图片这些资源通常要求完整、正确、有序。TCP 主要解决两个问题。

问题TCP 的解法
数据包传输过程中丢失超时重传。发送端等待确认,超时未确认就重发
数据包到达顺序错乱序号机制。每个包带序号,接收端按序重新组装

因为要保证可靠性,TCP 会比 UDP 更重。但 Web 页面需要的正是这种可靠传输。

前端页面慢,有时不是服务器慢,而是网络 RTT 高、丢包多、重传多,导致 TCP 层耗时增加。

三次握手:建立可靠连接

真正发送 HTTP 请求之前,TCP 需要先建立连接。

这个过程就是三次握手。它的核心目的,是同步双方初始序号,并确认双方都有发送和接收能力。

简化过程如下:

1客户端 -> 服务器:SYN,初始序号 J
2服务器 -> 客户端:SYN + ACK,确认 J+1,同时给出自己的初始序号 K
3客户端 -> 服务器:ACK,确认 K+1

换成更口语的说法:

1客户端:我想建立连接,我从 J 开始发,你听得到吗?
2服务器:听到了,你下一个从 J+1 发。我也从 K 开始发,你听得到吗?
3客户端:听到了,你下一个从 K+1 发。可以正式通信了。

为什么不是两次

两次握手无法让服务器确认客户端具备接收能力。

如果只有:

1客户端 -> 服务器:SYN
2服务器 -> 客户端:SYN + ACK

服务器并不知道客户端是否收到了自己的响应。连接状态就不够可靠。

为什么不是四次

四次当然也能完成确认,但没必要。

服务器在第二次握手里,把“确认客户端 SYN”和“发送自己的 SYN”合并成一条消息:

1SYN + ACK

这样刚好三次就能确认双方收发能力。

这就是三次握手最常见的解释:每一方的发送和接收能力都需要被确认,而服务器把两个动作合并了。

四次挥手:断开连接

数据传输结束后,TCP 连接需要释放资源。

断开过程通常是四次挥手。

1A -> B:FIN,序号 M
2B -> A:ACK,确认 M+1
3B -> A:FIN,序号 N
4A -> B:ACK,确认 N+1

可以理解为:

1A:我没有数据要发了,准备断开。
2B:知道了,但我可能还有数据没发完。
3B:我的数据也发完了,可以断开。
4A:收到,再见。

为什么挥手比握手多一次

因为 TCP 是全双工通信。

一端说“我发完了”,并不代表另一端也发完了。A 关闭发送方向后,B 可能还有数据要继续传。

所以 B 的 ACK 和 FIN 通常不能像握手时那样合并,必须分开处理。

这就是四次挥手的来源。

回到前端性能

理解 TCP 以后,很多前端性能优化手段就有了底层解释。

优化手段背后的网络逻辑
减少请求数每个连接都有建立成本,请求越碎,调度和连接成本越明显
使用 HTTP/2 多路复用在单个 TCP 连接上并发传输多个请求和响应,减少重复握手
使用 CDN缩短用户到资源节点的物理距离,降低 RTT 和丢包概率
preconnect提前完成 DNS、TCP、TLS,真正请求时直接复用连接
合理缓存静态资源避免重复走网络链路
控制资源大小减少传输时间、丢包重传和解析成本

早期经常强调雪碧图、文件合并,是因为 HTTP/1.1 下连接和请求成本明显。HTTP/2 之后,资源策略会更细,但减少关键链路上的网络等待仍然重要。

RTT 对 TTFB 的影响

RTT 是 Round Trip Time,也就是一次往返时延。

TCP 三次握手至少需要一次 RTT,TLS 握手还会继续增加耗时。用户离服务器越远,RTT 往往越高。

这也是 CDN 有用的原因。

如果用户在广州,请求却要绕到很远的源站,哪怕服务器处理很快,网络往返也会把 TTFB 拉高。

前端看到的是:

1页面白屏时间变长
2HTML 更晚返回
3首屏渲染更晚开始

所以 CDN、就近接入、预连接,本质上都是在减少等待。

丢包和重传

TCP 可靠,但可靠性不是免费的。

如果网络丢包,TCP 会重传。重传会增加请求完成时间。

弱网下,用户不只是下载速度慢,还可能因为丢包导致资源反复等待。一个关键 JS 包迟迟没下载完,页面交互就会被拖住。

这也是为什么移动端性能优化里,资源体积和关键资源数量都很重要。资源越大,传输过程越容易被网络质量放大影响。

preconnect 的实际意义

如果页面确定马上要请求某个域名,可以提前建立连接:

1<link rel="preconnect" href="https://cdn.example.com">

浏览器可以提前做:

  • DNS 解析
  • TCP 连接
  • TLS 握手

等真正请求资源时,连接已经准备好,能减少等待。

但不要滥用 preconnect。每个连接都有成本,预连接太多会浪费资源。只给关键域名使用更合适。

一条链路串起性能

从网络角度看,一次页面加载大致可以串成:

1DNS 解析
2  -> TCP 三次握手
3  -> TLS 握手
4  -> HTTP 请求/响应
5  -> HTML 解析
6  -> CSSOM 构建
7  -> 渲染树构建
8  -> 布局
9  -> 首次绘制

其中 DNS、TCP、TLS、HTTP 都会进入 TTFB 或影响资源返回时间。TTFB 越晚,浏览器越晚开始后续渲染。

所以性能优化不是一个孤立动作。

减少 JS 体积、压缩图片、使用缓存、开启 CDN、优化接口、预连接关键域名,都在同一条链路上发挥作用。

TCP/IP 看起来是网络基础,但它和前端性能关系很直接。

IP 负责尽力送达,TCP 在 IP 之上提供可靠、有序传输。三次握手建立连接,四次挥手释放连接。RTT、丢包、重传、连接复用都会影响 TTFB,而 TTFB 又会影响 FP。

所以我现在看页面加载优化,不会只说“减少请求数、压缩资源”。我会顺着链路往前查:用户离资源有多远,DNS 是否慢,连接是否提前建立,资源是否命中缓存,关键请求是否被阻塞,弱网下有没有重传。把这些点串起来以后,CDN、preconnect、缓存、接口优化、资源压缩才不是零散动作,而是一条链路上的不同位置。