浏览器的渲染和内存管理是一套连贯的机制。掌握浏览器渲染原理及垃圾回收原理,能写出更高效的代码。
一、 核心渲染流程
浏览器将 HTML/CSS/JS 转化为屏幕可见内容,需经过 6 个关键步骤,且前 4 步为构建阶段,后 2 步为渲染阶段。
1. HTML ➩ DOM (文档对象模型)
- 字节流转换:浏览器接收的 HTML 数据是字节流,需转化为字符流(如 UTF-8)
- 标记化(tokenization):将字符流拆分未最小单元(标签、属性、文本等),并打上标记(如
html、lang=zh) - DOM 树构建:基于标记生成的 DOM 树,树中每一个节点对应 HTML 元素,包含层级关系和基本属性
- 浏览器通过 “HTML 解释器“ 逐行读取 HTML 代码,将每个标签(如
<div>、<p>)、属性、文本转化为树形结构的节点(DOM 节点) - 解析过程中遇见
<script>标签,会 暂停 HTML 解析(js 可能会修改 DOM ),知道 JS 执行完成;若遇到<link rel="stylesheet">,则并行解析 CSS,不阻塞 HTML 解析。
- 浏览器通过 “HTML 解释器“ 逐行读取 HTML 代码,将每个标签(如
2. CSS ➩ CSSOM (css 对象模型)
- 预解析线程:浏览器启用预解析线程提前下载外部 CSS 文件,并行解析
- CSSOM 生成:浏览器通过 ”css 解析器“ 读取所有 CSS (內联、内部、外部),将样式规则转化为树形结构的节点 (CSSOM 节点)
- 样式计算:CSSOM 具有“继承性”(子节点继承父节点样式)和“优先级”(如
!important> 内联样式 > ID 选择器),且 CSSOM 构建完成前,浏览器不会开始渲染 (需结合 DOM 确定元素的最终样式)
3. DOM + CSSOM ➩ 渲染树(Render Tree)
- 浏览器遍历 DOM 树,为每一个 可见元素 匹配对应的 CSSOM 样式,生成“渲染树”
- 渲染树仅包含“可见元素”,会过滤掉
- 不可见的 DOM (如
<head>标签、display: none的元素) - 不影响视觉的节点(如
<script>、<style>)
- 不可见的 DOM (如
- 伪元素处理:如
::before、::after虽不在 DOM 树中,但有集合信息,会被加入渲染树
4. 渲染树 ➩ 布局 (Layout,也叫回流/Reflow)
- 几何信息计算:浏览器遍历渲染树,计算每一个节点 具体位置(x/y 坐标)和尺寸(宽/高),结果储存在 “布局树” 中
- 过滤不可见节点:如
display: node的节点不参与布局 - 布局基于“视口(Viewport)”大小计算,若视口缩放(如用户调整浏览器窗口),会重新出发布局
- 分层优化:根据节点特性(如
transform、opacity、滚动条)创建层次树(Layout Tree),每个层对应布局树中的部分节点,后续可独立处理
5. 布局 ➩ 绘制
- 浏览器根据布局树,将每一个节点的样式(颜色、背景、边框等)绘制到屏幕的像素网络 上,包括填充颜色、绘制边框、渲染图片等
- 绘制是按层进行的(如文字层、背景层、图片层),后续可通过“图层合成”优化性能
6. 绘制 ➩ 合成
- 浏览器将绘制好的“图层”合并成最终的屏幕图像,避免图层重叠导致的视觉错误
- 若元素使用了
transform、opacity等属性,浏览器可直接操作图层合成,跳过布局和绘制,性能极高
二、 重排(Reflow)和重绘(Repaint):渲染流程的“二次触发”
当页面元素状态变化时,浏览器重新执行部分渲染流程,这就是重拍和重绘,两者的核心区别是 是否修改“布局信息”。
1. 重排(回流):修改布局没,性能消耗高
元素的位置和尺寸发生变化(如:宽高、边距、位置),导致浏览器需要重新计算布局树,触发 “布局 ➩ 绘制 ➩ 合成” 流程。
常见触发场景
- 修改元素的盒模型属性(
width、height、margin、padding、float、position、display:node等) - 增删 DOM 节点(如
appendChild、removeChild) - 内容的改变(文本改变或图片大小改变)
- 页面首次渲染
- 改变视口大小(如窗口缩放、手机横屏)
- 读取“布局相关属性”(如
offsetWidth、getComputedStyle)
浏览器会自动优化 DOM 操作,将操作储存在列队中。当查询 offsetLeft、offsetWidth 等属性时,浏览器会强制刷新列队,因为不立即执行可能导致结果错误。
这相当于强制打断了浏览器的优化流程,引发了重排。
2. 重绘:仅改样式,性能消耗低
元素的样式变化但不影响布局(如颜色、背景),浏览器无需重新计算布局,直接触发 "绘制 ➩ 合成" 路程。
常见触发场景
- 修改
color、background-color、border-color等前景色、背景色 - 改变
box-shadow、text-shadow等阴影变化 - 切换
visibility: hidden(仅隐藏,保留布局,触发重绘)
3. 两者的关系
- 重排必然会重绘:布局变了,样式自然要重新绘制
- 重绘不一定重排:样式变了,布局可能不会变化
- 性能优先级:重排的性能消耗远高于重绘,优化的核心是”减少重排的次数“
三、 垃圾回收
js 是自动内存管理语言,无需手动释放内存,”垃圾回收(Garbage Collection)“ 负责回收 “不再使用的内存”,避免内存泄漏。
┌─────────────────────────────────────────┐
│ 新生代 (New Space) │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ From │ │ To │ │
│ │ 存储: │ │ 作用: │ │
│ │ 短期存活对象 │ │ 复制存活对象 │ │
│ │ (局部变量) │ │ 清空 From 区 │ │
│ └─────────────┘ └─────────────┘ │
│ 算法:Scavenge(复制算法) │
└─────────────────────────────────────────┘
↓(对象存活超2次)
┌─────────────────────────────────────────┐
│ 老生代 (Old Space) │
│ ┌─────────────────────────────────┐ │
│ │ 存储: │ │
│ │ 长期对象 (全局变量、DOM) │ │
│ │ 算法: │ │
│ │ 1. 标记-清除(删垃圾) │ │
│ │ 2. 标记-整理(紧凑内存) │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
1. 什么是垃圾
- 不再被引用的对象(如局部函数执行完毕后,内部变量失去引用)
- 引用链断裂的对象(如两个相互引用的对象,外部无任何引用)
2. 浏览器核心垃圾回收算法
V8 是 Chrome 和 Node.js 的核心引擎,采用 ‘分代回收’策略,将内存分为“新生代”和“老生代”,对应不同算法:
| 内存区域 | 储存对象 | 回收算法 | 核心逻辑 |
|---|---|---|---|
| 新生代 | 短期对象(如局部对象) | Scavenge(复制算法) | 将内存分为两个区域(From/To),存货对象复制到 To 区,清空 From 区,效率极高 |
| 老生代 | 长期对象(如全局对象、DOM 节点) | 标记 - 清除(Mark-Sweep) + 标记-整理(Mark-Compact) | 1. 标记:遍历内存,标记存活对象;2. 清除:删除未标记的“垃圾”;3. 整理:将存活对象紧凑排列,减少内存碎片 |
3. 常见的内存泄漏场景
- ❌ 意外的全局场景(如
var a = 123未声明,挂载在window上) - ❌ 未清除的定时器(
setInterval未使用clearInterval销毁) - ❌ 未解绑的事件监听(如
addEventListener后未removeEventListener) - ❌ 未释放的 DOM 引用(如删除 DOM 节点后,仍保留变量引用)
四、 连续修改 DOM 为什么会导致重排?
浏览器本身会优化重排,但若连续修改 DOM 时触发 “强制同步布局”,会直接打破优化,导致多次重排。
1. 浏览器的重排优化:“重排列队”
- 浏览器会将“DOM 修改操作”(如
element.style.width = '100px)暂存到“重排列队”中,而非每次修改立即触发重排 - 当列队积攒到一定数量,或当当前事件循环结束时,浏览器会 批量处理列队的操作, 只会触发一次重排,减少性能消耗
2. 连续修改 DOM 导致重排的核心:“强制同步布局”
- 若在 “连续修改 DOM” 的过程中,读取了布局相关属性(如
offsetWidth、clientHeight、getComputedStyle),浏览器为了返回“最新的准确值”,会强制刷新“重新排队“,立即执行所有待处理的 DOM 修改触发重排
const div = document.querySelector('.box');
// ❌ 错误示例:循环 10 次,触发 10 次重排
for (let i = 0; i < 10; i++) {
div.styles.width = `${i * 10}px`;
// 读取布局属性,强制刷新列队,触发重排
console.log(div.offsetWidth);
}
3. 优化方案:“读写分离”
- 先批量完成所有“写操作”(修改 DOM),再统一进行“读操作”(读取布局属性),仅触发 1 次重排
const div = document.querySelector('.box');
// 批量写操作
for (let i = 0; i < 10; i++) {
div.style.width = `${i * 10}px`;
}
// 统一读操作
console.log(div.offsetWidth);
五、 懒加载
懒加载(延迟加载)是一种将资源标识为非阻塞(非关键)资源并且在仅需要它们的时候加载的策略。这是一种缩短关键渲染路径长度的方法,可以缩短页面的加载时间。
最常规的做法就是代码拆分,将 JavaScript、CSS、HTML 分割成较小的代码块。这样前期发送的所需的最小代码,改善页面的加载时间。其余的按需加载。
1. JavaScript
脚本类型模块化,任何类型为 type="module" 的脚本标签都被视为一个 JavaScript 模块,并且默认情况下会被延迟。
2. CSS
默认情况下,CSS 被视为渲染阻塞资源,因此,在 CSSOM 构造完成之前,浏览器不会渲染任何已处理的内容。CSS 尽量小,才能尽快传达,建议使用媒体类型和查询实现非阻塞渲染
<link href="style.css" rel="stylesheet" media="all" />
<link href="portrait.css" rel="stylesheet" media="(orientation:portrait)" />
<link href="print.css" rel="stylesheet" media="print" />
3. 字体
默认情况下,字体请求会延迟到构造渲染树之前,只可能会导致文本渲染延迟。可以使用 **<link rel="preload">、 CSS font-display 属性和字体加载 API 来覆盖默认行为并预加载网络字体资源。
4. 图片和 iframe
<img> 元素上的 loading 属性(或 <iframe> 上的 loading 属性)可用于指示浏览器推迟加载屏幕外的图像/iframe,直到用户滚动到其旁边
<img src="image.jpg" alt="..." loading="lazy" />
<iframe src="video-player.html" title="..." loading="lazy"></iframe>
当早期加载的内容全部加载完毕后,load 事件就会触发;这时,可能在可视视口范围内有一些延迟加载的图片还没有加载。就可以通过布尔属性 complete 值来确定某一个图片是否加载完成
六、 性能优化建议
1. 避免频繁 DOM 操作
- 避免在循环中操作 DOM
- 使用
DocumentFragment或requestAnimationFrame批量操作
2. 优化 CSS
- 简化选择器,避免使用复杂的 CSS 选择器
- 移除不必要的样式
- 将 CSS 拆分未独立的模块
- 最小化和压缩 CSS
- 不将样式应用于不需要的元素
- 使用 CSS 精灵图减少图像相关的 HTTP 请求
- 预加载重要的资源:使用
rel="preload"将<link>元素转为预加载器,用于关键资源,包括 CSS 文件、字体和图片 - 处理动画:使用
prefers-reduced-motion的媒体查询,在用户对动画的操作系统级偏好选择性的提供动画样式 - 优先使用
transform、opacity等不会触发重排的属性 - 在 GPU 上进行动画处理:下面的使用场景浏览器将自动将这些动画发送到 GPU 处理
- 3D 变换动画,例如
transform: translateZ()和rotate3d() - 具有某些其他属性动画的元素,例如
position: fixed - 应用
will-change的元素 - 特定的在自己层中渲染的元素,包括
<video>、<canvas>、<iframe>
- 3D 变换动画,例如
- 使用
will-change优化元素的变化:浏览器可能会在元素发生实际变化之前进行优化设置。这类优化可以通过提前完成可能需要大量的工作,提高页面的响应速度 - 优化渲染阻塞:使用
media属性告诉浏览器在何时应用什么样式 - 改善字体性能:
- 字体加载:字体仅在使用
font-family属性应用于元素时才会加载,而不是首次使用@font-faceat 规则引用时/* 字体在此处没有加载 */
@font-face {
font-family: 'Open Sans';
src: url('OpenSans-Regular-webfont.woff2') format('woff2');
}
/* 奇怪,这里不能放空格 */
h1,
h2,
h3 {
/* 字体实际上在此处加载 */
font-family: 'Open Sans';
} - 只加载需要的字形
@font-face {
font-family: 'Open Sans';
src: url('OpenSans-Regular-webfont.woff2') format('woff2');
unicode-range: U+0025-00FF;
} - 使用
font-display描述定义字体的显示行为
- 字体加载:字体仅在使用
- 使用 CSS 局限优化样式重新计算:(浏览器目前对
contain: content支持不太友好)通过设置 CSS 局限模块中定义属性,可以让浏览器将页面的不同部分隔离开,并独立优化它们的渲染。
3. 优化 JavaScript
- 并不总是需要框架,有时候,小项目,使用几行标准的 JavaScript 来实现功能更不错
- 考虑更简单的解决方案
- 删除未使用的代码
- 避免在循环中查询布局属性
- 尽早加载关键资源
- 分解长任务
- 优化事件性能
- 在完成需求后及时清理不需要的事件监听
- 使用事件委托,减少要跟踪的事件监听器的数量可以提高性能
- 编写更高效的代码
- 减少 DOM 操作
- 批量进行 DOM 更改
- 简化 HTML 代码,删除不必要的冗余代码
- 减少循环代码的数量,并在合适的时机使用
break和continue - 将计算任务移到主线程之外
- 使用异步代码
- 在 Web Worker 中进行计算
- 使用 WebGPU
- 将获取的布局属性值赋值给变量,再进行操作
- 使用
requestAnimationFrame处理动画 - 推迟非关键 JavaScript 的执行
- 给
<script>元素添加async属性 - 直到需要时才使用 DOM 脚本添加 JavaScript
- 使用
import()函数动态加载 JavaScript 模块
- 给
- 使用浏览器内置的特性
- 使用内置的客户端表单验证
- 使用浏览器自带的
<video>播放器 - 使用 CSS 动画而不是 JavaScript 动画库
4. 避免不必要的重排/重绘
- 使用
visibility: hidden代替display: none(前者仅触发重绘,后者会触发重排) - 使用 CSS 动画代替 JavaScript 动画
- 避免使用 table 布局(table 元素在改变时会触发真个 table 的重排)
5. 优化多媒体
- 优化图像传递:
- 加载策略:懒加载可视区域以下的图像,而不是在初始加载时,无论访客是否滚动参看,都下载这些内容。浏览器厂商目前正在开发一种原生的
lazyload属性,目前处于试验阶段 - 最优格式:最优格式通常取决于图像的特点。
- SVG 格式更适合颜色较少且不太逼真的照片的图像。如果这些图像仅以位图形式存在,这应选择 PNG 做后备格式
- PNG 可以以三种不同的输出组合进行保存,且支持透明格式,但是可能储存过大
- JPEG 有良好的压缩,用户会先看到一个低分辨率的图像版本,随着图像下载而逐渐清晰,而不是图像全分辨率从上而下的加载
- WebP 即适用于图像又适用于动图的绝佳选择。提供了比 PNG 或 JPEG 更好的压缩,支持更高的色深、动画帧和透明度
- AVIF 高性能和免税版的图像格式(甚至比 WebP 更高效),是动图和图像的不错选择
- 提供最佳尺寸:使用
<picture>和<source>元素建立针对分辨率不同而提供的不同的图像 - 控制下载图像的优先级:将重要的图像优先展示给访问者会提供更好的感知性能
- 加载策略:懒加载可视区域以下的图像,而不是在初始加载时,无论访客是否滚动参看,都下载这些内容。浏览器厂商目前正在开发一种原生的
- 在加载图像时避免卡顿:由于图像是异步加载的,并且第一次绘制后继续加载,如果在加载前未定义其尺寸,会导致页面重新布局。在配置元素的 CSS 时,给定元素图片要渲染的尺寸
- 压缩所有的视频:大多数的视频压缩工作都是比较视频内相邻帧之间的细节,目的是移除两帧中的相同的细节
- 优化
<source>顺序:按从小到大的顺序排序视频源<video width="400" height="300" controls="controls">
<!-- WebM: 10 MB -->
<source src="video.webm" type="video/webm" />
<!-- MPEG-4/H.264: 12 MB -->
<source src="video.mp4" type="video/mp4" />
<!-- Ogg/Theora: 13 MB -->
<source src="video.ogv" type="video/ogv" />
</video> - 视屏自动播放:要确保背景视屏自动循环播放,必须添加
autoplay、muted、playsinline属性。虽然loop和autoplay足以让视频循环和自动播放,但想在移动浏览器中自动播放的话,muted属性是必需的。playsinline对于移动 Safari 浏览器来说是必需的,它允许视频播放不强制全屏模式 - 从无声视频中移除音频:对于没有无声播放的视频,移除音频可以节省 20% 的宽带
<video autoplay="" loop="" muted="true" playsinline="" id="hero-video">
<source src="banner_video.webm" type='video/webm; codecs="vp8, vorbis"' />
<source src="web_banner.mp4" type="video/mp4" />
</video> - 视频预加载:预加载属性有
auto(播放前会下载整个视频)、metadata(默认值,在播放前下载视频的 3%)、none(播放开始之前,不会被下载) 三个可选值。控制在页面加载多少视频可以节省数据 - 考虑使用流媒体:视频流媒体允许适当的视频大小和宽带(基于网络速度)被传送到最终用户。类似于响应式图像,正确大小的视频会被传送到浏览器,确保视频快速启动、缓存少以及播放优化
6. 使用 CDN 托管静态资源
这样可以减少加载时间,也可以运用 CDN 的优点加快用户访问资源。