Skip to content

从输入 URL 到页面渲染的完整流程

概述

"从输入 URL 到页面显示发生了什么?"是前端面试中最经典的问题之一。这个问题涵盖了网络、浏览器、渲染等多个知识领域,能够全面考察候选人的知识广度和深度。

流程总览

用户输入 URL

URL 解析

DNS 解析

建立 TCP 连接(三次握手)

TLS 握手(HTTPS)

发送 HTTP 请求

服务器处理请求

返回 HTTP 响应

浏览器解析响应

构建 DOM 树

构建 CSSOM 树

构建渲染树

布局(Layout)

绘制(Paint)

合成(Composite)

页面显示

第一阶段:URL 处理

1. 用户输入处理

javascript
// 浏览器判断输入内容
function processInput(input) {
  // 1. 检查是否是合法 URL
  if (isValidURL(input)) {
    return input;
  }

  // 2. 检查是否是搜索关键词
  // 如果不是 URL,使用默认搜索引擎搜索
  return `https://www.google.com/search?q=${encodeURIComponent(input)}`;
}

2. URL 解析

javascript
// URL 结构
const url = new URL('https://www.example.com:8080/path/page?name=value#hash');

console.log(url.protocol); // 'https:'
console.log(url.host);     // 'www.example.com:8080'
console.log(url.hostname); // 'www.example.com'
console.log(url.port);     // '8080'
console.log(url.pathname); // '/path/page'
console.log(url.search);   // '?name=value'
console.log(url.hash);     // '#hash'

3. 检查缓存

浏览器按顺序检查以下缓存:

1. Service Worker 缓存
2. Memory Cache(内存缓存)
3. Disk Cache(磁盘缓存)
4. Push Cache(HTTP/2 推送缓存)
javascript
// Service Worker 拦截请求示例
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      // 缓存命中,直接返回
      if (response) {
        return response;
      }
      // 缓存未命中,发起网络请求
      return fetch(event.request);
    })
  );
});

第二阶段:网络请求

1. DNS 解析

将域名解析为 IP 地址:

浏览器 DNS 缓存
    ↓ (未命中)
操作系统 DNS 缓存
    ↓ (未命中)
本地 hosts 文件
    ↓ (未命中)
路由器 DNS 缓存
    ↓ (未命中)
ISP DNS 服务器
    ↓ (未命中)
递归查询根域名服务器

顶级域名服务器(.com)

权威域名服务器

返回 IP 地址
javascript
// DNS 预解析优化
// <link rel="dns-prefetch" href="//example.com">

// DNS 解析时间测量
const [navigation] = performance.getEntriesByType('navigation');
console.log('DNS 解析时间:', navigation.domainLookupEnd - navigation.domainLookupStart, 'ms');

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

客户端                    服务器
   |                        |
   |------- SYN=1 --------->|  第一次:客户端发送 SYN
   |    seq=x               |
   |                        |
   |<--- SYN=1, ACK=1 ------|  第二次:服务器返回 SYN+ACK
   |    seq=y, ack=x+1      |
   |                        |
   |------- ACK=1 --------->|  第三次:客户端发送 ACK
   |    ack=y+1             |
   |                        |
   |====== 连接建立 =========|

为什么是三次握手?

  • 确认双方的发送和接收能力都正常
  • 防止历史连接请求造成混乱
  • 同步双方的初始序列号

3. TLS 握手(HTTPS)

客户端                           服务器
   |                               |
   |------ Client Hello ---------->|  发送支持的加密套件
   |                               |
   |<----- Server Hello -----------|  选择加密套件
   |<----- Certificate ------------|  发送证书
   |<----- Server Hello Done ------|
   |                               |
   |------ Client Key Exchange --->|  发送预主密钥
   |------ Change Cipher Spec ---->|  切换加密模式
   |------ Finished -------------->|
   |                               |
   |<----- Change Cipher Spec -----|
   |<----- Finished ---------------|
   |                               |
   |======= 加密通道建立 ===========|
