LOADING

加载过慢请开启缓存 浏览器默认开启

从敲下网址到看见 3D 场景:一次浏览器渲染全链路拆解

前言

你有没有过这种瞬间:在浏览器地址栏敲下一个网址,回车,几秒后一个能拖动旋转的三维场景就出现在眼前。整个过程快得像呼吸一样自然,但你真要说出”从手指敲下回车,到画面进到我眼睛里,中间到底发生了什么”,大概率卡壳。

这不是刁难。这条链路从你的指尖开始,一路穿过键盘硬件、CPU 与中断、操作系统内核、浏览器引擎、GPU、显示器面板,最后到人眼的感光细胞——它横跨了至少八个领域:计算机组成原理、操作系统、网络、浏览器引擎、图形 API、GPU 架构、显示硬件、视觉神经。写这篇文章,就是想把它从敲键盘那一刻,一路拆到光子打进视网膜,让每个环节、每一层”谁在干活、干了什么、数据长什么样”都不含糊。我会尽量用大白话,不假设你已经懂啥是”中断””渲染管线”或”深度缓冲”。

先建立直觉:整条链路,按”谁在干活”分成四层

为了不让你迷路,先用”责任归属”给链路分层。从你按下回车到看见画面,数据依次流经这四层,每一层干的事、用的硬件、跑的代码位置都不同:

  1. 硬件层(计算机组成原理的主场):键盘、网卡、GPU、显示器面板这些都是物理设备。它们通过总线(bus,主板上传递数据的一组公共线路)和主板相连,靠电信号、中断、DMA 跟主机通信。
  2. 操作系统内核层(操作系统的主场):驱动、中断处理、进程/线程调度、虚拟内存、网络协议栈、窗口系统——它夹在”裸硬件”和”应用程序”中间,负责把千奇百怪的硬件抽象成统一接口,并决定什么时候让谁用 CPU。这一层跑在 内核态(拥有最高权限、能直接管硬件的代码运行模式)。
  3. 用户态应用层(浏览器):浏览器是跑在 OS 之上的普通程序,它在自己的内存空间里解析 HTML、执行 JS、调用图形接口。它跑在 用户态(权限受限的运行模式,不能直接碰硬件),要借 OS 的 系统调用(syscall,应用程序向内核”申请办事”的统一入口,比如”帮我发个网络包””帮我画个图”) 才能发网络、画图形。后面你会反复看到”用户态 ↔ 内核态”的切换,记住它本质就是”低权限的 App 想干硬件的活,得敲门找高权限的 OS 代办”。
  4. 物理/生理层:显示器把电信号变成光,光进入眼睛变成神经信号,大脑合成画面。这一层已经出了计算机,到了人体。

记住这四层,后面每一个环节我都会标一句”这一步是哪一层在干活”。访问一个 3D 场景比看普通网页多出来的复杂度,几乎全在”用户态浏览器调用 GPU 画 3D”那一段——所以文章后半段会重点讲它。


第零段:从键盘到浏览器——你按下的那一下,数据怎么走

这是全链路真正的起点,也是最容易被人忽略的一段。这一段的全部故事,就是**一个按键的电信号,如何变成浏览器里的”回车事件”**。

1. 键盘内部:按键是”电”不是”字”(硬件层)

键盘内部是一张按键矩阵——横竖排成网格的导电线路。你按下某个键,就接通了对应的一横一竖,键盘里那颗微控制器(MCU,键盘内部自带的一小块简易芯片,专门管理按键扫描)不停地扫描这张网,发现”第 3 行第 5 列接通了”,就生成一串扫描码(scancode,用来标识”哪个按键位置被触发”的一串数字,注意它不含”回车””Q”这种语义)——注意,它不知道你按的是”回车”还是”Q”,只知道”矩阵的某个位置变了”。

这个扫描码通过 USB 接口(遵循 HID 人机接口设备协议,HID 即”人机接口设备”,键盘鼠标都走这套标准)发给电脑主机。这一步就是计算机组成原理里讲的 I/O(输入/输出,Input/Output,指 CPU 与外部设备之间的数据往来):键盘是外部设备,通过标准接口与主机通信。

两点硬件细节补上:USB 是主从架构、主机轮询的——不是键盘主动”推”数据,而是主机(电脑)每隔 1ms(全速)或 125µs(高速)主动问键盘”有新的按键吗”,键盘才回扫描码;所以 USB 设备不能”随时打断”主机,得等被问。另外机械按键按下会有几毫秒抖动(bounce,触点弹跳产生多个假信号),MCU 会做去抖(debounce)——延时或多次采样确认,避免一个键被当成按了好几次。

2. 中断:键盘”打断”了正在干活的 CPU(计算机组成原理)

数据到了主机主板上的 USB 控制器(一块管理 USB 接口、临时接收外设数据的芯片),它不会默默排队等 CPU 有空——它会立刻向 CPU 发一个中断请求(IRQ,Interrupt Request,外设”有事要 CPU 处理”时发出的电信号)

这里就是计算机组成原理的核心机制之一。CPU 当时大概率在跑别的进程(比如你刚切走的音乐软件)。收到中断后,它必须暂停手头工作、把当前现场(也就是 CPU 内部那批正在用的临时存储格子——寄存器,以及记录”现在执行到哪”的程序计数器)先暂存到栈(stack,一块”后进先出”的临时记忆区,像一摞便签)里(这叫”保存上下文”),然后跳去执行预先登记好的中断处理程序(ISR,Interrupt Service Routine,OS 事先写好、专门应对某类中断的一小段程序)。等键盘的事处理完,再”恢复现场”回去接着跑原来的程序。

