一句话总结
完整流程分两大阶段:网络阶段和渲染阶段。网络阶段:URL 解析 → DNS 解析(域名→IP)→ TCP 三次握手 → TLS 握手(HTTPS)→ 发送 HTTP 请求 → 服务端处理 → 返回 HTTP 响应。渲染阶段:解析 HTML → 构建 DOM 树 → 解析 CSS → 构建 CSSOM 树 → 合并为渲染树 → 布局(Layout/Reflow)→ 绘制(Paint)→ 合成(Composite)→ 显示。关键优化:避免重排(改宽高/位置),减少重绘(改颜色),利用合成(transform/opacity)。
初级理解
完整流程概览
# 输入 https://www.example.com 后的完整过程:
# === 网络阶段 ===
# 1. URL 解析
# 判断是 URL 还是搜索关键词
# 解析协议(https)、域名(www.example.com)、路径(/)
# 2. DNS 解析
# 浏览器缓存 → OS 缓存 → hosts → DNS 服务器
# 域名 → IP 地址(93.184.216.34)
# 3. TCP 三次握手
# SYN → SYN+ACK → ACK
# 建立客户端与服务端的可靠连接
# 4. TLS 握手(HTTPS)
# Client Hello → Server Hello + 证书 → 密钥交换
# 建立加密通道
# 5. 发送 HTTP 请求
# GET / HTTP/1.1
# Host: www.example.com
# ...
# 6. 服务端处理
# Nginx 接收 → 转发到应用服务器 → 业务处理
# → 返回 HTML
# 7. 返回 HTTP 响应
# HTTP/1.1 200 OK
# Content-Type: text/html
# <html>...</html>
# === 渲染阶段 ===
# 8. 解析 HTML → DOM 树
# 9. 解析 CSS → CSSOM 树
# 10. DOM + CSSOM → 渲染树(Render Tree)
# 11. 布局(Layout):计算每个元素的位置和大小
# 12. 绘制(Paint):填充像素
# 13. 合成(Composite):分层合并显示
中级深入
浏览器渲染详细流程
# 1. 解析 HTML → DOM 树
# 字节 → 字符 → Token → 节点 → DOM 树
# 遇到 <script> 阻塞 DOM 解析(除非 async/defer)
# 遇到 <link> 不阻塞 DOM 解析,但阻塞渲染
# 2. 解析 CSS → CSSOM 树
# 浏览器 CSS 默认样式 + 外部 CSS + 内联样式
# CSS 解析不阻塞 DOM 构建,但阻塞渲染
# (因为 CSSOM 没构建完,渲染树无法生成)
# 3. DOM + CSSOM → 渲染树
# 只包含可见元素(display:none 不在渲染树中)
# 每个可见节点匹配对应的 CSS 规则
# 4. 布局(Layout / Reflow)
# 计算每个节点的几何信息(位置、大小)
# 从根节点开始递归计算
# 触发条件:DOM 变化、样式变化、窗口大小变化
# 5. 绘制(Paint)
# 将渲染树转换为屏幕上的像素
# 生成绘制指令列表
# 6. 合成(Composite)
# 将多个图层合并为最终画面
# transform/opacity 只触发合成,不触发重排重绘
重排(Reflow)vs 重绘(Repaint)
# 重排(Reflow):重新计算布局
# 触发条件:
# 1. 添加/删除 DOM 元素
# 2. 修改元素尺寸(width/height/padding/margin/border)
# 3. 修改元素位置(position/top/left)
# 4. 修改字体大小
# 5. 改变窗口大小
# 6. 读取某些属性(offsetWidth/scrollTop 等)
# 重绘(Repaint):重新绘制像素
# 触发条件:
# 1. 修改颜色(color/background-color)
# 2. 修改可见性(visibility)
# 3. 修改阴影(box-shadow)
# 性能影响:重排 > 重绘 > 合成
# 重排一定触发重绘,重绘不一定触发重排
# 优化建议:
# 1. 批量修改 DOM(DocumentFragment)
# 2. 使用 class 替代逐个修改 style
# 3. 避免频繁读取布局属性
# 4. 动画使用 transform/opacity(只触发合成)
# 5. 使用 requestAnimationFrame
高级进阶
script 标签的 async 和 defer
# 普通 script(无属性):
# HTML 解析到 script → 暂停解析 → 下载并执行 JS
# → 继续解析 HTML
# 阻塞 DOM 构建
# <script async>:
# HTML 解析和 JS 下载并行
# JS 下载完立即执行(暂停 HTML 解析)
# 执行顺序不确定(谁先下载完谁先执行)
# 适用:独立脚本(如统计、广告)
# <script defer>:
# HTML 解析和 JS 下载并行
# JS 在 HTML 解析完成后、DOMContentLoaded 前执行
# 执行顺序确定(按文档顺序)
# 适用:依赖 DOM 的脚本
# 总结:
# 普通:下载+执行都阻塞
# async:下载不阻塞,执行阻塞,顺序不确定
# defer:下载不阻塞,执行不阻塞,顺序确定
关键渲染路径优化
# 关键渲染路径(Critical Rendering Path):
# 从收到 HTML 到首次渲染的步骤
# 优化策略:
# 1. 减少关键资源数量
# - 内联关键 CSS(首屏样式)
# - 延迟加载非关键 CSS/JS
# - 使用 media 属性(print 等)
# 2. 减少关键字节数
# - 压缩 HTML/CSS/JS(Gzip/Brotli)
# - 代码分割(Code Splitting)
# - Tree Shaking
# 3. 缩短关键路径长度
# - 减少请求次数(合并文件)
# - 使用 CDN
# - 预加载关键资源:
# <link rel="preload" href="font.woff2" as="font">
# <link rel="preconnect" href="https://api.example.com">
实战场景
# 场景1:首屏加载优化
# 1. 服务端渲染(SSR):直接返回渲染好的 HTML
# 2. 骨架屏:先显示占位,数据到了再填充
# 3. 图片懒加载:<img loading="lazy">
# 4. 路由懒加载:按需加载 JS
# 场景2:动画性能优化
# 错误做法(触发重排):
element.style.left = '100px'; // 每帧都重排
element.style.top = '100px';
# 正确做法(只触发合成):
element.style.transform = 'translate(100px, 100px)';
# transform 在合成线程执行,不阻塞主线程
# 场景3:强制同步布局问题
# 错误(读取布局属性触发强制重排):
element.style.width = '100px';
console.log(element.offsetWidth); // 强制重排!
element.style.height = '100px';
# 正确(先读后写):
console.log(element.offsetWidth); // 先读
element.style.width = '100px'; // 后写
element.style.height = '100px';
面试模拟
面试官:从输入 URL 到页面展示,发生了什么?
你:分两大阶段。网络阶段:URL 解析 → DNS 解析 → TCP 三次握手 → TLS 握手(HTTPS)→ HTTP 请求 → 服务端处理 → HTTP 响应。渲染阶段:解析 HTML 构建 DOM → 解析 CSS 构建 CSSOM → 合并为渲染树 → 布局计算位置大小 → 绘制像素 → 合成显示。JS 会阻塞 DOM 解析,CSS 会阻塞渲染。
面试官:什么是重排和重绘?如何优化?
你:重排是重新计算布局(改尺寸/位置),重绘是重新绘制像素(改颜色)。重排一定触发重绘。优化:1)批量修改 DOM(DocumentFragment);2)用 class 替代逐个改 style;3)避免频繁读取布局属性;4)动画用 transform/opacity(只触发合成,性能最好);5)使用 requestAnimationFrame。