javascript
// 性能测量
const [navigation] = performance.getEntriesByType('navigation');
console.log('TCP 连接时间:', navigation.connectEnd - navigation.connectStart, 'ms');
console.log('TLS 握手时间:', navigation.secureConnectionStart > 0
  ? navigation.connectEnd - navigation.secureConnectionStart
  : 0, 'ms');

4. 发送 HTTP 请求

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

5. 服务器处理与响应

http
HTTP/1.1 200 OK
Date: Thu, 01 Jan 2024 00:00:00 GMT
Server: nginx/1.18.0
Content-Type: text/html; charset=utf-8
Content-Length: 1234
Content-Encoding: gzip
Cache-Control: max-age=3600
ETag: "abc123"

<!DOCTYPE html>
<html>...

6. TCP 四次挥手(连接关闭)

客户端                    服务器
   |                        |
   |------- FIN=1 --------->|  第一次:客户端请求关闭
   |    seq=u               |
   |                        |
   |<------ ACK=1 ----------|  第二次:服务器确认
   |    ack=u+1             |
   |                        |
   |<------ FIN=1 ----------|  第三次:服务器请求关闭
   |    seq=v               |
   |                        |
   |------- ACK=1 --------->|  第四次:客户端确认
   |    ack=v+1             |
   |                        |
   |====== 连接关闭 =========|

为什么是四次挥手?

  • TCP 是全双工,需要双向都关闭
  • 服务器可能还有数据要发送

第三阶段:浏览器解析

1. 构建 DOM 树

字节 (Bytes)

字符 (Characters)

令牌 (Tokens)

节点 (Nodes)

DOM 树
html
<!-- 输入 HTML -->
<!DOCTYPE html>
<html>
  <head>
    <title>页面标题</title>
  </head>
  <body>
    <div class="container">
      <p>Hello World</p>
    </div>
  </body>
</html>
javascript
// DOM 树结构
Document
├── DOCTYPE: html
└── html
    ├── head
    │   └── title
    │       └── #text: 页面标题
    └── body
        └── div.container
            └── p
                └── #text: Hello World

2. 构建 CSSOM 树

css
/* 输入 CSS */
body { font-size: 16px; }
.container { width: 100%; }
p { color: blue; }
javascript
// CSSOM 树结构
StyleSheetList
└── CSSStyleSheet
    └── CSSRuleList
        ├── CSSStyleRule: body { font-size: 16px }
        ├── CSSStyleRule: .container { width: 100% }
        └── CSSStyleRule: p { color: blue }

3. JavaScript 执行

html
<!-- 同步脚本会阻塞解析 -->
<script src="app.js"></script>

<!-- defer: 延迟到 DOM 解析完成后执行,按顺序执行 -->
<script defer src="app.js"></script>

<!-- async: 加载完立即执行,不保证顺序 -->
<script async src="analytics.js"></script>
         HTML 解析
            |
    ┌───────┴───────┐
    |               |
同步 script      defer/async
    |               |
阻塞 HTML 解析   不阻塞 HTML 解析
    |               |
执行完毕         defer: DOMContentLoaded 前执行
    |            async: 加载完立即执行
继续解析

4. 构建渲染树(Render Tree)

javascript
// 渲染树 = DOM 树 + CSSOM 树
// 特点:
// 1. 只包含可见元素
// 2. display: none 的元素不在渲染树中
// 3. visibility: hidden 的元素在渲染树中(占位但不显示)
// 4. 伪元素(::before, ::after)在渲染树中

RenderTree
└── RenderView
    └── RenderBody
        └── RenderBlock (div.container)
            └── RenderBlock (p)
                └── RenderText: "Hello World"

第四阶段:渲染流程

1. 布局(Layout / Reflow)

计算每个元素的几何信息(位置、大小):

javascript
// 布局计算的信息
{
  element: 'div.container',
  x: 0,
  y: 0,
  width: 1920,
  height: 500,
  children: [
    {
      element: 'p',
      x: 0,
      y: 0,
      width: 1920,
      height: 20
    }
  ]
}