为什么要有中断?因为外设比 CPU 慢得多。如果让 CPU 傻等键盘,电脑就卡死了。中断让 CPU 平时该干嘛干嘛,硬件”有事了喊一声”才来处理——这是现代计算机能边听歌边上网的根本。

补一层硬件/OS 细节:外设的中断信号先送到 中断控制器(现代是 APIC,高级可编程中断控制器),由它定优先级、再交给某个 CPU 核心;CPU 查 IDT(中断描述符表,内核事先登记的”中断号 → 处理函数”映射)跳到对应 ISR。为快,ISR 通常只做”最紧急的一点点”(比如标记”键盘有数据”),耗时后续交给软中断 / 底半部(bottom half,如 tasklet、workqueue)延后做——这叫”顶半部/底半部”分工,避免一个中断卡住别的中断太久。

3. OS 设备驱动 + 输入子系统:把扫描码翻译成”回车”(操作系统层)

跳进的中断处理程序,属于操作系统内核。它干两件事:

  • 设备驱动:键盘驱动从硬件寄存器(register,硬件芯片里临时存放数据的最小存储单元,相当于键盘的”收件箱”)里读出那个扫描码。驱动是 OS 里”专门伺候某款硬件”的代码,屏蔽了不同键盘的差异——不管是机械键盘还是笔记本内置键盘,OS 拿到的都是统一的扫描码。
  • 输入子系统:OS 把扫描码翻译成虚拟键码(virtual-key code,与具体键盘无关、只代表”回车””A”这类逻辑按键的统一编码),再结合当前键盘布局/输入法,判断出”这是回车键被按下了”,生成一个按键事件(一个描述”发生了什么输入”的数据包)。

接着 OS 的窗口系统登场:它维护着”当前哪个窗口拥有焦点”——也就是你现在光标所在的浏览器地址栏。OS 把这个回车事件,通过进程间通信(IPC,Inter-Process Communication,不同程序之间互相传消息的机制)投递给浏览器进程。

这一层体现了操作系统的两大职责:硬件抽象(驱动让应用不用关心硬件细节)和资源仲裁(窗口系统决定事件该给哪个程序)。浏览器从头到尾没碰过键盘硬件一根针脚,全是 OS 在中间传话。

再补具体的落地形式:在 Linux 上,键盘事件在内核表现为 evdev 设备产生的 EV_KEY 事件,回车对应码 KEY_ENTER、值 1 表示按下(0 抬起、2 长按重复)——这就是”扫描码 → 虚拟键码”的实况。之后窗口系统(X11 或 Wayland)依据”当前拥有焦点的窗口”,通过各自协议(X11 的 ClientMessage、Wayland 的 wl_keyboard 事件)把按键事件投到目标客户端(这里是浏览器进程)。OS 的”焦点仲裁”就发生在这一步。

4. 浏览器收到回车:触发导航(用户态应用层)

浏览器(比如 Chrome 的多进程架构里,是负责 UI 的浏览器进程)的地址栏组件收到”回车”事件,识别出”用户要访问这个 URL”,于是触发一次导航(navigation)

现代浏览器是多进程的:UI、渲染网页、网络、GPU 各跑在不同进程,彼此用 IPC 说话、互相沙箱隔离(一个网页崩了不会拖垮整个浏览器)。所以浏览器进程会把”去拿这个网址”的任务,交给独立的网络进程/网络服务去办。

到这里,第零段结束。一个物理按键,已经变成了”网络进程手里的一个 URL 字符串”。接下来才进入大家更熟悉、但最容易被讲浅的网络段。

补一句导航细节:浏览器进程先解析 URL(拆出 scheme=https、host=example.com、port、path),确认是合法导航;再由导航控制器(navigation controller)通过 Mojo IPC(Chromium 跨进程通信框架)把”去加载这个 URL”的请求发给网络服务(network service)——这正是前面说的”用户态进程间靠 IPC 说话”。网络服务才是真正去走 §5 以降那套 DNS/TCP/TLS 的组件。


第一段:URL 变成能用的资源(网络段,逐层拆透)

这一段人人都讲,但少有人讲透两件事:一是网络本身分好几层,每层各管什么;二是同一份数据从上层流到下层,每过一层就被”套一层信封”、改一个名字——应用数据到了传输层叫”段”,到了网络层叫”包”,到了链路层叫”帧”。先建立这个”分层 + 封装”的骨架,TCP 三次握手才算真讲明白。

5. 先看清网络的分层:为什么数据在每个地方叫不同的名字

现实世界用的是 TCP/IP 模型,从你的程序到网线,数据依次经过这几层,每层只解决自己那一段的问题:

  • 应用层(Application):直接面对你的程序,协议如 HTTP/HTTPS(网页)、DNS(查 IP)。这层的数据叫消息(message)或”应用数据”——比如”我要 example.com 的首页”。
  • 传输层(Transport):只管”端到端怎么可靠/高效地把数据送到对面那个程序”。主力是 TCP(保证可靠、按序、不丢不重),它把应用数据切成一块块,每块叫 段(segment);另一个是 UDP(发了就走、不保证必达),它的单位叫 数据报(datagram)
  • 网络层(Internet):只管”从哪个网络地址走到哪个网络地址”,以及中途该怎么选路(路由)。协议是 IP。它在传输层的数据前面再套一个头,变成 包(packet,也叫 IP 数据报)
  • 链路层(Link / 网络接入):只管”在一段具体的物理链路(比如你家和路由器之间这根网线)上,把数据从一个设备送到紧挨着的下一个设备”。典型是以太网。它在 IP 包前后加帧头帧尾,变成 帧(frame)
  • 物理层(Physical):帧里的 0/1 被编码成电信号(网线)或光信号(光纤),真正在介质里跑。

