Skip to content

题目 ​

请详细说明从输入 URL 到页面展示发生了什么(DNS 解析 → TCP 握手 → 发送请求 → 接收响应 → 解析 HTML → 构建 DOM/CSSOM → 生成 Render Tree → 布局 → 绘制)。

📝 标准答案 ​

核心要点 ​

完整流程分为 9 个主要步骤:

  1. URL 解析:解析 URL 的各个部分(协议、域名、端口、路径等)
  2. DNS 解析:将域名解析为 IP 地址
  3. 建立 TCP 连接:三次握手建立连接
  4. 发送 HTTP 请求:构造并发送请求报文
  5. 服务器处理请求:服务器接收并处理请求
  6. 接收 HTTP 响应:接收响应报文
  7. 解析 HTML:构建 DOM 树
  8. 加载资源:加载 CSS、JS、图片等资源
  9. 渲染页面:构建渲染树、布局、绘制

详细说明 ​

1. URL 解析 ​

https://www.example.com:443/path/page.html?query=value#hash

协议:https
域名:www.example.com
端口:443(HTTPS 默认端口)
路径:/path/page.html
查询参数:query=value
哈希:hash

浏览器检查:

  • URL 是否合法
  • 协议是否支持(http、https、file 等)
  • 是否需要编码(中文、特殊字符)

2. DNS 解析(域名 → IP 地址) ​

www.example.com → 93.184.216.34

DNS 查询顺序:

1. 浏览器缓存
   ↓ 未找到
2. 操作系统缓存(hosts 文件)
   ↓ 未找到
3. 路由器缓存
   ↓ 未找到
4. ISP DNS 服务器
   ↓ 未找到
5. 根 DNS 服务器
   ↓
6. 顶级域名服务器(.com)
   ↓
7. 权威 DNS 服务器(example.com)
   ↓
返回 IP 地址

DNS 解析过程:

javascript
// 递归查询示例
浏览器 → 本地 DNS 服务器:查询 www.example.com
本地 DNS → 根服务器:查询 www.example.com
根服务器 → 本地 DNS:返回 .com 服务器地址
本地 DNS → .com 服务器:查询 www.example.com
.com 服务器 → 本地 DNS:返回 example.com 服务器地址
本地 DNS → example.com 服务器:查询 www.example.com
example.com 服务器 → 本地 DNS:返回 93.184.216.34
本地 DNS → 浏览器:返回 93.184.216.34

优化:

  • DNS 预解析:<link rel="dns-prefetch" href="//example.com">
  • DNS 缓存:减少查询次数
  • 使用 CDN:就近访问

🧠 深度理解 ​

3. 建立 TCP 连接(三次握手) ​

客户端                           服务器
  |                                |
  |  SYN (seq=x)                   |
  |------------------------------->|
  |                                |
  |  SYN-ACK (seq=y, ack=x+1)      |
  |<-------------------------------|
  |                                |
  |  ACK (ack=y+1)                 |
  |------------------------------->|
  |                                |
  |  连接建立,开始传输数据         |

三次握手的原因:

  • 确认双方的收发能力
  • 防止已失效的连接请求突然又传到服务器
  • 同步序列号

HTTPS 还需要 TLS 握手:

1. Client Hello(客户端支持的加密套件)
2. Server Hello(选择的加密套件)
3. Certificate(服务器证书)
4. Server Key Exchange(密钥交换)
5. Client Key Exchange(客户端密钥)
6. Change Cipher Spec(切换到加密通信)
7. Finished(握手完成)

4. 发送 HTTP 请求 ​

http
GET /path/page.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: text/html,application/xhtml+xml
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: session_id=abc123
Cache-Control: max-age=0

请求组成:

  • 请求行:方法、路径、协议版本
  • 请求头:各种元信息
  • 请求体:POST 等方法的数据

5. 服务器处理请求 ​

1. 接收请求
2. 解析请求(路由匹配)
3. 业务逻辑处理
4. 数据库查询
5. 生成响应内容
6. 返回响应

6. 接收 HTTP 响应 ​

http
HTTP/1.1 200 OK
Date: Wed, 27 Nov 2025 12:00:00 GMT
Server: nginx/1.18.0
Content-Type: text/html; charset=UTF-8
Content-Length: 1234
Cache-Control: max-age=3600
ETag: "abc123"
Connection: keep-alive

<!DOCTYPE html>
<html>
<head>
  <title>Example</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h1>Hello World</h1>
  <script src="script.js"></script>
</body>
</html>

响应组成:

  • 状态行:协议版本、状态码、状态描述
  • 响应头:各种元信息
  • 响应体:HTML 内容