触发重排的操作:

javascript
// 这些操作会触发重排
element.offsetHeight;     // 读取几何属性
element.style.width = '100px';  // 修改几何属性
element.className = 'new-class';  // 修改样式
window.getComputedStyle(element);  // 获取计算样式

// 优化:批量修改样式
// 不好
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';

// 好
element.style.cssText = 'width: 100px; height: 100px; margin: 10px';
// 或使用 class
element.className = 'new-styles';

2. 绘制(Paint)

将渲染树转换为屏幕上的像素:

javascript
// 绘制顺序(Stacking Context)
// 1. 背景颜色
// 2. 背景图片
// 3. 边框
// 4. 子元素
// 5. 轮廓(outline)

// 绘制分层
// - 某些属性会创建新的图层
// - transform, opacity, will-change 等

触发重绘的操作:

javascript
// 只触发重绘,不触发重排
element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.visibility = 'hidden';

3. 合成(Composite)

将多个图层合成为最终图像:

javascript
// 创建合成层的属性
// - transform: translateZ(0)
// - will-change: transform
// - position: fixed
// - video, canvas, iframe 等

// 合成层的优点
// 1. 独立于主线程
// 2. GPU 加速
// 3. 只重绘当前层

// 使用 will-change 提示浏览器
.animated-element {
  will-change: transform;
}

4. 关键渲染路径优化

html
<!-- 优化关键渲染路径 -->

<!-- 1. 关键 CSS 内联 -->
<style>
  /* 首屏关键样式 */
  .header { ... }
  .hero { ... }
</style>

<!-- 2. 非关键 CSS 异步加载 -->
<link rel="preload" href="non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

<!-- 3. JavaScript 延迟加载 -->
<script defer src="app.js"></script>

<!-- 4. 预加载关键资源 -->
<link rel="preload" href="critical-font.woff2" as="font" type="font/woff2" crossorigin>

性能指标

关键性能指标

javascript
// 使用 Performance API 测量
const timing = performance.timing;

// DNS 解析时间
const dnsTime = timing.domainLookupEnd - timing.domainLookupStart;

// TCP 连接时间
const tcpTime = timing.connectEnd - timing.connectStart;

// 请求响应时间
const requestTime = timing.responseEnd - timing.requestStart;

// DOM 解析时间
const domParseTime = timing.domComplete - timing.domLoading;

// 首次渲染时间
const firstPaint = timing.domContentLoadedEventEnd - timing.navigationStart;

// 页面完全加载时间
const loadTime = timing.loadEventEnd - timing.navigationStart;

// 使用 PerformanceObserver
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`${entry.name}: ${entry.startTime}ms`);
  }
});

observer.observe({ entryTypes: ['paint', 'largest-contentful-paint'] });

Web Vitals

javascript
// LCP (Largest Contentful Paint) - 最大内容绘制
new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  const lastEntry = entries[entries.length - 1];
  console.log('LCP:', lastEntry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });

// FID (First Input Delay) - 首次输入延迟
new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  entries.forEach(entry => {
    console.log('FID:', entry.processingStart - entry.startTime);
  });
}).observe({ type: 'first-input', buffered: true });

// CLS (Cumulative Layout Shift) - 累计布局偏移
let clsValue = 0;
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) {
      clsValue += entry.value;
    }
  }
  console.log('CLS:', clsValue);
}).observe({ type: 'layout-shift', buffered: true });

完整流程图

┌─────────────────────────────────────────────────────────────────────┐
│                         用户输入 URL                                 │
└────────────────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                          URL 解析                                    │
│  解析协议、域名、端口、路径、查询参数                                  │
└────────────────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                         检查缓存                                     │
│  Service Worker → Memory Cache → Disk Cache → Push Cache            │
└────────────────────────────────┬────────────────────────────────────┘
                                 │ 缓存未命中