关键机制——封装(encapsulation):每往下一层,就在上层数据前面”套一层信封”(加一个该层的头部,有时还有尾部),里面那层对下层完全不可见。到了接收方,再从外到内一层层剥掉信封(解封装 / decapsulation),把内层数据往上交。这就是为什么”数据在每一个地方流转叫什么不同”——名字跟着它”现在被哪一层处理”走。记住这点,下面 TCP 握手和封装就顺了。

6. DNS:域名怎么变成 IP(应用层的起点)

你输入 https://example.com,浏览器先得知道”example.com 在哪台机器上”。这一步叫 DNS 解析(DNS,Domain Name System,域名系统,相当于互联网的电话簿,负责”网址 ↔ IP”的翻译)。完整链路是这样的:

  1. 浏览器先查自己的 DNS 缓存;没有,再去问 OS 的解析器(stub resolver,操作系统里一小段负责转发查询的代码)
  2. OS 解析器去问预先配置好的 递归解析器(recursive resolver,比如运营商的、或 8.8.8.8 / 1.1.1.1 这类公共 DNS)——“请帮我查出 example.com 的 IP,我不懂具体步骤,你查完告诉我结果”。
  3. 递归解析器自己也不能一步到位,它要逐级问:根服务器.)→ 告诉它”.com 该问谁”;TLD 顶级域服务器.com)→ 告诉它”example.com 的权威服务器在哪”;权威服务器(example.com 自己的 DNS)→ 返回真正的记录(IPv4 的 A 记录或 IPv6 的 AAAA 记录),比如 93.184.216.34
  4. 这几步问答本身都是 DNS 报文,通常走 UDP 53 端口(报文小、要快;超长或区域传送才用 TCP)。

如果你的站点用了 CDN(内容分发网络),权威服务器会根据”是谁在问”(来源 IP)返回一个离你最近的边缘机房 IP——这就是你访问大网站总觉得”秒开”的部分原因。最终浏览器拿到一个 IP,网络段才真正开始建连接。

7. TCP 三次握手:连接到底是怎么建立的(传输层,重点讲透)

拿到 IP 后要建立连接。HTTP 跑在 TCP 之上,而 TCP 是面向连接、可靠、按字节流传输的协议——“可靠”靠的是序列号(sequence number):双方各挑一个随机初始序号,给每一个字节编号,用来去重、排序、确认”你发的东西我收到了没”。连接建立的过程叫三次握手(three-way handshake),核心就是双方互换这个序号、互相确认”我能发、你能收”。

我们一步一步走(设客户端初始序号为 x,服务端为 y):

  • 第一次握手 —— SYN:客户端 → 服务端,发一个 TCP 段,把 SYN 标志位置 1,序号 seq = x。含义是:”我想跟你建立连接,这是我的初始序号 x,请你确认。”(注意 SYN 虽然不带应用数据,但按规则占用 1 个序号,所以对方要确认的是 x+1。)客户端发完进入 SYN-SENT 状态。
  • 第二次握手 —— SYN-ACK:服务端 → 客户端,回一个 TCP 段,SYN=1 且 ACK=1seq = y,确认号 ack = x+1。含义是:”你的连接请求收到了(ack=x+1 表示’我已收到序号 x 及之前的一切’),我这边也想建立连接,这是我的初始序号 y。”服务端进入 SYN-RCVD 状态。
  • 第三次握手 —— ACK:客户端 → 服务端,发一个 TCP 段,ACK=1ack = y+1seq = x+1。含义是:”确认收到你的序号 y,连接可以开了。”双方都进入 ESTABLISHED,连接正式建立。

为什么偏偏是 3 次,不是 2 次也不是 4 次?

  • 连接是双向的,必须双向都确认”我能发、你能收”。两次握手(SYN,然后 SYN-ACK 就当连上了)只能让服务端确认”客户端能发、自己能收”,但客户端还不知道服务端能不能发回来——万一第二次的 SYN-ACK 丢了,服务端以为连上了开始发数据,客户端却压根没准备好,服务端白白等待。第三次 ACK 正是补上”客户端确认收到了服务端的 SYN”这一环。
  • 还有一个老问题:网络上可能漂着客户端很久以前发的旧 SYN(比如上次连接失败的重传)。如果两次握手就开连接,服务端可能被一个迟到失效的请求误开连接、空等。三次握手要求客户端最后再确认一次,旧请求没有这第三步,就不会误开。
  • 那为什么不是 4 次?因为服务端的”确认(ACK)”和”我也想连(SYN)”可以合并进同一个报文(也就是第二次的 SYN-ACK)。分开发是 4 步,合并就是 3 步——3 次已经是效率最优。

握手期间交换的每一个,都是 TCP 段(segment)。握手一结束,浏览器要发的 HTTP 请求就装在后续带着 PSH/ACK 的 TCP 段里,段里的序号继续 x+1、x+2…… 递增往上爬。

8. TLS 握手:HTTPS 怎么做到”别人偷看不了”

如果是 https,在 TCP 连接之上还要再来一次 TLS 握手(TLS,传输层安全协议,负责把通信内容加密,防止被窃听或篡改)。简化流程:

  1. ClientHello:客户端告诉服务端自己支持的 TLS 版本、加密套件清单、一个客户端随机数。
  2. ServerHello + 证书:服务端选定套件,回自己的随机数,并附上证书(certificate,里面含服务端公钥,由权威 CA 数字签名背书——相当于”这张公钥确实是 example.com 的”)
  3. 客户端验证证书链(一路查到它信任的根 CA),确认没被中间人掉包。
  4. 双方用非对称加密(如 ECDHE 密钥交换)协商出一个对称会话密钥——之后所有 HTTP 业务数据都用这个对称密钥加密传输。