7. 解析 HTML(构建 DOM 树) ​

html
<!DOCTYPE html>
<html>
  <head>
    <title>Example</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <div id="app">
      <h1>Hello</h1>
      <p>World</p>
    </div>
    <script src="script.js"></script>
  </body>
</html>

DOM 树构建过程:

1. 字节流 → 字符流
   01001000 01010100 ... → <html><head>...

2. 字符流 → Token
   <html> → StartTag: html
   <head> → StartTag: head
   </head> → EndTag: head

3. Token → Node
   StartTag: html → HTMLHtmlElement
   StartTag: head → HTMLHeadElement

4. Node → DOM Tree
   Document
     └─ html
         ├─ head
         │   └─ title
         └─ body
             └─ div#app
                 ├─ h1
                 └─ p

8. 加载资源 ​

资源加载顺序:

1. HTML 解析(边解析边加载)
2. CSS 文件(阻塞渲染)
3. JavaScript 文件(阻塞解析)
4. 图片、字体等(异步加载)

CSS 加载:

html
<!-- 阻塞渲染,不阻塞解析 -->
<link rel="stylesheet" href="style.css">

JavaScript 加载:

html
<!-- 阻塞解析和渲染 -->
<script src="script.js"></script>

<!-- 异步加载,不阻塞解析 -->
<script async src="script.js"></script>

<!-- 延迟执行,不阻塞解析 -->
<script defer src="script.js"></script>

资源优先级:

1. HTML(最高)
2. CSS(高)
3. JavaScript(高)
4. 字体(中)
5. 图片(低)
6. 预加载资源(可配置)

9. 渲染页面 ​

渲染流程:

1. 构建 DOM 树
   HTML → DOM Tree

2. 构建 CSSOM 树
   CSS → CSSOM Tree

3. 合并生成渲染树
   DOM + CSSOM → Render Tree
   (只包含可见节点)

4. 布局(Layout/Reflow)
   计算每个节点的位置和大小

5. 绘制(Paint)
   将节点绘制到屏幕上

6. 合成(Composite)
   将多个图层合成最终图像

详细过程:

javascript
// 1. DOM Tree
Document
  └─ html
      ├─ head
      │   └─ style
      └─ body
          └─ div
              ├─ h1 (Hello)
              └─ p (World)

// 2. CSSOM Tree
StyleSheet
  └─ div
      ├─ color: blue
      └─ font-size: 16px

// 3. Render Tree(排除 display: none)
RenderObject
  └─ RenderBlock (div)
      ├─ RenderBlock (h1)
      │   └─ RenderText (Hello)
      └─ RenderBlock (p)
          └─ RenderText (World)

// 4. Layout(计算位置)
div: x=0, y=0, width=800, height=600
h1: x=0, y=0, width=800, height=32
p: x=0, y=32, width=800, height=20

// 5. Paint(绘制)
绘制背景色 → 绘制边框 → 绘制文本 → ...

// 6. Composite(合成)
图层1(背景) + 图层2(内容) → 最终画面

关键渲染路径(Critical Rendering Path) ​

HTML → DOM
CSS → CSSOM
DOM + CSSOM → Render Tree → Layout → Paint → Composite

优化关键渲染路径:

  1. 减少关键资源数量

    html
    <!-- 合并 CSS 文件 -->
    <link rel="stylesheet" href="all.css">
    
    <!-- 内联关键 CSS -->
    <style>
      /* 首屏关键样式 */
    </style>
  2. 减少关键资源大小

    html
    <!-- 压缩 CSS/JS -->
    <link rel="stylesheet" href="style.min.css">
    
    <!-- 移除未使用的 CSS -->
    <!-- 使用 PurgeCSS 等工具 -->
  3. 缩短关键路径长度

    html
    <!-- 使用 CDN -->
    <link rel="stylesheet" href="https://cdn.example.com/style.css">
    
    <!-- 预加载关键资源 -->
    <link rel="preload" href="font.woff2" as="font">