┌─────────────────────────────────────────────────────────────────────┐
│                         DNS 解析                                     │
│  浏览器缓存 → 系统缓存 → hosts → 路由器 → ISP → 递归查询              │
└────────────────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                      建立 TCP 连接                                   │
│                       三次握手                                       │
└────────────────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                      TLS 握手(HTTPS)                               │
│                  协商加密套件,交换密钥                               │
└────────────────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                      发送 HTTP 请求                                  │
│                  请求行 + 请求头 + 请求体                             │
└────────────────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                      服务器处理请求                                  │
│            负载均衡 → Web 服务器 → 应用服务器 → 数据库                │
└────────────────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                      返回 HTTP 响应                                  │
│                  状态行 + 响应头 + 响应体                             │
└────────────────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                       浏览器解析                                     │
│  ┌──────────────┐    ┌──────────────┐                               │
│  │  构建 DOM 树  │    │ 构建 CSSOM 树│                               │
│  └──────┬───────┘    └──────┬───────┘                               │
│         └────────┬──────────┘                                       │
│                  ▼                                                  │
│         ┌──────────────┐                                            │
│         │  构建渲染树   │                                            │
│         └──────┬───────┘                                            │
│                │                                                    │
└────────────────┼────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                        渲染流程                                      │
│  ┌────────┐   ┌────────┐   ┌────────┐                               │
│  │  布局   │ → │  绘制   │ → │  合成   │                              │
│  │(Layout)│   │(Paint) │   │(Comp.) │                               │
│  └────────┘   └────────┘   └────────┘                               │
└────────────────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                         页面显示                                     │
└─────────────────────────────────────────────────────────────────────┘

面试回答模板

简短版(1-2分钟)

从输入 URL 到页面显示,主要经历以下步骤:

1. URL 解析:解析协议、域名、路径等
2. DNS 解析:将域名解析为 IP 地址
3. 建立连接:TCP 三次握手,HTTPS 还需要 TLS 握手
4. 发送请求:发送 HTTP 请求到服务器
5. 服务器响应:返回 HTML 内容
6. 解析渲染:
   - 构建 DOM 树和 CSSOM 树
   - 合并为渲染树
   - 布局计算位置大小
   - 绘制和合成
7. 显示页面

详细版(展开每个环节的细节)

可以根据面试官的追问,详细展开:

  • DNS 解析的层级缓存
  • TCP 三次握手的具体过程
  • 浏览器的多进程架构
  • 渲染过程中的重排重绘
  • 各种优化手段

常见面试追问

1. 为什么 DNS 解析需要递归查询?

因为 DNS 是分层设计的,没有一个服务器知道所有域名。从根域名服务器开始,逐级查询到最终的权威服务器。

2. TCP 为什么是三次握手而不是两次?

两次握手无法确认客户端的接收能力,可能导致服务器资源浪费。三次握手能确保双方的发送和接收能力都正常。

3. CSS 和 JavaScript 如何影响渲染?

  • CSS 不阻塞 DOM 解析,但阻塞渲染
  • JavaScript 阻塞 DOM 解析(除非 async/defer)
  • 脚本前的 CSS 会阻塞脚本执行

4. 如何优化关键渲染路径?

  • 内联关键 CSS
  • 异步加载非关键 CSS
  • 使用 defer/async 加载 JavaScript
  • 减少关键资源数量和大小
  • 使用预加载(preload)和预连接(preconnect)

5. 重排和重绘的区别?

  • 重排:几何属性变化,需要重新计算布局,开销大
  • 重绘:样式变化但不影响布局,开销相对小
  • 重排必定触发重绘,重绘不一定触发重排

总结

理解从 URL 到页面渲染的完整流程,需要掌握:

  1. 网络层面:DNS、TCP/IP、HTTP/HTTPS
  2. 浏览器层面:多进程架构、渲染引擎
  3. 解析层面:HTML、CSS、JavaScript 解析
  4. 渲染层面:渲染树、布局、绘制、合成
  5. 优化层面:缓存策略、关键渲染路径优化

这个问题能够串联起前端开发中的大部分知识点,是理解 Web 工作原理的重要基础。