为什么这么绕?因为非对称加密慢但能安全地交换密钥,对称加密快却难安全地把密钥直接传给对方。所以握手阶段用前者”安全地交换出”后者,之后用后者高速加密海量数据——各取所长。

分层小贴士:传统上 TCP/IP 协议栈整个跑在 OS 内核里;但现代浏览器(如 Chromium)为了性能,把 HTTP/3(基于 QUIC——一种跑在 UDP 上的新传输协议,UDP 则是一种”发了就走、不保证必达”的轻量传输协议)实现在了用户态——也就是说新协议的部分处理绕开了内核。本文仍以最主流的 TCP+TLS 为主线讲。

9. 封装到底:应用数据怎么变成线路上的信号(内核 + 硬件层)

不管协议在哪算,最后把字节真正送上物理线路,必须借 OS。浏览器调用 OS 提供的 socket API(如 BSD socket、Windows 的 Winsock)发起 系统调用(syscall),把 HTTP 请求字节交给内核。之后内核网络栈一层层”套信封”:

  1. 内核 TCP 层:在应用数据前加 TCP 头部(含源端口、目的端口、序号 seq、确认号 ack、标志位、窗口大小、校验和)。源端口是浏览器随机选的(用来区分”本机哪个程序”),目的端口是 443(HTTPS 的默认端口)。加完头部,这坨变成 TCP 段(segment)
  2. 内核 IP 层:在 TCP 段前加 IP 头部(含源 IP、目的 IP、协议号=6 表示”里面是 TCP”、TTL 生存时间、校验和)。IP 负责”从哪个网络地址到哪个网络地址”,并据此决定下一跳怎么走。加完变成 IP 包(packet / IP datagram)
  3. 内核链路层(以太网):在 IP 包前加以太网帧头(含源 MAC、目的 MAC)——MAC 是网卡的物理地址;目的 MAC 通常是你路由器的 MAC,靠 ARP(地址解析协议,用来把 IP 查成 MAC)得到——帧尾再加一个 FCS 校验。加完变成 以太网帧(frame)
  4. 网卡驱动 → 网卡硬件:帧交给网卡驱动,驱动把它交给网卡。网卡通过 DMA(直接内存访问,Direct Memory Access,一种外设不劳烦 CPU、自己直接读写内存的技术) 把帧从系统内存搬到网卡自己的发送缓冲区,再按物理层编码(如 8b/10b、PAM3)把数字帧变成电信号(网线)或光信号(光纤),送上线路。

DMA 是计算机组成原理里极关键的一招:它让网卡、磁盘这类外设能直接读写内存,而不经过 CPU。如果没有 DMA,网卡每收到一个字节都得 CPU 亲自搬运,CPU 立刻被 I/O 淹没。你看到的”高速网络”,背后是 DMA 在替 CPU 扛搬运工。

所以同一个 HTTP 请求,在应用层叫”消息”,出了 TCP 叫”段”,出了 IP 叫”包”,出了以太网叫”帧”——每套一层信封就改一次名字,正是第 5 节说的”数据在每个地方叫不同的名字”。

10. 回来的路:服务器数据如何逆向解封装回到浏览器

服务器回的 HTML/CSS/JS、以及那个 .glb 3D 模型,走完全相反的路径回到你手里:

对方网卡发信号 → 你的网卡 DMA 把帧收进内存 → 网卡驱动 → 内核链路层拆帧(剥 MAC 头)→ IP 层拆包(剥 IP 头、查校验)→ TCP 层拆段:按序号重组、丢了的段要求重传、把数据按正确顺序排好(这就是”可靠”的体现),最后通过 socket 系统调用把干净的”应用数据”交还给浏览器缓冲区

到这里,第一段结束。浏览器手里终于有了能解析的字节流。下一段讲这些字节怎么变成”能画 3D 的网页”。


第二段:浏览器把数据变成”能画 3D 的网页”(用户态 + CPU)

回到浏览器的渲染进程,它开始消化这些字节。这里补一个常被忽略的点:JS 是 CPU 在跑的,而且跑在 OS 给的虚拟内存里

11. 解析与构建:DOM / CSSOM / 渲染树 / 布局 / 绘制(用户态,浏览器引擎主场)

浏览器拿到的只是一长串字节(HTML/CSS/JS 的 UTF-8 原文),要变成能画的画面,得经过”解析 → 建树 → 布局 → 绘制”四步。这整段发生在渲染进程里,是纯 CPU 的活。

HTML 解析:字节怎么变成 DOM 树

  • 第一步词法分析(tokenization):字节流先按编码解码成字符,再被切成一个个标签 token<div>、属性、</div> 等都算 token)。HTML 的词法规则不是正则能搞定的,有一套专门的 tokenizer 状态机。
  • 第二步树构建(tree construction):token 流按 HTML5 规范定义的插入模式状态机被塞进一棵 DOM 树。比如你在 <table> 里直接写 <tr>,规范会”偷偷”补一个 <tbody>——这就是为什么写错嵌套浏览器也能渲染:HTML 解析器内置大量容错修复规则,它从不轻易报错,而是尽力猜你的意图。最终产物就是 DOM 树(页面结构的树,JS 后面能增删改的就是它)。

CSS 解析:样式规则怎么变成 CSSOM 树
CSS 字节也被 tokenize、解析成一条条规则(选择器 + 声明块),组织成 CSSOM 树(CSS Object Model)。它和 DOM 是两套独立树——一个管结构,一个管样式。