常见误区 ​

  1. 误区:DNS 解析很快,不需要优化

    html
    <!-- ❌ 错误:忽略 DNS 优化 -->
    <img src="https://cdn1.example.com/1.jpg">
    <img src="https://cdn2.example.com/2.jpg">
    <img src="https://cdn3.example.com/3.jpg">
    <!-- 每个域名都需要 DNS 解析 -->
    
    <!-- ✅ 正确:DNS 预解析 -->
    <link rel="dns-prefetch" href="//cdn1.example.com">
    <link rel="dns-prefetch" href="//cdn2.example.com">
    <link rel="dns-prefetch" href="//cdn3.example.com">
  2. 误区:CSS 不阻塞页面加载

    html
    <!-- ❌ CSS 会阻塞渲染 -->
    <link rel="stylesheet" href="large.css">
    <!-- 页面会等待 CSS 加载完成才渲染 -->
    
    <!-- ✅ 正确:关键 CSS 内联,非关键 CSS 异步加载 -->
    <style>
      /* 首屏关键样式 */
    </style>
    <link rel="preload" href="non-critical.css" as="style" onload="this.rel='stylesheet'">
  3. 误区:JavaScript 放在 head 中

    html
    <!-- ❌ 错误:阻塞页面解析 -->
    <head>
      <script src="large.js"></script>
    </head>
    
    <!-- ✅ 正确:放在 body 底部或使用 defer/async -->
    <body>
      <!-- 内容 -->
      <script defer src="script.js"></script>
    </body>

进阶知识 ​

1. HTTP/2 的多路复用 ​

HTTP/1.1:
请求1 -----> 响应1
请求2 -----> 响应2
请求3 -----> 响应3
(串行,需要多个 TCP 连接)

HTTP/2:
请求1 --|
请求2 --|--> 同一个 TCP 连接 --> 响应1、响应2、响应3
请求3 --|
(并行,单个 TCP 连接)

2. 浏览器渲染进程 ​

浏览器进程(Browser Process)
  ├─ 网络进程(Network Process)
  │   └─ 负责网络请求
  ├─ GPU 进程(GPU Process)
  │   └─ 负责 3D 绘制
  └─ 渲染进程(Renderer Process)
      ├─ 主线程(Main Thread)
      │   ├─ 解析 HTML/CSS
      │   ├─ 执行 JavaScript
      │   └─ 布局和绘制
      ├─ 合成线程(Compositor Thread)
      │   └─ 图层合成
      └─ 光栅化线程(Raster Thread)
          └─ 位图生成

3. 资源提示(Resource Hints) ​

html
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//example.com">

<!-- 预连接(DNS + TCP + TLS) -->
<link rel="preconnect" href="https://example.com">

<!-- 预加载(高优先级) -->
<link rel="preload" href="style.css" as="style">

<!-- 预获取(低优先级,空闲时加载) -->
<link rel="prefetch" href="next-page.html">

<!-- 预渲染(提前渲染整个页面) -->
<link rel="prerender" href="next-page.html">

💡 面试回答技巧 ​

🎯 一句话回答(快速版) ​

主要分两部分:网络请求(DNS 解析 → TCP 连接 → HTTP 请求响应)和页面渲染(解析 HTML → 构建 DOM/CSSOM → 渲染树 → 布局 → 绘制)。

📣 口语化回答(推荐) ​

面试时可以这样回答:

"从输入 URL 到页面展示,我分网络请求和页面渲染两部分来说。

网络请求阶段:

首先是 DNS 解析,把域名转换成 IP 地址。会依次查询浏览器缓存、系统缓存、路由器缓存、ISP 缓存,都没有才去 DNS 服务器查询。

然后是 TCP 连接,通过三次握手建立连接。如果是 HTTPS 还要进行 TLS 握手。

接着发送 HTTP 请求,服务器返回 HTML 文档。

页面渲染阶段:

浏览器解析 HTML 构建 DOM 树,解析 CSS 构建 CSSOM 树。

然后把 DOM 和 CSSOM 合并成渲染树,渲染树只包含可见节点,display: none 的不会在里面。

接着进行布局(Layout),计算每个节点的位置和大小。

最后绘制(Paint)到屏幕上。

优化的话,可以用 DNS 预解析、HTTP/2 多路复用、资源压缩、关键 CSS 内联、JS 延迟加载等。"

推荐回答顺序 ​

  1. 先说整体流程:

    • "从输入 URL 到页面展示,主要分为网络请求和页面渲染两大部分"
    • "网络请求包括 DNS 解析、TCP 连接、HTTP 请求响应"
    • "页面渲染包括解析 HTML、构建 DOM/CSSOM、渲染树、布局、绘制"
  2. 再详细展开每个步骤:

    • DNS 解析的查询顺序
    • TCP 三次握手
    • HTTP 请求响应的格式
    • DOM 树和 CSSOM 树的构建
    • 渲染树的生成和渲染流程
  3. 然后说优化点:

    • DNS 预解析
    • HTTP/2 多路复用
    • 资源压缩和合并
    • 关键 CSS 内联
    • JavaScript 延迟加载
  4. 最后说注意事项:

    • CSS 阻塞渲染
    • JavaScript 阻塞解析
    • 关键渲染路径优化

