跳到主要内容

浏览器的渲染和内存管理是一套连贯的机制。掌握浏览器渲染原理及垃圾回收原理,能写出更高效的代码。

一、 核心渲染流程

浏览器将 HTML/CSS/JS 转化为屏幕可见内容,需经过 6 个关键步骤,且前 4 步为构建阶段,后 2 步为渲染阶段

1. HTML ➩ DOM (文档对象模型)

  • 字节流转换:浏览器接收的 HTML 数据是字节流,需转化为字符流(如 UTF-8)
  • 标记化(tokenization):将字符流拆分未最小单元(标签、属性、文本等),并打上标记(如 htmllang=zh
  • DOM 树构建:基于标记生成的 DOM 树,树中每一个节点对应 HTML 元素,包含层级关系和基本属性
    • 浏览器通过 “HTML 解释器“ 逐行读取 HTML 代码,将每个标签(如 <div><p>)、属性、文本转化为树形结构的节点(DOM 节点)
    • 解析过程中遇见 <script> 标签,会 暂停 HTML 解析(js 可能会修改 DOM ),知道 JS 执行完成;若遇到 <link rel="stylesheet"> ,则并行解析 CSS,不阻塞 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>
  • 伪元素处理:如 ::before::after 虽不在 DOM 树中,但有集合信息,会被加入渲染树

4. 渲染树 ➩ 布局 (Layout,也叫回流/Reflow)

  • 几何信息计算:浏览器遍历渲染树,计算每一个节点 具体位置(x/y 坐标)和尺寸(宽/高),结果储存在 “布局树” 中
  • 过滤不可见节点:如 display: node 的节点不参与布局
  • 布局基于“视口(Viewport)”大小计算,若视口缩放(如用户调整浏览器窗口),会重新出发布局
  • 分层优化:根据节点特性(如 transformopacity、滚动条)创建层次树(Layout Tree),每个层对应布局树中的部分节点,后续可独立处理

5. 布局 ➩ 绘制

  • 浏览器根据布局树,将每一个节点的样式(颜色、背景、边框等)绘制到屏幕的像素网络 上,包括填充颜色、绘制边框、渲染图片等
  • 绘制是按层进行的(如文字层、背景层、图片层),后续可通过“图层合成”优化性能

6. 绘制 ➩ 合成

  • 浏览器将绘制好的“图层”合并成最终的屏幕图像,避免图层重叠导致的视觉错误
  • 若元素使用了 transformopacity 等属性,浏览器可直接操作图层合成,跳过布局和绘制,性能极高

二、 重排(Reflow)和重绘(Repaint):渲染流程的“二次触发”

当页面元素状态变化时,浏览器重新执行部分渲染流程,这就是重拍和重绘,两者的核心区别是 是否修改“布局信息”

1. 重排(回流):修改布局没,性能消耗高

元素的位置和尺寸发生变化(如:宽高、边距、位置),导致浏览器需要重新计算布局树,触发 “布局 ➩ 绘制 ➩ 合成” 流程。

常见触发场景

  • 修改元素的盒模型属性(widthheightmarginpaddingfloatpositiondisplay:node 等)
  • 增删 DOM 节点(如 appendChildremoveChild
  • 内容的改变(文本改变或图片大小改变)
  • 页面首次渲染
  • 改变视口大小(如窗口缩放、手机横屏)
  • 读取“布局相关属性”(如 offsetWidthgetComputedStyle
为什么查询布局属性会导致重排 ?

浏览器会自动优化 DOM 操作,将操作储存在列队中。当查询 offsetLeftoffsetWidth 等属性时,浏览器会强制刷新列队,因为不立即执行可能导致结果错误。

这相当于强制打断了浏览器的优化流程,引发了重排。

2. 重绘:仅改样式,性能消耗低

元素的样式变化但不影响布局(如颜色、背景),浏览器无需重新计算布局,直接触发 "绘制 ➩ 合成" 路程。

常见触发场景

  • 修改 colorbackground-colorborder-color 等前景色、背景色
  • 改变 box-shadowtext-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” 的过程中,读取了布局相关属性(如 offsetWidthclientHeightgetComputedStyle ),浏览器为了返回“最新的准确值”,会强制刷新“重新排队“,立即执行所有待处理的 DOM 修改触发重排
循环中修改 DOM 后立即读取布局属性,会触发 N 次重排
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 次重排
仅触发 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
  • 使用 DocumentFragmentrequestAnimationFrame 批量操作

2. 优化 CSS

  • 简化选择器,避免使用复杂的 CSS 选择器
  • 移除不必要的样式
  • 将 CSS 拆分未独立的模块
  • 最小化和压缩 CSS
  • 不将样式应用于不需要的元素
  • 使用 CSS 精灵图减少图像相关的 HTTP 请求
  • 预加载重要的资源:使用 rel="preload"<link> 元素转为预加载器,用于关键资源,包括 CSS 文件、字体和图片
  • 处理动画:使用 prefers-reduced-motion 的媒体查询,在用户对动画的操作系统级偏好选择性的提供动画样式
  • 优先使用 transformopacity 等不会触发重排的属性
  • 在 GPU 上进行动画处理:下面的使用场景浏览器将自动将这些动画发送到 GPU 处理
    • 3D 变换动画,例如 transform: translateZ()rotate3d()
    • 具有某些其他属性动画的元素,例如 position: fixed
    • 应用 will-change 的元素
    • 特定的在自己层中渲染的元素,包括 <video><canvas><iframe>
  • 使用 will-change 优化元素的变化:浏览器可能会在元素发生实际变化之前进行优化设置。这类优化可以通过提前完成可能需要大量的工作,提高页面的响应速度
  • 优化渲染阻塞:使用 media 属性告诉浏览器在何时应用什么样式
  • 改善字体性能
    • 字体加载:字体仅在使用 font-family 属性应用于元素时才会加载,而不是首次使用 @font-face at 规则引用时
      /*  字体在此处没有加载  */
      @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 代码,删除不必要的冗余代码
    • 减少循环代码的数量,并在合适的时机使用 breakcontinue
    • 将计算任务移到主线程之外
      • 使用异步代码
      • 在 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>
  • 视屏自动播放:要确保背景视屏自动循环播放,必须添加 autoplaymutedplaysinline 属性。虽然 loopautoplay 足以让视频循环和自动播放,但想在移动浏览器中自动播放的话,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 的优点加快用户访问资源。