渲染树:结构 + 样式第一次合体
浏览器把 DOM 和 CSSOM 合并成渲染树(Render Tree / Layout Tree):它只收录”最终要画出来的元素”——display:none 的节点直接不进(因为它根本不画),但 visibility:hidden 的会进(它占位置、只是看不见);还会补一些 DOM 里没有的匿名盒子(比如一段裸文字被自动包成的匿名块)。渲染树每个节点都带着”算好的最终样式”。

布局(Layout / Reflow):算每个元素在哪、多大
渲染树只说了”谁有什么样式”,还没说位置。布局阶段按 CSS 盒模型(content / padding / border / margin 四层嵌套)和文档流规则(普通流、浮动、绝对/固定定位)递归计算每个盒子的精确几何(x/y/宽/高)。这步也叫 reflow(重排),通常自顶向下递归——一个元素尺寸变了可能牵连整条子树,所以频繁改布局很贵。

绘制(Paint):生成”怎么画”的指令
布局定下几何后,绘制阶段按正确的层叠顺序(z-index、定位、透明度等)把每个元素转成一组绘制指令 / 绘制记录(paint records,例如”画这个矩形、填这个色、画这段文字”)。注意这时还没真正出像素,只是生产”待办清单”。

现代浏览器在这之后还多一步分层与合成(compositing):把某些元素(带 transform/opacity、有 will-change 提示、或视频/Canvas)提升成独立的合成层(composited layer),每层是一张位图,后面由 GPU 单独搬动。这正是为什么 CSS 动画用 transform 比改 left 流畅——前者只动合成层、不触发重排重绘;后者要从布局一路重跑到绘制。

12. JS 引擎:V8 怎么让 JS 跑起来(计算机组成 + OS 交叉点)

那个 3D 场景是 JS 加载并驱动的。JS 表面是”脚本语言”,底下却是一套精密的编译器 + 运行时。以 Chrome 的 V8 引擎为例:

  • 两阶段执行(Ignition + TurboFan):JS 源码先被快速编译成 Ignition 字节码(一种中间指令,由解释器直接跑,启动快);当某段代码被反复执行(成为”热点”),V8 把它交给 TurboFan 优化编译器翻成高度优化的机器码。这就是 JIT(即时编译,Just-In-Time,边跑边编译、越跑越快)。若后来类型变了(比如同一函数突然收到不同类型的参数),TurboFan 的假设失效,V8 会去优化(deoptimize)退回字节码。
  • 隐藏类(Hidden Class / Shape):JS 对象属性看似自由,V8 内部却给每个”形状相同”的对象一个隐藏类,用固定偏移量直接定位属性(像 C++ 结构体),而不是每次哈希查找。属性增删会让对象”切换”到新隐藏类(transition)——所以保持对象形状稳定更快。
  • 内联缓存(Inline Cache, IC):V8 在属性访问/函数调用处记下”上次的类型假设”,下次同类型直接走快路径——单态(monomorphic)最快,类型一杂就退化成多态甚至巨态,变慢。
  • 垃圾回收(GC):JS 不用你手动释放内存。V8 用分代式回收:新对象在”年轻代”,活过几轮的晋升”老年代”;年轻代用 Scavenge(复制存活对象),老年代用 Mark-Sweep-Compact(标记→清除→压缩碎片)。回收会短暂暂停 JS(”GC 停顿”),是偶发卡顿的来源之一。

而这些代码、对象、调用栈,最终都由 CPU 执行、放在 OS 的虚拟内存里:变量/对象在 堆(heap,动态申请的大块内存,由 GC 管),函数调用嵌套在 栈(stack,后进先出的临时记忆区)。你在程序里看到的地址是”虚拟地址”(OS 虚构的整齐空间),OS 和 CPU 在背后悄悄映射到真实物理内存条——写代码时不用管,但要知道这层”虚实映射”存在。

还有个和 3D 直接相关的事:事件循环(event loop)。JS 是单线程的,靠”调用栈 + 宏任务队列 + 微任务队列”调度。你写的 requestAnimationFrame 回调被排进”渲染时机”的宏任务,浏览器在每次刷新前调用它——于是每帧重算旋转角、重画。Promise 的 .then 是微任务,会在当前宏任务结束后、下一个之前清空。理解这个,才明白”为什么我的 3D 循环能在不阻塞页面的情况下一直转”。

JS 加载完 Three.js 这类库,在 <canvas>(一块画布元素)上调用 WebGL / WebGPU,把”画 3D”的活正式交给 GPU。下面进入全文核心。


第三段(核心):GPU 怎么把一个 3D 场景画成 2D 像素

这是 3D 网页和看新闻网页最大的分水岭。我们用一个具体例子贯穿:屏幕中央一个正在旋转的立方体,某一帧是怎么被画出来的

先对齐词:GPU 是块擅长”同时算海量相同任务”的芯片——几万个顶点、几百万像素要算,CPU 一个个算会慢死,GPU 并行干。WebGL/WebGPU 是 JS 和 GPU 之间的翻译官。但浏览器不是直接写 GPU 寄存器,这中间还有 OS,待会儿讲。

13. 顶点着色器与”坐标的旅行”:MVP 矩阵链