重点强调 ​

  • ✅ DNS 解析的缓存机制
  • ✅ TCP 三次握手的必要性
  • ✅ 渲染树只包含可见节点
  • ✅ 关键渲染路径的优化

可能的追问 ​

Q1: 为什么需要三次握手,两次不行吗?

A:

  • 两次握手无法确认客户端的接收能力
  • 可能导致已失效的连接请求被服务器接受
  • 无法正确同步双方的序列号

Q2: CSS 为什么会阻塞渲染?

A:

  • 浏览器需要 CSSOM 才能构建渲染树
  • 如果 CSS 未加载完成,无法确定元素样式
  • 为了避免 FOUC(无样式内容闪烁),浏览器会等待 CSS 加载

Q3: async 和 defer 的区别?

A:

html
<!-- async:异步加载,加载完立即执行(可能阻塞解析) -->
<script async src="script.js"></script>

<!-- defer:异步加载,DOMContentLoaded 前执行(不阻塞解析) -->
<script defer src="script.js"></script>

执行顺序:

  • async:不保证顺序,谁先加载完谁先执行
  • defer:保证顺序,按照在文档中的顺序执行

Q4: 如何减少首屏加载时间?

A:

  1. 减少关键资源:合并 CSS/JS,内联关键 CSS
  2. 压缩资源:Gzip、Brotli 压缩
  3. 使用 CDN:就近访问,减少延迟
  4. 懒加载:图片、非关键资源延迟加载
  5. 预加载:DNS 预解析、资源预加载
  6. HTTP/2:多路复用,减少连接数

💻 代码示例 ​

性能监控 ​

javascript
// 使用 Performance API 监控页面加载性能
window.addEventListener('load', () => {
  const perfData = performance.timing;
  
  // DNS 解析时间
  const dnsTime = perfData.domainLookupEnd - perfData.domainLookupStart;
  console.log('DNS 解析时间:', dnsTime, 'ms');
  
  // TCP 连接时间
  const tcpTime = perfData.connectEnd - perfData.connectStart;
  console.log('TCP 连接时间:', tcpTime, 'ms');
  
  // 请求时间
  const requestTime = perfData.responseStart - perfData.requestStart;
  console.log('请求时间:', requestTime, 'ms');
  
  // 响应时间
  const responseTime = perfData.responseEnd - perfData.responseStart;
  console.log('响应时间:', responseTime, 'ms');
  
  // DOM 解析时间
  const domTime = perfData.domComplete - perfData.domLoading;
  console.log('DOM 解析时间:', domTime, 'ms');
  
  // 首屏时间
  const firstPaintTime = perfData.domContentLoadedEventEnd - perfData.navigationStart;
  console.log('首屏时间:', firstPaintTime, 'ms');
  
  // 总加载时间
  const loadTime = perfData.loadEventEnd - perfData.navigationStart;
  console.log('总加载时间:', loadTime, 'ms');
});

// 使用 PerformanceObserver 监控资源加载
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('资源:', entry.name);
    console.log('加载时间:', entry.duration, 'ms');
    console.log('类型:', entry.initiatorType);
  }
});

observer.observe({ entryTypes: ['resource'] });

优化示例 ​

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>优化示例</title>
  
  <!-- DNS 预解析 -->
  <link rel="dns-prefetch" href="//cdn.example.com">
  
  <!-- 预连接 -->
  <link rel="preconnect" href="https://api.example.com">
  
  <!-- 关键 CSS 内联 -->
  <style>
    /* 首屏关键样式 */
    body { margin: 0; font-family: sans-serif; }
    .header { height: 60px; background: #333; }
  </style>
  
  <!-- 预加载关键资源 -->
  <link rel="preload" href="font.woff2" as="font" crossorigin>
  <link rel="preload" href="hero.jpg" as="image">
  
  <!-- 非关键 CSS 异步加载 -->
  <link rel="preload" href="non-critical.css" as="style" onload="this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="non-critical.css"></noscript>
</head>
<body>
  <header class="header">
    <h1>网站标题</h1>
  </header>
  
  <main>
    <!-- 图片懒加载 -->
    <img data-src="image.jpg" class="lazyload" alt="图片">
  </main>
  
  <!-- JavaScript 延迟加载 -->
  <script defer src="main.js"></script>
  
  <!-- 懒加载脚本 -->
  <script>
    // 图片懒加载
    const lazyImages = document.querySelectorAll('.lazyload');
    
    const imageObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          img.classList.remove('lazyload');
          imageObserver.unobserve(img);
        }
      });
    });
    
    lazyImages.forEach(img => imageObserver.observe(img));
  </script>
</body>
</html>

🔗 相关知识点 ​

📚 参考资料 ​

基于 MIT 许可发布 | 隐私政策