关于浏览器渲染机制的整体介绍
一、从 URL 到页面渲染的完整流程 URL 输入 ↓ 1. DNS 解析 → IP 地址 ↓ 2. TCP 三次握手 → 建立连接 ↓ 3. TLS 握手(HTTPS)→ 安全连接 ↓ 4. 发送 HTTP 请求 → 服务器返回 HTML ↓ 5. 浏览器解析 HTML → 构建 DOM 树 ↓ 6. 解析 CSS → 构建 CSSOM 树 ↓ 7. DOM + CSSOM → Render Tree(合成渲染树) ↓ 8. Layout(布局/回流)→ 计算每个节点位置 ↓ 9. Paint(绘制)→ 像素绘制 ↓ 10. Composite(合成)→ GPU 合成最终页面
二、解析 HTML 构建 DOM 树 2.1 解析过程 HTML 字节流 ↓ 字节 → 字符(解码) ↓ 字符 → Token(分词) ↓ Token → Node (构建节点) ↓ Node → DOM 树
2.2 遇到资源时的行为
标签
行为
是否阻塞 HTML 解析
<script>
下载 + 执行
✅ 阻塞(脚本会操作 DOM)
<script async>
下载不阻塞,下载完立即执行
❌ 但执行时阻塞
<script defer>
下载不阻塞,HTML 解析完后按顺序执行
❌
<link rel="stylesheet">(外部样式引入)
下载 CSS
❌ 不阻塞(但阻塞渲染)
<img>
异步下载图片
❌
<link rel="preload">
高优先级预加载
❌
三、解析 CSS 构建 CSSOM 树 3.1 CSS 解析规则 .container p { color : red; }// 先找所有 <p >,再判断是否在 .container 内
3.2 CSS 阻塞渲染
CSS 不阻塞 HTML 解析,但阻塞渲染
原因:浏览器需要 CSSOM 才能构建渲染树
如果不阻塞,页面会先显示无样式的闪烁内容(FOUC)
四、渲染树(Render Tree) 4.1 构建规则 DOM 树 + CSSOM 树 → 渲染树 渲染树只包含: ✅ 可见节点(display: block/inline /flex 等) ❌ 不包含不可见节点(display: none、head、meta 等)
4.2 与 DOM 树的区别
DOM 树
渲染树
包含内容
所有节点
可见节点
head/meta
✅
❌
display: none
✅
❌
visibility: hidden
✅
✅(占位但不可见 )
但是,这里有一个关键的前提:浏览器是在解析真实的 DOM 树时才会忽略它 。 使用 v-show 时,无论条件真假,Vue 都会将该元素真实地渲染到 DOM 树中 ,并且始终保留在 DOM 里。因此,在 Vue 将 DOM 提交给浏览器后,浏览器依然会对其进行解析。只不过在构建渲染树的那一瞬间,浏览器发现它有 display: none 样式,于是将其从渲染树中剔除,最终不会触发后续的布局(Layout)和绘制(Paint)工作 。
五、布局(Layout / 回流 Reflow) 5.1 什么是回流 回流 = 计算每个节点在屏幕上的精确位置和大小
触发条件:
页面首次加载
添加/删除可见 DOM 元素
元素位置变化(position、display)
元素尺寸变化(width、height、margin、padding)
内容变化(文字、图片大小改变)
浏览器窗口大小改变
读取布局属性(offsetTop、scrollWidth、getComputedStyle 等)
5.2 减少回流的方法 1. 批量修改 DOM
const list = document .getElementById ('list' )for (let i = 0 ; i < 10 ; i++) { const li = document .createElement ('li' ) li.textContent = `Item ${i} ` list.appendChild (li) } const fragment = document .createDocumentFragment ()for (let i = 0 ; i < 10 ; i++) { const li = document .createElement ('li' ) li.textContent = `Item ${i} ` fragment.appendChild (li) } list.appendChild (fragment) list.style .display = 'none' list.style .display = ''
2. 避免频繁读取布局属性
for (let i = 0 ; i < 10 ; i++) { console .log (element.offsetHeight ) element.style .height = `${i * 10 } px` } const height = element.offsetHeight for (let i = 0 ; i < 10 ; i++) { element.style .height = `${i * 10 } px` }
3. 使用 transform 代替 top/left
element.style .top = '100px' element.style .left = '50px' element.style .transform = 'translate(50px, 100px)'
六、绘制(Paint / 重绘 Repaint) 6.1 什么是重绘 重绘 = 将节点绘制到屏幕上(不涉及布局计算)
触发条件(不改变几何属性):
颜色变化(color、background-color)
背景图片变化
visibility 变化
阴影变化(box-shadow、text-shadow)
outline 变化
6.2 回流 vs 重绘
回流(Reflow)
重绘(Repaint)
触发条件
几何属性变化
外观属性变化
代价
高(重新计算布局)
低(重新绘制像素)
关系
回流一定会触发重绘
重绘不一定触发回流
七、合成(Composite) 7.1 什么是合成 合成 = 将多个层合并成最终页面
渲染流程:Layout → Paint → Composite Layout:计算几何信息 Paint:绘制到多个层(Layer) Composite:GPU 合成最终页面
7.2 层(Layer)的概念 .element { will-change : transform; transform : translateZ (0 ); contain : layout; }
什么时候创建新层:
position: fixed/sticky
will-change: transform/opacity
transform: translateZ(0)
video、canvas、iframe
CSS 动画(@keyframes)
7.3 GPU 加速 transform : translate ();opacity : 0.5 ;filter : blur (10px );
八、JS 执行与渲染的关系 8.1 渲染线程与 JS 线程互斥 因为 JS 可以操作 DOM,如果同时渲染会导致不一致。浏览器在 JS 执行时会暂停渲染。
8.2 浏览器渲染流程(每帧) ┌─────────────────────────────────────┐ │ 一帧的时间(16 .6 ms) │ ├─────────────────────────────────────┤ │ 1 . 处理 JS 回调(宏任务) │ │ 2 . 清空微任务队列 │ │ 3 . requestAnimationFrame 回调 │ │ 4 . Layout(如果需要) │ │ 5 . Paint(如果需要) │ │ 6 . Composite(如果需要) │ └─────────────────────────────────────┘
8.3 requestAnimationFrame 的作用 function animate ( ) { element.style .transform = `translateX(${x} px)` x++ requestAnimationFrame (animate) } requestAnimationFrame (animate)
九、浏览器缓存机制 9.1 强缓存 Cache -Control : max -age =3600 Expires: Thu, 01 Jan 2025 00 :00 :00 GMT 命中强缓存:直接用缓存,不发请求(状态码 200 from memory/disk cache )
9.2 协商缓存 Last - Modified / If - Modified - Since // 基于时间ETag / If - None - Match // 基于内容指纹(推荐)命中协商缓存:服务器告诉浏览器缓存有效(状态码 304 Not Modified )
9.3 缓存流程 请求 → 有缓存? → 强缓存有效? → 直接用(200 from cache) → 强缓存失效? → 发请求携带 ETag/ Last - Modified → 服务器返回 304 ? → 用缓存 → 服务器返回 200 ? → 更新缓存
十、浏览器存储
存储方式
容量
过期时间
作用域
用途
Cookie
4KB
可设置
同源
鉴权、跟踪
localStorage
5-10MB
永久
同源
持久化数据
sessionStorage
5-10MB
会话结束
同源+窗口
临时数据
IndexedDB
几乎无限
永久
同源
大量结构化数据
Cache API
可配置
可设置
同源
PWA 离线缓存
适用场景 Cookie — 鉴权与跟踪
用户登录状态(Session ID、JWT Token)
购物车数据(小型电商)
用户偏好设置(语言、主题)
第三方跟踪(广告、统计)
CSRF 防护(CSRF Token)
localStorage — 持久化本地数据
用户偏好设置(主题、语言、布局)
表单草稿自动保存
离线数据缓存(PWA)
本地缓存不常变的配置数据
历史记录(搜索历史、浏览记录)
sessionStorage — 会话级临时数据
多页表单的数据传递(如分步注册)
页面间的状态保持(如 Tab 切换)
一次性密码/验证码的临时存储
页面刷新后恢复状态
同一窗口的标签页间数据隔离
IndexedDB — 大量结构化数据
离线应用数据存储(PWA、离线文档)
大文件缓存(图片、视频元数据)
客户端数据仓库(替代部分后端存储)
复杂查询(支持索引、游标、事务)
本地数据库(如笔记应用、待办应用)
Cache API — 网络资源缓存
PWA 离线缓存(Service Worker 配合)
静态资源缓存(JS、CSS、图片)
API 响应缓存(减少网络请求)
字体文件缓存(避免重复下载)
预缓存关键资源(首屏加载优化)
十一、预加载与预连接 <link rel ="dns-prefetch" href ="//cdn.example.com" > <link rel ="preconnect" href ="https://fonts.googleapis.com" > <link rel ="preload" href ="main.js" as ="script" > <link rel ="prefetch" href ="next-page.html" > <link rel ="prerender" href ="next-page.html" >
各指令适用场景 dns-prefetch — DNS 预解析
CDN 域名:静态资源和主站不同域
第三方域名:统计(Google Analytics)、广告(AdSense)
API 域名:后端接口地址
登录后跳转的域名:OAuth 回调地址
任何 100% 确定会访问的外部域名
<link rel ="dns-prefetch" href ="//api.example.com" > <link rel ="dns-prefetch" href ="//cdn.example.com" > <link rel ="dns-prefetch" href ="//fonts.googleapis.com" >
preconnect — 预连接
高优先级域名:首屏必须的资源(字体、关键 API)
跨域 API 请求:减少首次请求延迟
第三方登录:微信、Google、GitHub OAuth
视频/音频 CDN:流媒体资源
<link rel ="preconnect" href ="https://fonts.googleapis.com" crossorigin > <link rel ="preconnect" href ="https://fonts.gstatic.com" crossorigin > <link rel ="preconnect" href ="https://api.example.com" >
preload — 预加载(高优先级)
关键 CSS/JS:首屏渲染必须的资源
关键图片:首屏大图、Hero Image
字体文件:避免 FOUT(字体闪烁)
数据预取:首屏需要的 API 数据
<link rel ="preload" href ="critical.css" as ="style" > <link rel ="preload" href ="main.js" as ="script" > <link rel ="preload" href ="hero.webp" as ="image" > <link rel ="preload" href ="font.woff2" as ="font" type ="font/woff2" crossorigin >
prefetch — 预获取(低优先级)
下一页资源:用户可能访问的下一个页面
非关键但可能用到的资源:侧边栏、推荐内容
空闲时预加载:不影响当前页面性能
<link rel ="prefetch" href ="/about" > <link rel ="prefetch" href ="/dashboard" > <link rel ="prefetch" href ="/api/comments?page=1" >
prerender — 预渲染(提前渲染页面)
已知用户即将跳转的页面:登录后跳转首页
搜索结果预渲染:用户点击概率高的结果
付费页面预渲染:从购物车到结算页
<link rel ="prerender" href ="/dashboard" >
对比总结
指令
优先级
下载时机
执行/渲染
适用场景
dns-prefetch
低
立即
仅 DNS 解析
所有外部域名
preconnect
中
立即
DNS + TCP + TLS
高优先级域名
preload
高
立即
不执行/不渲染
首屏关键资源
prefetch
低
空闲时
不执行/不渲染
下一页资源
prerender
最低
空闲时
完整渲染
已知跳转页面
十二、性能优化总结 12.1 加载优化
减少 HTTP 请求(合并文件、雪碧图)
压缩资源(gzip、minify)
使用 CDN
图片优化(WebP、懒加载)
预加载关键资源
12.2 渲染优化
减少回流(批量修改、使用 transform)
减少重绘(使用 class 批量修改)
使用 GPU 加速(transform、opacity)
使用 requestAnimationFrame 做动画
虚拟滚动(大列表)
12.3 代码优化
减少主线程阻塞(Web Worker)
事件委托
防抖/节流
代码分割(Code Splitting)
懒加载(Lazy Load)
十三、高频问题汇总 Q1: 为什么 CSS 不阻塞 DOM 解析但阻塞渲染? HTML 解析和 CSS 解析是并行的。但渲染需要 DOM + CSSOM 合成渲染树,所以必须等 CSS 加载完才能渲染。
Q2: async 和 defer 的区别?
async:下载完立即执行,执行时阻塞 HTML 解析,不保证顺序
defer:HTML 解析完后按顺序执行,不阻塞 HTML 解析
Q3: 回流和重绘的区别?
回流:几何属性变化(位置、大小),需要重新计算布局
重绘:外观属性变化(颜色、背景),只需重新绘制像素
回流一定触发重绘,重绘不一定触发回流
Q4: 如何减少回流?
批量修改 DOM(DocumentFragment、一次性 cssText)
避免频繁读取布局属性
使用 transform 代替 top/left
使用 visibility 代替 display: none
使用 absolute/fixed 脱离文档流
Q5: 浏览器渲染一帧的流程? JS 回调 → 微任务 → rAF → Layout → Paint → Composite,每帧约 16.6ms(60fps)
Q6: 什么是强制同步布局? element.style .width = '100px' console .log (element.offsetWidth )
十四、一句话总结 浏览器渲染核心流程:解析 HTML/CSS → 构建 DOM/CSSOM → 合成渲染树 → 布局(回流)→ 绘制(重绘)→ 合成。性能优化的关键是减少回流、合理使用缓存、利用 GPU 加速。