立方体由 8 个顶点、12 个三角形拼成。顶点着色器要干的,是把每个顶点从”它自己在模型里的坐标”一路变到”它在你屏幕上该出现的位置”。这段数学旅行分几站,合称 MVP 变换

  • 模型变换(Model):立方体在自己本地坐标里,先旋转、缩放、平移到场景中的某个位置(”旋转”就发生在这里,每帧改一下旋转角,立方体就转起来了)。
  • 视图变换(View):把整个场景摆到”相机(你的眼睛)”的视角下——相当于把世界搬到了相机前面。
  • 投影变换(Projection):把 3D 压成 2D。透视投影会模拟”近大远小”,这就是为什么远处的边看起来短。
  • 之后 GPU 做裁剪(Clipping):把相机视野之外的部分切掉;再做透视除法得到归一化设备坐标(NDC,一个标准的立方体空间);最后视口变换映射到真实屏幕像素。

打个比方:模型变换是”把雕塑摆进展厅并转动它”,视图变换是”你走到一个观看位置”,投影变换是”举起相机按下快门把三维压成照片”,视口变换是”把照片缩放到你手机屏幕大小”。

这背后的数学,是 4×4 矩阵乘一个 4 维向量。 GPU 用齐次坐标:空间中的点写成 [x, y, z, 1],方向向量写成 [x, y, z, 0](最后的 w 区分”点”和”向量”——平移只作用于点、不作用于方向)。模型/视图矩阵 MV 都是刚体变换(旋转、平移、缩放),乘完 w 仍为 1;关键的投影矩阵 P 不一样:透视投影会把 w 设成”与深度成反比”的值(越远的顶点 w 越大)。乘完得到的是”裁剪空间”坐标,GPU 再做透视除法——把 x/y/z 全部除以 w——才得到 NDC(范围 [-1,1])。除以 w 这一下,正是”近大远小”的真正数学来源,也保证三角形内部的颜色、纹理能按透视正确插值(perspective-correct interpolation),否则斜着看贴图会歪。

14. 光栅化:把三角形填进像素网格

顶点着色器给出三角形在屏幕上的位置后,GPU 有一段你改不了的硬件流程 光栅化(Rasterization):它判断这个三角形覆盖了哪些屏幕像素,对每个被覆盖的像素生成一个”片元(fragment,像素候选)”。同时做背面剔除(背对你的三角形不画,省算力)。

光栅化具体怎么”填”? 三角形三个顶点变换到屏幕后,GPU 先做三角形设置(triangle setup),算出三条边的边函数(edge function,用顶点坐标得出的线性方程)。然后对覆盖范围内的每个候选像素,用边函数做符号测试:像素中心落在三条边”内侧”才被覆盖——这就是 coverage(覆盖率)。被覆盖的像素生成一个片元,并带着从三个顶点插值来的属性(颜色、纹理坐标 UV、法线等);插值用重心坐标,且因上节那个 w 而做透视校正,保证斜看也正确。

两个相关细节:Early-Z(提前深度测试)在片元着色之前先用 Z-buffer 粗筛掉肯定被挡住的片元,省掉后面昂贵的着色计算;MSAA(多重采样抗锯齿)对每个像素取多个子样本位置分别测覆盖,边缘更平滑——这就是为什么 3D 物体斜边不”狗牙”。

15. 深度缓冲:3D 和 2D 最本质的区别(必讲)

这是 3D 渲染的命门,却常被科普略过。屏幕是 2D 的,但场景是 3D 的——当立方体的正面和背面都投影到同一块屏幕区域时,到底画哪一个?

GPU 用一块 深度缓冲(Z-buffer,深度缓冲区) 解决:每个像素除了存颜色,还存一个”离相机多远”的深度值。画每个片元时,GPU 先比一下”我这个片元是不是比已经画在那儿的更近”——更近才画(更新颜色和深度),更远就丢弃。这样无论三角形以什么顺序提交,最终留下的都是离眼睛最近的那一层

没有 Z-buffer,你就分不清立方体的前面挡住了后面,看到的会是一团乱麻。它是”3D 能正确显示”的基石,也是 3D 区别于纯 2D 绘图的根本所在。

16. 片元着色器:每个像素最终是什么颜色

通过深度测试的片元,交给片元着色器算最终颜色——它是一段在 GPU 上对每个片元运行的小程序(你用 GLSL/WGSL 写,编译后在 GPU 海量核心上并行跑)。它拿到的输入是上一步插值来的属性(UV 纹理坐标、法线、世界坐标),输出这个片元最终的 RGBA 颜色,写进帧缓冲。常见计算包括:

  • 光照:比如 Lambert 漫反射用 点积(表面法线 N, 指向光源方向 L) 算明暗(N·L 越大越亮);Phong/Blinn 再加一层高光。
  • 纹理采样:用 UV 去贴图上取颜色,GPU 自动做双线性插值(四个相邻 texel 混出平滑结果);缩小时还会用 mipmap(预先缩好的多层贴图)避免远处闪烁。
  • 若开启混合(blending),新颜色还会和帧缓冲里已有的颜色按规则混合(比如半透明)。

回到那个比喻:顶点着色器决定”布的每个角钉在墙上哪”,光栅化决定”布覆盖了哪些墙砖”,Z-buffer 决定”前后两层布哪层在前”,片元着色器决定”每块砖上印的是什么花纹和明暗”。

17. 命令怎么送到 GPU:command buffer + OS 驱动(操作系统 + 计算机组成)

前面一直说”浏览器调用 WebGL 画”,但浏览器作为用户态程序不能直接操纵 GPU 硬件。真实流程是:

  1. 浏览器把”画这几个三角形、用这个着色器”的指令,写进一块共享内存(shared memory,浏览器进程和驱动双方都能读写的同一块内存区域,用来高效传递指令)——命令缓冲区(command buffer)
  2. 通过 OS 的 GPU 驱动(Windows 上是 WDDM 模型、Linux 上是 Mesa,都是 OS 用来”对接某款显卡”的软件层)提交。驱动是 OS 里专门伺候 GPU 的代码,把通用命令翻译成这款 GPU 懂的指令,并负责调度。
  3. GPU 接收命令,在它自己的 显存(VRAM) 里读顶点/纹理数据,调动成千上万个核心并行执行顶点/片元着色器,结果写回显存里的帧缓冲。

