一、渲染流程
把整个过程想象成“你在网上点了一份外卖,从下单到吃进嘴里”的全过程。 整个过程可以分为两大阶段:网络层(找外卖、等外卖) 和 渲染层(拆包装、摆盘吃)。考虑到你提到的各种特殊情况,我为你详细梳理如下:
第一阶段:网络层(找外卖、等外卖)
1. URL 解析与导航(确认外卖地址)
当你敲下网址(如 https://www.example.com)并按下回车时,浏览器首先要“审题”:
- URL 合法吗? 浏览器会检查你输入的到底是网址还是搜索词。如果格式不对,它可能会自动帮你补全(比如加上 https://),或者直接当成搜索词丢给搜索引擎。
- 安全拦截: 浏览器还会检查这个网址是不是已知的恶意网站,如果是,就会弹出警告页面。
2. DNS 域名解析(查电话簿)
电脑只认 IP 地址(如 192.168.1.1),不认识网址。所以浏览器要开始“查电话簿”:
- 缓存命中(秒查): 浏览器会先查自己的缓存,再查操作系统的缓存。如果最近刚访问过,直接拿到 IP,速度极快。
- 缓存未命中(逐级问路): 如果缓存里没有,浏览器就会去问本地 DNS 服务器(通常是你的宽带运营商),如果它也不知道,就会一层层往上问:根域名服务器 → 顶级域服务器(如 .com) → 权威域名服务器,直到拿到 IP 地址。
DNS 查询的层级结构可以简要概括为: 浏览器缓存 → 操作系统缓存 → 本地 DNS 服务器 → 根域名服务器 → 顶级域服务器 → 权威域名服务器
3. 建立 TCP 连接(打电话确认)
拿到 IP 后,浏览器要和服务器建立连接,也就是经典的 “TCP 三次握手”:
| 第一次握手 | 浏览器 | SYN | “喂,你在吗?” |
| 第二次握手 | 服务器 | SYN-ACK | “我在,你听得到吗?” |
| 第三次握手 | 浏览器 | ACK | “听得到,那我们开始聊吧!” |
4. TLS 握手(HTTPS 专属:对暗号与验明正身)
如果你用的是 https://,在 TCP 连接后,还要进行安全验证(TLS 协商)。这就像接头时对暗号:
- 浏览器和服务器要商量好用什么方式加密数据,服务器还要出示自己的 “数字证书” 证明身份。
- 代价: 这会额外增加几次网络往返,所以 HTTPS 会比 HTTP 稍微慢一点点,但换来的是绝对的安全。
5. 发送 HTTP 请求与接收响应(下单与收货)
- 发请求: 浏览器向服务器发送请求:“请把首页的 HTML 给我!”
- 服务器处理: 后端可能要去查数据库、做业务逻辑。
- 收响应: 服务器返回状态码,并把 HTML 文件像快递一样发给浏览器。
常见的 HTTP 状态码:
| 200 | OK | 请求成功 |
| 301 | Moved Permanently | 永久重定向到另一个网址 |
| 404 | Not Found | 找不到页面 |
| 500 | Internal Server Error | 服务器内部错误 |
第二阶段:渲染层(拆包装、摆盘吃)
浏览器拿到 HTML 后,真正的“画画”工作就开始了。现代浏览器是多进程的,真正干活的是 渲染进程:
6. 解析 HTML,构建 DOM 树(搭骨架)
浏览器逐行读取 HTML 代码,把它转换成内存中的 “文档对象模型(DOM)树”。这就像搭房子的钢筋混凝土骨架。
7. 解析 CSS,构建 CSSOM 树(定装修)
同时,浏览器会解析 CSS 样式表,生成 CSSOM 树。这决定了骨架上哪里刷什么颜色的漆。
注意: 如果 CSS 还没下载完,浏览器不会把半成品展示给你看(防止页面闪烁),它会等 CSS 解析完再往下走。这也是为什么我们通常建议把关键 CSS 内联在 <head> 中,或尽早加载 CSS 文件。
8. 构建渲染树(Render Tree)
浏览器把 DOM 树和 CSSOM 树合并,生成 “渲染树”。
关键点: 渲染树只包含需要显示的元素。像 <head> 标签或者被 CSS 设置为 display: none 的元素,会被直接扔掉,不浪费算力。
9. 布局(Layout / 回流 Reflow)
浏览器开始计算渲染树上每个节点在屏幕上的确切位置和大小(比如这个按钮距离左边多少像素,高度是多少)。
10. 绘制(Paint / 重绘 Repaint)
根据计算好的位置和样式,浏览器调用图形库,把节点真正画在屏幕上,填充颜色、绘制文本和边框。
11. 合成(Compositing)与显示
如果页面有复杂的层叠关系或动画,浏览器会把不同的图层像叠透明塑料片一样合并在一起,最终通过 GPU 显示到你的屏幕上。
回流(Reflow)与重绘(Repaint)的区别:
- 回流:元素的几何属性(位置、大小)发生变化,需要重新计算布局,代价较高。
- 重绘:元素的外观(颜色、背景等)发生变化,但不影响布局,代价相对较低。
- 合成(Compositing)是最高效的,因为它不触发回流和重绘,直接由 GPU 处理。
二、URL组成
一个标准的 URL 格式长这样:
协议://域名:端口/路径?查询参数#哈希值
完整示例:
https://www.shop.com:443/goods/detail?id=123&color=red#comments
1. 协议 (Scheme) —— https://
- 是什么: 规定了浏览器和服务器之间用什么“语言”来交流。
- 实际开发中的例子:
- http:// 或 https://:最常见的网页协议。
- ftp://:文件传输协议,用来下载文件的。
- mailto::点击后会唤起你电脑上的邮件软件(比如 mailto:admin@test.com)。
- 大师避坑: 现在为了安全,浏览器对 HTTP 和 HTTPS 混用的限制非常严。如果你的网页是 HTTPS 的,但里面引用了一个 HTTP 的图片,浏览器通常会直接拦截并报错(这叫 混合内容安全策略)。
2. 域名 (Host) —— www.shop.com
- 是什么: 服务器的“名字”或“门牌号”。
- 实际开发中的例子: 就像我们之前聊过的,浏览器拿到这个名字后,会去问 DNS 服务器,把它翻译成机器能懂的 IP 地址(比如 192.168.1.1)。
3. 端口 (Port) —— :443
- 是什么: 服务器上具体负责接待的“服务窗口”。一台服务器可以同时跑很多个服务,端口就是用来区分它们的。
- 实际开发中的例子:
- HTTP 默认端口是 80,HTTPS 默认端口是 443。
- 注意: 如果使用的是默认端口,URL 中通常会省略不写(就像上面的例子,虽然默认是 443,但一般不写出来)。只有在开发环境或者特殊配置时,你才会看到类似 http://localhost:3000 这样的地址。
4. 路径 (Path) —— /goods/detail
- 是什么: 服务器上具体文件的“存放位置”。
- 实际开发中的例子: 这就好比去大商场,/goods 是商品区,/detail 是详情页。在后端代码里,这个路径通常会被路由系统拦截,然后返回对应的页面或数据。
5. 查询参数 (Query) —— ?id=123&color=red
- 是什么: 以 ? 开头,用来给服务器传递额外信息的。多个参数之间用 & 连接,键和值之间用 = 连接。
- 实际开发中的例子:
- 你在淘宝搜索“手机”,URL 会变成 search?q=手机。
- 分页功能:/list?page=2&size=10(告诉服务器我要第 2 页,每页 10 条数据)。
- 大师避坑: 作为前端,我们经常需要获取这些参数。在 Vue/React 等现代框架中,有现成的 API(如 useSearchParams)可以直接拿到 { id: '123', color: 'red' }。
6. 哈希值 (Hash) —— #comments
- 是什么: 以 # 开头,也叫锚点。它的作用是在页面内部进行定位。
- 实际开发中的例子:
- 页面跳转: 知乎或长文章里,点击“跳转到评论区”,URL 后面就会加上 #comments,页面会自动滚动到评论区的位置。
- 前端路由: 早期的 Vue/React 单页应用(SPA)就是用 Hash 模式来做路由的,比如 /#/user/profile。因为 # 后面的内容永远不会发送给服务器,所以刷新页面也不会报 404 错误。
三、DNS服务器
1. 什么是 DNS 服务器?(通俗理解)
DNS(Domain Name System,域名系统)被称为 “互联网的电话簿”。
- 人类习惯: 我们记不住 139.186.239.126 这样一串毫无规律的 IP 地址,我们只记得 cloud.tencent.com。
- 机器习惯: 电脑和网络设备之间通信,只认 IP 地址。
DNS 服务器就是负责把 “人类好记的域名” 翻译成 “机器好懂的 IP 地址” 的计算机。当你在浏览器输入网址时,浏览器就会去问 DNS 服务器:“这个网址对应的 IP 是多少?”
2. DNS 解析的“问路”流程(核心重点)
DNS 并不是一个单一的服务器,而是一个层级分明、互相配合的系统。当你访问一个网站时,通常会经历以下几个角色的接力:
第一步:找本地 DNS 服务器(你的专属向导)
当你输入网址,浏览器首先会问你的 “本地 DNS 服务器”(通常由你的宽带运营商 ISP 提供,或者你手动设置的公共 DNS)。
| 情况 A(缓存命中) | 如果向导最近刚查过这个网址,它会直接告诉你 IP 地址 | 速度非常快 |
| 情况 B(缓存未命中) | 向导也不知道,它就会代替你,一层一层往上问 | 需要逐级查询 |
第二步:问根域名服务器(互联网总枢纽)
向导会去问根域名服务器(全球只有 13 个逻辑根服务器):“你知道 tencent.com 在哪吗?”
根服务器自己也不存具体的 IP,但它会指路:“我不清楚,但你去问负责 .com 的顶级域名服务器吧。”
第三步:问顶级域名服务器(TLD Server)
向导顺着线索,去问 .com 的 TLD 服务器。TLD 服务器会说:“tencent.com 的具体信息,你去问它的权威域名服务器吧。”
第四步:问权威域名服务器(最终答案)
向导最后找到腾讯的权威域名服务器,这里真正存储了 cloud.tencent.com 对应的 IP 地址。权威服务器把 IP 交给向导,向导再把 IP 交还给你的浏览器。浏览器拿到 IP,就可以开始建立 TCP 连接了!
DNS 解析的完整流程可以概括为: 浏览器 → 本地 DNS 服务器 → 根域名服务器 → 顶级域服务器(TLD) → 权威域名服务器 → 拿到 IP 地址
四、基于DNS的前端优化
1. DNS 预解析 (dns-prefetch)
既然查电话簿需要时间,我们可以在页面加载初期,就让浏览器“提前查好”后续需要用到的域名。
- 怎么做: 在 HTML 的 <head> 标签中,添加 <link rel="dns-prefetch" href="//xxx.com">。
- 实战举例: 如果你的页面用到了第三方的统计代码(比如 stats.example.net)或者外部字体(比如 fonts.googleapis.com),提前预解析它们,能省下约 150ms 到 300ms 的等待时间。
2. 预连接 (preconnect)
比 dns-prefetch 更进一步。它不仅提前查电话簿(DNS 解析),还提前和服务器建立 TCP 连接,甚至完成 TLS 握手(HTTPS 安全验证)。
- 怎么做: <link rel="preconnect" href="https://cdn.example.com">。
- 实战举例: 对于首屏最关键的资源(比如核心 JS/CSS 所在的 CDN 域名),使用 preconnect 收益极高。
3. 客户端/原生层面的优化
如果你开发的是 App 内嵌的 H5 页面(WebView),可以利用原生客户端的能力来优化:
- WebView 预热: 在 App 启动时,后台悄悄创建一个 1×1 像素的隐藏 WebView,提前去解析常用域名的 IP 并缓存起来。等用户真正打开 H5 页面时,DNS 解析瞬间完成。
- IP 直连 / HTTPDNS: 传统的 DNS 容易受到运营商劫持(被恶意篡改 IP 或强插广告)。通过 HTTPDNS 技术,客户端可以直接通过 HTTP 协议向可靠的 DNS 服务器请求 IP,拿到 IP 后直接发起网络请求,既防劫持又提速。
4. 服务端/运维层面的优化(需与后端配合)
虽然这是后端的工作,但前端了解它能更好地理解全局:
- 智能 DNS (GSLB): 通过 DNS 识别用户的地理位置和网络运营商,把用户调度到最近的服务器。比如广州电信用户访问,就解析到广州电信的机房 IP,避免跨地域、跨运营商的延迟。
- 合理设置 TTL(缓存时间): TTL 决定了 DNS 结果在本地缓存多久。如果网站 IP 经常变动,TTL 设短点;如果很稳定,TTL 设长点,减少频繁查电话簿的开销。
| dns-prefetch | 仅 DNS 解析 | 节省 20~120ms | 第三方资源、外部域名 |
| preconnect | DNS + TCP + TLS | 节省 100~300ms | 关键 CDN、字体库 |
| HTTPDNS | 绕过运营商 DNS | 防劫持 + 提速 | 移动端 App |
| WebView 预热 | DNS 缓存 | 几乎 0 延迟 | App 内嵌 H5 |
五、DNS预解析 / 预连接底层原理
要理解这两个技术为什么能提速,我们首先要知道,浏览器加载一个跨域资源(比如 CDN 上的 CSS 或图片),默认必须 “串行” 走完三个步骤:
如果等到真正需要加载资源时才去走这三步,就会发生阻塞。而 dns-prefetch 和 preconnect 的核心思想就是:把串行的工作提前做,把耗时的等待变成并行的准备。
DNS 预解析 (dns-prefetch)
底层实现:
它只做一件事:提前把域名解析成 IP 地址,并将结果缓存在本地操作系统中。它不会建立 TCP 连接,也不会进行 TLS 握手。
为什么更快?
消除网络往返延迟:DNS 解析通常需要 20~120ms(弱网下甚至更长)。如果不预解析,当浏览器解析到 <img src="…"> 时,必须停下来去查 IP。而使用 dns-prefetch 后,浏览器在解析 HTML 的早期空闲时间,就提前把 IP 查好并放进了本地缓存。等真正加载图片时,直接读取本地缓存,耗时几乎为 0。
预连接 (preconnect)
底层实现:
它比 dns-prefetch 做得更多。它不仅提前解析 DNS,还会提前完成 TCP 握手,如果是 HTTPS 网站,还会提前完成 TLS 握手。
为什么更快?
消除多重握手延迟:TCP 握手需要 1 个网络往返(RTT),TLS 握手需要 1~2 个 RTT。预连接把这些耗时的步骤提前到了页面加载初期,与 HTML 解析并行执行。当后续真正发起请求时,发现连接已经建好,直接复用,从而消除了建立新连接的延迟。
| dns-prefetch | ✅ | ❌ | ❌ | 低 |
| preconnect | ✅ | ✅ | ✅ | 较高 |
避坑指南
-
黄金搭档: 对于支持 preconnect 的现代浏览器,它会完成全套准备;对于不支持的老旧浏览器,它会“降级”去执行 dns-prefetch,保证兼容性。
<link rel="preconnect" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com"> -
千万不要滥用: 预连接(preconnect)会占用浏览器的 CPU 和网络带宽。如果预连接了但最后没用到,就是白白浪费资源。最佳实践是:只对首屏最关键的 3~6 个第三方域名(如核心 CDN、字体库)使用 preconnect,其余的跨域域名使用 dns-prefetch 即可。
-
同源无效: 这两个标签只对跨域资源有效。不要对当前网站的域名做预解析或预连接,因为浏览器在加载当前页面时早就建好连接了,写了也是白写。
六、回流与重绘
为了让你通俗易懂地理解,我们继续用“画家画画”的例子来打比方。
- 回流(Reflow): 相当于“重新排版”。如果你改变了元素的大小、位置,画家就必须重新计算整个页面的排版,非常耗时。
- 重绘(Repaint): 相当于“重新上色”。如果你只是改变了元素的背景色或文字颜色,元素的位置和大小没变,画家只需要重新涂一下颜色即可,相对省时。
1. 什么是回流(Reflow)?
当 DOM 结构、元素的几何尺寸(宽、高、边距、位置等)发生变化时,浏览器需要重新计算元素的几何属性,并重新构建渲染树。
触发条件:
- 修改元素的宽高、内外边距(margin/padding)。
- 改变元素的定位(position、top/left)。
- 添加或删除可见的 DOM 元素。
- 获取某些特定属性(如 offsetTop、scrollTop、getComputedStyle())。【重点避坑】
代价: 极其昂贵。一旦回流,必然伴随重绘。
2. 什么是重绘(Repaint)?
当元素的外观发生变化,但不影响其几何布局时,浏览器只需要重新绘制该元素。
触发条件:
- 修改背景色(background-color)。
- 修改文字颜色(color)。
- 修改可见性(visibility: hidden)。
代价: 相对较小。重绘不一定会触发回流。
3. 优化指南
① 批量修改样式(避免连续回流)
错误示范: 用 JS 连续修改元素的多个样式。这会触发 3 次回流!
element.style.width = '100px';
element.style.height = '200px';
element.style.padding = '10px';
优化方案: 把样式写在一个 CSS 类里,然后用 JS 一次性切换类名。只触发 1 次回流。
element.classList.add('new-style');
② 避免频繁读取几何属性(强制同步布局)
错误示范: 在循环中交替“修改样式”和“读取属性”。浏览器为了给你准确的值,会强制立即触发回流。
const box = document.getElementById('box');
// 在循环中交替“修改”和“读取”,浏览器会被迫每次都重新计算
for (let i = 0; i < 1000; i++) {
box.style.top = box.offsetTop + 1 + 'px'; // 读取 offsetTop 会强制浏览器立刻回流!
}
优化方案: 把读取操作集中起来,或者缓存到变量中。
const box = document.getElementById('box');
// 先把需要的值读出来,存到变量里
let currentTop = box.offsetTop;
for (let i = 0; i < 1000; i++) {
currentTop += 1;
box.style.top = currentTop + 'px'; // 只修改,不读取,浏览器可以批量合并回流
}
③ 使用离线 DOM 操作
场景: 需要向页面中插入 1000 个列表项。如果直接 appendChild 1000 次,会触发 1000 次回流。
const ul = document.getElementById('list');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.innerText = `Item ${i}`;
ul.appendChild(li); // 每次循环都会触发一次回流,页面要回流 1000 次!
}
优化方案: 使用 DocumentFragment(文档碎片)。先在内存中把 1000 个元素拼好,最后一次性插入 DOM,只触发 1 次回流。
const ul = document.getElementById('list');
const fragment = document.createDocumentFragment(); // 创建一个内存中的“隐形容器”
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.innerText = `Item ${i}`;
fragment.appendChild(li); // 添加到隐形容器中,不触发回流
}
ul.appendChild(fragment); // 一次性把 1000 个 li 塞进真实 DOM,只触发 1 次回流!
④ 动画优化(使用 CSS3 或 transform)
场景: 让一个方块从左移到右。
错误示范: 用 JS 定时器不断修改 left 或 top 属性,这会疯狂触发回流,导致动画卡顿。
const box = document.getElementById('box');
let left = 0;
setInterval(() => {
left += 1;
box.style.left = left + 'px'; // 修改 left 会触发回流,CPU 疯狂计算,动画卡顿
}, 16); // 16ms 约等于 60fps
优化方案: 使用 CSS3 的 transform: translateX()。transform 会开启 GPU 硬件加速,直接在合成层处理,完全不触发回流和重绘,动画如丝般顺滑。
const box = document.getElementById('box');
let x = 0;
setInterval(() => {
x += 1;
// transform 不会改变文档流,直接在 GPU 的“合成层”处理,完全不触发回流!
box.style.transform = `translateX(${x}px)`;
}, 16);
网硕互联帮助中心



评论前必须登录!
注册