说说地址栏输入 URL 敲下回车后发生了什么
问题解析(面试官考察点)
面试官通过此问题主要考察:
- 对浏览器工作原理的整体理解
- 掌握从 URL 输入到页面展示的完整流程
- 了解网络请求、资源加载、渲染引擎等关键环节
- 能够分析性能瓶颈和优化点
- 理解各阶段的技术细节和相互关系
核心概念(基础知识点)
完整流程概览
┌─────────────────────────────────────────────────────────────┐
│ 从输入 URL 到页面展示的完整流程 │
├─────────────────────────────────────────────────────────────┤
│ 1. URL 解析 │
│ - 判断输入类型(URL 或搜索关键词) │
│ - 协议补全、编码处理 │
├─────────────────────────────────────────────────────────────┤
│ 2. DNS 查询 │
│ - 浏览器缓存 → OS 缓存 → hosts → DNS 服务器 │
│ - 获取目标 IP 地址 │
├─────────────────────────────────────────────────────────────┤
│ 3. 建立 TCP 连接 │
│ - 三次握手建立连接 │
│ - HTTPS 还需 TLS 握手 │
├─────────────────────────────────────────────────────────────┤
│ 4. 发送 HTTP 请求 │
│ - 构建请求行、请求头、请求体 │
│ - 发送请求并等待响应 │
├─────────────────────────────────────────────────────────────┤
│ 5. 服务器处理请求 │
│ - 解析请求、路由匹配 │
│ - 执行业务逻辑、查询数据库 │
│ - 返回 HTTP 响应 │
├─────────────────────────────────────────────────────────────┤
│ 6. 浏览器接收响应 │
│ - 解析响应头、状态码 │
│ - 处理重定向、缓存 │
│ - 接收响应体 │
├─────────────────────────────────────────────────────────────┤
│ 7. 页面渲染 │
│ - 解析 HTML 构建 DOM 树 │
│ - 解析 CSS 构建 CSSOM 树 │
│ - 合并为 Render Tree │
│ - 布局(Layout)和绘制(Paint) │
└─────────────────────────────────────────────────────────────┘
关键性能指标
| 指标 | 说明 | 优化目标 |
|---|---|---|
| TTFB | Time To First Byte,首字节时间 | < 200ms |
| FP | First Paint,首次绘制 | < 1s |
| FCP | First Contentful Paint,首次内容绘制 | < 1.8s |
| LCP | Largest Contentful Paint,最大内容绘制 | < 2.5s |
| TTI | Time To Interactive,可交互时间 | < 3.8s |
详细解答(代码示例)
1. URL 解析
// URL 解析过程示意
function parseURL(input) {
let url = input.trim();
// 1. 判断是否为搜索关键词
if (!isValidURL(url) && !url.includes(' ') && !url.includes('.')) {
// 使用默认搜索引擎
url = `https://www.google.com/search?q=${encodeURIComponent(url)}`;
}
// 2. 补全协议
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
// 3. 解析 URL 组成部分
const urlObj = new URL(url);
return {
href: urlObj.href, // 完整 URL
protocol: urlObj.protocol, // 协议: https:
host: urlObj.host, // 主机: example.com:443
hostname: urlObj.hostname, // 主机名: example.com
port: urlObj.port, // 端口: 443
pathname: urlObj.pathname, // 路径: /path/to/page
search: urlObj.search, // 查询: ?query=value
hash: urlObj.hash // 锚点: #section
};
}
// HSTS(HTTP Strict Transport Security)检查
// 如果域名在 HSTS 列表中,强制使用 HTTPS
const hstsList = ['google.com', 'github.com', 'example.com'];
function checkHSTS(hostname) {
return hstsList.some(domain => hostname.endsWith(domain));
}
2. DNS 查询详解
// DNS 查询流程(简化示意)
async function resolveDNS(hostname) {
// 1. 浏览器缓存检查
const browserCache = checkBrowserCache(hostname);
if (browserCache) return browserCache;
// 2. 操作系统缓存检查
const osCache = checkOSCache(hostname);
if (osCache) return osCache;
// 3. hosts 文件检查
const hostsEntry = checkHostsFile(hostname);
if (hostsEntry) return hostsEntry;
// 4. DNS 服务器查询(递归查询)
const dnsServers = ['8.8.8.8', '114.114.114.114'];
for (const dnsServer of dnsServers) {
try {
const ip = await queryDNS(hostname, dnsServer);
cacheDNSResult(hostname, ip);
return ip;
} catch (e) {
continue;
}
}
throw new Error('DNS resolution failed');
}
// DNS 预解析优化
<link rel="dns-prefetch" href="//api.example.com">
<link rel="preconnect" href="//api.example.com" crossorigin>
3. TCP 三次握手
客户端 服务器
| |
|-------- 1. SYN (seq=x) -------->|
| 请求建立连接,发送序列号 x |
| |
|<------- 2. SYN-ACK (seq=y, ack=x+1) |
| 同意建立连接,发送序列号 y |
| 确认收到客户端序列号 x+1 |
| |
|-------- 3. ACK (ack=y+1) ------->|
| 确认收到服务器序列号 y+1 |
| |
|======== TCP 连接建立完成 ==========|
关键参数:
- seq: 序列号,用于数据排序和确认
- ack: 确认号,期望收到的下一个序列号
- SYN: 同步标志,用于建立连接
- ACK: 确认标志
// Node.js TCP 连接示例
const net = require('net');
const client = new net.Socket();
client.connect(80, 'example.com', () => {
console.log('TCP 连接已建立');
// 发送 HTTP 请求
client.write('GET / HTTP/1.1\r\n');
client.write('Host: example.com\r\n');
client.write('\r\n');
});
client.on('data', (data) => {
console.log('收到数据:', data.toString());
client.destroy();
});
client.on('close', () => {
console.log('连接已关闭');
});
4. HTTPS TLS 握手
HTTPS 额外步骤(TLS 握手):
客户端 服务器
| |
|-------- Client Hello ---------->|
| 支持的 TLS 版本、加密套件列表 |
| 客户端随机数 |
| |
|<------- Server Hello ------------|
| 选定的 TLS 版本、加密套件 |
| 服务器证书(含公钥) |
| 服务器随机数 |
| |
|-------- 验证证书 ---------------->|
| 检查证书链、有效期、域名匹配 |
| |
|-------- 密钥交换 ---------------->|
| 生成预主密钥,用公钥加密发送 |
| |
|<------- 加密通信开始 --------------|
| 双方生成会话密钥,对称加密通信 |
TLS 1.3 优化:
- 握手从 2-RTT 减少到 1-RTT
- 支持 0-RTT 会话恢复
5. HTTP 请求与响应
// 请求报文
GET /api/users HTTP/1.1
Host: api.example.com
Connection: keep-alive
Accept: application/json
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: session=abc123; user=john
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...
// 请求体(GET 通常为空)
// 响应报文
HTTP/1.1 200 OK
Date: Mon, 18 Mar 2024 08:00:00 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 1234
Connection: keep-alive
Cache-Control: max-age=3600
ETag: "abc123"
Set-Cookie: session=xyz789; Path=/; HttpOnly; Secure
{"users": [{"id": 1, "name": "张三"}, ...]}
// 使用 Fetch API 发送请求
async function fetchData() {
const response = await fetch('https://api.example.com/users', {
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer token123'
},
// 其他选项
mode: 'cors',
cache: 'default',
credentials: 'include'
});
// 检查响应状态
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// 解析响应体
const data = await response.json();
return data;
}
6. 页面渲染流程
// 渲染流程示意(简化)
class Renderer {
constructor() {
this.domTree = null;
this.cssomTree = null;
this.renderTree = null;
}
// 1. 解析 HTML 构建 DOM
parseHTML(html) {
const parser = new DOMParser();
this.domTree = parser.parseFromString(html, 'text/html');
return this.domTree;
}
// 2. 解析 CSS 构建 CSSOM
parseCSS(css) {
// CSS 解析器将 CSS 转换为对象模型
this.cssomTree = this.parseCSSRules(css);
return this.cssomTree;
}
// 3. 合并 DOM 和 CSSOM 构建 Render Tree
buildRenderTree() {
this.renderTree = this.combineTrees(this.domTree, this.cssomTree);
return this.renderTree;
}
// 4. 布局(Layout/Reflow)
layout() {
// 计算每个节点的几何信息(位置、大小)
this.calculateLayout(this.renderTree);
}
// 5. 绘制(Paint)
paint() {
// 将渲染树绘制到屏幕上
this.drawToScreen(this.renderTree);
}
// 6. 合成(Composite)
composite() {
// 将多个图层合成为最终页面
this.compositeLayers();
}
}
深入理解(原理剖析)
浏览器多进程架构
Chrome 多进程架构:
┌─────────────────────────────────────────────────────────────┐
│ 浏览器主进程(Browser Process) │
│ - 地址栏、书签栏、前进后退按钮 │
│ - 网络请求管理 │
│ - 文件访问权限 │
├─────────────────────────────────────────────────────────────┤
│ GPU 进程(GPU Process) │
│ - 3D CSS 渲染 │
│ - Canvas 绘制 │
│ - 视频解码 │
├─────────────────────────────────────────────────────────────┤
│ 插件进程(Plugin Process) │
│ - Flash、PDF 查看器等 │
├─────────────────────────────────────────────────────────────┤
│ 渲染进程(Renderer Process)× N │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Tab 1 │ │ Tab 2 │ │ Tab 3 │ │
│ │ (站点 A) │ │ (站点 B) │ │ (站点 C) │ │
│ │ │ │ │ │ │ │
│ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │
│ │ │ Blink │ │ │ │ Blink │ │ │ │ Blink │ │ │
│ │ │ 渲染引擎 │ │ │ │ 渲染引擎 │ │ │ │ 渲染引擎 │ │ │
│ │ │ ┌─────┐ │ │ │ │ ┌─────┐ │ │ │ │ ┌─────┐ │ │ │
│ │ │ │ V8 │ │ │ │ │ │ V8 │ │ │ │ │ │ V8 │ │ │ │
│ │ │ │ JS │ │ │ │ │ │ JS │ │ │ │ │ │ JS │ │ │ │
│ │ │ └─────┘ │ │ │ │ └─────┘ │ │ │ │ └─────┘ │ │ │
│ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
站点隔离(Site Isolation):
- 不同站点使用不同渲染进程
- 防止恶意站点访问其他站点的数据
- 增加内存使用,但提高安全性
关键渲染路径(Critical Rendering Path)
关键渲染路径优化:
┌─────────────────────────────────────────────────────────────┐
│ 关键资源(阻塞渲染) │
│ ├── HTML:必须下载并解析 │
│ ├── CSS:阻塞渲染,需要构建 CSSOM │
│ └── JavaScript:默认阻塞解析(除非 async/defer) │
├─────────────────────────────────────────────────────────────┤
│ 优化策略: │
│ 1. 减少关键资源数量 │
│ - 内联关键 CSS │
│ - 延迟加载非关键 JS │
│ │
│ 2. 减少关键资源大小 │
│ - 压缩(Gzip/Brotli) │
│ - 代码分割 │
│ - Tree Shaking │
│ │
│ 3. 优化关键路径长度 │
│ - 预加载关键资源 │
│ - 使用 CDN │
│ - HTTP/2 服务器推送 │
└─────────────────────────────────────────────────────────────┘
渲染阻塞示例:
<html>
<head>
<!-- 阻塞渲染 -->
<link rel="stylesheet" href="large.css">
<!-- 阻塞解析 -->
<script src="app.js"></script>
<!-- 优化:异步加载 -->
<script src="analytics.js" async></script>
<!-- 优化:延迟执行 -->
<script src="app.js" defer></script>
</head>
</html>
重排(Reflow)与重绘(Repaint)
// 触发重排的操作(性能开销大)
function triggerReflow() {
// 读取布局属性(强制同步布局)
const width = element.offsetWidth;
// 修改布局属性
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';
// 添加/删除元素
document.body.appendChild(newElement);
// 修改字体大小
element.style.fontSize = '16px';
}
// 触发重绘的操作(性能开销较小)
function triggerRepaint() {
element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.visibility = 'hidden';
element.style.borderRadius = '5px';
}
// 优化:批量修改样式
function optimizeReflow() {
// 方法 1:使用 CSS 类
element.classList.add('new-styles');
// 方法 2:使用 CSSText
element.style.cssText = 'width: 100px; height: 100px; margin: 10px;';
// 方法 3:先隐藏再修改
element.style.display = 'none';
// ... 批量修改 ...
element.style.display = 'block';
// 方法 4:使用 DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
fragment.appendChild(div);
}
document.body.appendChild(fragment);
// 方法 5:使用 requestAnimationFrame
requestAnimationFrame(() => {
// 在下一帧统一修改
element.style.width = '100px';
});
}
浏览器缓存机制
缓存策略决策树:
┌─────────────────────────────────────────────────────────────┐
│ 浏览器请求资源 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 1. Service Worker 缓存? │
│ 是 → 返回 SW 缓存 │
│ 否 → 继续 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2. 内存缓存(Memory Cache)? │
│ 是 → 返回内存缓存(快,但容量小) │
│ 否 → 继续 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3. 磁盘缓存(Disk Cache/HTTP Cache)? │
│ 是 → 根据缓存策略决定是否使用 │
│ 否 → 继续 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 4. 发送网络请求 │
│ - 协商缓存(304 Not Modified) │
│ - 或返回新资源(200) │
└─────────────────────────────────────────────────────────────┘
缓存控制头部:
Cache-Control: no-cache // 强制重新验证
Cache-Control: no-store // 不缓存
Cache-Control: max-age=3600 // 缓存 1 小时
Cache-Control: private // 仅浏览器缓存
Cache-Control: public // 可被中间代理缓存
Expires: Wed, 21 Oct 2025 07:28:00 GMT // 过期时间
ETag: "abc123" // 资源标识
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
最佳实践
1. 前端性能优化
<!-- 1. 资源预加载 -->
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//api.example.com">
<!-- 预连接(DNS + TCP + TLS) -->
<link rel="preconnect" href="https://cdn.example.com">
<!-- 预加载关键资源 -->
<link rel="preload" href="/css/critical.css" as="style">
<link rel="preload" href="/js/app.js" as="script">
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
<!-- 预获取下一页资源 -->
<link rel="prefetch" href="/next-page.html">
<!-- 2. 关键 CSS 内联 -->
<style>
/* 首屏关键 CSS */
.hero { ... }
.header { ... }
.nav { ... }
</style>
<!-- 非关键 CSS 异步加载 -->
<link rel="preload" href="/css/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<!-- 3. JavaScript 加载优化 -->
<!-- async: 异步加载,加载完立即执行 -->
<script src="analytics.js" async></script>
<!-- defer: 异步加载,DOM 解析完成后执行 -->
<script src="app.js" defer></script>
<!-- type="module": 自动 defer -->
<script type="module" src="app.mjs"></script>
<!-- 4. 图片优化 -->
<img src="photo.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1200.jpg 1200w"
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
loading="lazy"
alt="描述">
<!-- 5. 使用 Service Worker 缓存 -->
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
</script>
2. 服务端优化
# Nginx 性能优化配置
server {
listen 443 ssl http2;
# 启用 Gzip/Brotli 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript;
# 浏览器缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Vary Accept-Encoding;
}
# HTML 不缓存
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache";
}
# 启用 HTTP/2 Server Push
location = /index.html {
http2_push /css/critical.css;
http2_push /js/app.js;
}
}
3. 性能监控
// 使用 Performance API 监控性能
window.addEventListener('load', () => {
// Navigation Timing
const timing = performance.timing;
const metrics = {
// DNS 查询时间
dns: timing.domainLookupEnd - timing.domainLookupStart,
// TCP 连接时间
tcp: timing.connectEnd - timing.connectStart,
// SSL 握手时间
ssl: timing.connectEnd - timing.secureConnectionStart,
// 首字节时间(TTFB)
ttfb: timing.responseStart - timing.requestStart,
// DOM 解析时间
domParse: timing.domComplete - timing.domLoading,
// 资源加载时间
resourceLoad: timing.loadEventStart - timing.domContentLoadedEventEnd,
// 总加载时间
total: timing.loadEventEnd - timing.navigationStart
};
console.log('性能指标:', metrics);
// 上报到监控系统
reportMetrics(metrics);
});
// 使用 Performance Observer 监控 LCP、FID、CLS
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('性能条目:', entry.name, entry.startTime, entry.duration);
// LCP (Largest Contentful Paint)
if (entry.entryType === 'largest-contentful-paint') {
console.log('LCP:', entry.renderTime || entry.loadTime);
}
// FID (First Input Delay)
if (entry.entryType === 'first-input') {
console.log('FID:', entry.processingStart - entry.startTime);
}
}
});
observer.observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift'] });
面试要点
-
完整流程记忆要点
- URL 解析 → DNS 查询 → TCP 连接 → HTTP 请求 → 服务器处理 → 浏览器渲染
- 每个阶段的关键技术和优化点
-
DNS 优化
- DNS 预解析(dns-prefetch)
- 预连接(preconnect)
- 减少域名数量
- 使用 DNS 缓存
-
TCP/HTTP 优化
- 使用 HTTP/2 或 HTTP/3
- 启用 Keep-Alive
- 使用 CDN
- 压缩资源
-
渲染优化
- 减少关键资源数量
- 延迟加载非关键资源
- 避免渲染阻塞
- 优化重排重绘
-
面试高频问题
- 从输入 URL 到页面展示发生了什么?(按阶段详细回答)
- 什么是重排和重绘?如何优化?
- 浏览器缓存机制?
- 什么是 CRP(Critical Rendering Path)?
- 如何优化首屏加载速度?
- 什么是 TTFB?如何优化?
- Service Worker 的作用?