这里又一次看到分层职责:浏览器只管”我要画什么”(写命令),OS 驱动管”怎么跟这款 GPU 说”(翻译+调度),GPU 管”大规模并行算出来”(执行)。GPU 本质是 CPU 之外另一块”协处理器”,有自己的存储层次(显存)和调度器——这是计算机组成原理里”多处理器协作”的体现。

18. 逐帧动画与垂直同步:为什么转起来不撕

旋转立方体要连续动,靠 requestAnimationFrame:浏览器在每次屏幕刷新前调用你的绘制函数,于是每帧重算旋转角、重画。画好的帧先写进帧缓冲。

为什么会有撕裂、VSync 又怎么治? 显示器显示靠两块缓冲:前缓冲(正在被显示读的那块)后缓冲(GPU 正在画的那块)。理想情况是 GPU 画完后,在显示器”扫完一整屏、准备下一屏”的间隙(垂直消隐期 vblank)把两块交换(swap)。如果 GPU 画得太快、在显示器正扫到一半时就把后缓冲换上,同一屏里上半是旧画面、下半是新画面——这就是撕裂(tearing)

垂直同步(VSync)让 GPU 必须等到 vblank 才允许交换,画面永远完整。代价是:若 GPU 没在 16.7ms(60Hz)内画完这一帧,就只能沿用上一帧、等下一个 vblank 再换——掉帧、输入延迟变大三重缓冲(triple buffering)用第三块缓冲缓解:GPU 可提前画下一帧而不阻塞,减少等待。交换链(swap chain)就是 OS/驱动管理这几块缓冲、按时呈现的机制。

这里还有个 OS 与 GPU 协同的细节:浏览器写好的帧,通常先交给 OS 的窗口合成器——见下一段。


第四段:从帧缓冲到眼睛(补 OS 窗口合成器 + 显示硬件)

画面已经在显存里的帧缓冲了,剩下是物理世界的事。这段最容易被忽略,却最贴近”你真正看见了”。

19. OS 窗口合成器:你看到的屏幕,是 OS 拼出来的(操作系统层)

屏幕上有很多窗口——浏览器窗口、任务栏、别的 App。谁来决定最终屏幕上显示什么?是 OS 的窗口合成器(compositor,负责把多个窗口的画面叠在一起、合成最终一帧的组件):Windows 的 DWM、macOS 的 WindowServer、Linux 的 Wayland/Mutter(这三分别是三家系统各自的合成器名字)。

浏览器画好的 3D 帧,只是”浏览器这个窗口的一块图层”。合成器把所有窗口的图层(各自可能由 GPU 画好)合成(composite)成一张最终画面,再交给显示驱动。这就是为什么你能把浏览器窗口拖到别的窗口上面——是 OS 在实时决定叠放和显示。

合成器具体怎么”叠”? 每个窗口(甚至窗口内的合成层)在 GPU 里都是一张纹理(texture),合成器按窗口的位置、大小、透明度、层级(z-order),用 GPU 把它们在独立的合成线程上实时叠成最终画面。把合成单开一个”合成线程”,是为了即使主线程(JS/布局)卡了,滚动和动画依然流畅——这是现代浏览器响应不卡的关键设计。它还用脏矩形(damage region)只重绘发生变化的那一小块,省算力。

这又是一个操作系统职责:多窗口管理与显示仲裁。浏览器只管自己的窗口内容,最终”屏幕上这一刻长什么样”由 OS 拍板。

20. 显示子系统:DDIC 在幕后搬像素(硬件层)

合成好的画面,通过 显示接口(笔记本内部是 eDP,外接是 HDMI / DisplayPort) 送到显示器里的 显示驱动芯片(DDIC,Display Driver IC)。DDIC 是面板上的”搬运工”兼”指挥”:

  • 它按像素时钟(pixel clock)行列同步信号(HSync / VSync)接收帧数据,收到的其实是一串按行排列的像素值。
  • 面板内部还有个 时序控制器(TCON,Timing Controller),指挥行驱动(gate driver)逐行打开像素、列驱动(source driver)把对应电压/电流灌进每个子像素——一帧就是从上到下、从左到右一遍遍扫出来的。
  • 你看到的”动态画面”,本质是它每秒几十到几百次(对应刷新率)地重复这套扫描。60Hz 就是每秒重扫 60 遍同一块面板。

21. 两种屏幕,两种发光哲学:LCD 与 OLED

  • LCD(液晶):自己不发光,背后一整块背光板常亮,前面液晶分子像百叶窗转动控制透光多少,再过红绿蓝滤光片成彩色。黑只是”挡住光”,故黑不够黑。
  • OLED:每个像素自己是小灯泡,亮处通电发光、黑处断电熄灭——能纯黑、对比极高、响应极快。

护眼冷知识:不少 OLED 低亮度用 PWM 调光——像素以 240–480Hz 极高频率”亮灭亮灭”调亮度,人眼主观看不出闪,但敏感人群久看可能疲劳。这不是 OLED 更伤眼,只是权衡不同。

两个加深细节:子像素排布——LCD/OLED 每个”像素”其实由红绿蓝三个子像素(subpixel)拼成;不少 OLED 用 PenTile 排列(子像素共享)省成本,近看能发现点阵感。响应时间——LCD 液晶转动要时间(几到十几毫秒),快速移动画面会”拖影”;OLED 自发光响应极快,拖影几乎不可见。

22. 最后一跳:光子打进视网膜,大脑说”我看见了”(生理层)

被点亮的像素发出携带颜色的光子,穿过空气、角膜、晶状体,聚焦在眼球后壁视网膜上。视网膜是眼球内壁上铺满感光细胞的”底片”:约 1.2 亿个视杆细胞(rods)管暗光下的明暗(对颜色不敏感、灵敏度极高),约 600 万个视锥细胞(cones)管彩色和细节——视锥高度集中在视网膜中央一块叫黄斑(macula)/ 中央凹(fovea)的小区域,那里是你视觉最锐利的地方(盯屏幕中心、看书用的就是它)。光子打到这些细胞,触发光化学反应(视紫红质等)把光变成电信号,经双极细胞、神经节细胞汇总,沿视神经(约 100 万根纤维,相当于把上亿感光信号压缩后传出)送进大脑视觉皮层做识别与整合,你才”看见”了那个旋转立方体。

常见误解”视觉暂留”——其实更关键的是临界融合频率(CFF,Critical Fusion Frequency):当画面刷新够快(通常 50–60Hz 以上,且亮度越高阈值越高),大脑运动系统会把离散的帧融合成连续运动(φ 现象),而不是真的”残留影像”。只要帧率够、每帧间隔短于融合阈值,你就觉得是流畅转动,而非一张张闪过的图。


写在最后:一张”谁在干活”的总览表

把整条链路串起来,你按下回车到看见旋转的 3D 立方体,是一场跨八领域的接力。每一步的数据形态、谁在干活、负什么责,我整理成一张表,方便你随时回查:

阶段 哪一层在干活 职责 数据长什么样
按键 硬件(键盘 MCU) 扫描矩阵生成扫描码 矩阵坐标 → 扫描码
通知 CPU 计算机组成(中断) IRQ 打断 CPU、保存现场跳 ISR 电信号/中断号
翻译成按键 操作系统(驱动+输入子系统) 读扫描码→虚拟键码→事件,按焦点投递 按键事件
收到回车 用户态(浏览器进程) 识别导航,交网络进程 URL 字符串
查 IP 网络(应用层 DNS) 浏览器缓存→解析器→根→TLD→权威,逐级查 IP DNS 报文 / IP
建连接 网络(传输层 TCP) 三次握手 SYN→SYN-ACK→ACK,互验收发能力 TCP 段
加密 网络(TLS) 证书验身 + 协商对称密钥 加密握手报文
发请求 操作系统(系统调用+网络栈+驱动)+DMA socket 调用、封包(段→包→帧)、网卡 DMA 发出 以太网帧
收响应 同上(逆向解封装) 拆帧→拆包→拆段重组确认,交还浏览器 应用数据
解析页面 用户态(渲染进程)+CPU+虚拟内存 建 DOM/CSSOM/渲染树、布局绘制 渲染树
跑 JS 加载 3D 用户态(V8)+CPU+OS 虚拟内存 JIT 编译执行、拉模型、调 WebGL 机器码/对象
算顶点位置 GPU(顶点着色器)+显存 MVP 变换:模型→视图→投影→屏幕 屏幕坐标
填像素 GPU(光栅化) 三角覆盖像素、背面剔除 片元
决定谁在前 GPU(Z-buffer) 深度测试保最近层 深度值
算颜色 GPU(片元着色器) 光照/纹理/材质 RGBA
提交绘制 用户态+OS(GPU 驱动) 命令缓冲区→驱动翻译调度 GPU 指令
合成窗口 操作系统(窗口合成器) 多窗口图层合成最终画面 合成后帧
刷面板 硬件(DDIC+面板) 像素逐点搬到面板 像素电平
发光 硬件(LCD/OLED) 背光穿透 / 像素自发光
成像 生理(视网膜+大脑) 光变电信号、皮层合成 神经信号

整件事实则横跨:计算机组成原理(I/O、中断、DMA、存储层次、协处理器)、操作系统(驱动、系统调用、进程调度、虚拟内存、网络栈、窗口系统)、网络、浏览器引擎、图形 API、GPU 架构、显示硬件、视觉神经。**任何一个环节拖后腿,你感受到的就是”慢”或”卡”**——键盘驱动崩了收不到输入、DMA 没配好网卡吞吐暴跌、V8 卡顿 JS 加载慢、着色器写挫 GPU 跟不上、没开 VSync 撕裂、面板响应慢拖影,各有各的毛病。

写到这里,从你手指按下回车的那一瞬,到旋转立方体最终点亮视网膜上的感光细胞,整条链路应该已经在你脑子里连成一条线了。

你会发现一件很有意思的事:这条链路上,没有任何一个环节”懂”整个画面。键盘只懂”哪根线路通了”,CPU 只懂”下一条指令是什么”,GPU 只懂”把这个顶点乘哪个矩阵”,显示器只懂”这个像素该亮什么颜色”。它们各自只会自己那点事,却靠着一层层清清楚楚的责任划分——硬件归硬件、内核归内核、应用归应用——恰好接力拼出了你眼前这个能拖动、能旋转的三维世界。

而这恰恰是计算机最迷人的地方:无数简单、甚至笨拙的零件,被分好工、串成链、踩准毫秒级的节拍协同起来,最终托起了我们习以为常的”理所当然”。下次你再敲下网址、看着场景转起来,希望你能会心一笑——原来屏幕背后,是这样一长串精密到令人安心的协作。