8、DOM 树与渲染树(Render Tree)

说实话,很多做 Android 应用开发的同学,对 DOM 树可能还有点概念,但一提到 Render Tree 就懵了。我当年刚接触 WebKit 时也是这样——明明 DOM 树好好的,为什么还要搞个渲染树出来?这不是多此一举吗?

后来踩过坑才明白,这两棵树的关系,说白了就是「数据模型」和「显示模型」的区别。DOM 树管的是文档结构,而渲染树管的是「哪些东西需要画出来」。今天我们就来彻底搞懂它。

DOM 树结构

DOM 树,全称 Document Object Model Tree。它是 HTML 文档解析后的产物。每个 HTML 标签、文本、注释,都会变成 DOM 树上的一个节点。

举个例子:

<html>
  <head>
    <title>我的页面</title>
  </head>
  <body>
    <div id="container">
      <p>Hello World</p>
    </div>
  </body>
</html>

解析后,DOM 树大概是这样的结构:

Document
 └── html
      ├── head
      │    └── title
      │         └── "我的页面"
      └── body
           └── div#container
                └── p
                     └── "Hello World"

每个节点都是一个 Node 对象,具体类型有 ElementTextComment 等。它们通过 parentNodechildNodes 等指针串联起来。

关键点:DOM 树是「完整」的。所有节点都在里面,不管它是否可见。

RenderObject 与 RenderLayer

DOM 树建好了,但浏览器还不能直接拿它来画。为什么?你想想看,DOM 树里包含 <head><script>display:none 的元素,这些需要画出来吗?显然不需要。

所以 WebKit 又搞了一层——Render Tree(渲染树)。它的节点叫 RenderObject

RenderObject

每个 RenderObject 对应 DOM 树中一个「需要绘制」的节点。它知道自己的位置、大小、颜色、背景等样式信息。

我习惯把 RenderObject 理解为「画师手里的草稿」——它知道每个元素该画成什么样,但还没真正落笔。

RenderObject 的创建时机:

  • DOM 节点创建后,WebKit 会检查它的 CSS 样式
  • 如果样式要求显示(即 display != none),就创建一个对应的 RenderObject
  • 如果样式要求隐藏(display: none),就不创建

个人经验:我在做性能优化时发现,大量使用 display: none 的元素并不会拖慢渲染树的构建速度,因为它们压根不会进入渲染树。但 visibility: hidden 就不一样了——它仍然会创建 RenderObject,只是不画出来而已。

RenderLayer

RenderLayer 是更高一层的抽象。你可以把它理解为「一个独立的绘制平面」。比如 position: absolute 的元素、z-index 不为 auto 的元素、opacity 小于 1 的元素,都会创建自己的 RenderLayer。

为什么要搞 RenderLayer?

  • 支持层叠上下文(z-index 排序)
  • 支持独立滚动(overflow: scroll)
  • 支持硬件加速(GPU 合成)

我举个例子你就明白了:

<div style="position: relative; z-index: 10;">
  <p>我在上层</p>
</div>
<div style="position: relative; z-index: 1;">
  <p>我在下层</p>
</div>

这两个 div 会各自生成一个 RenderLayer。浏览器绘制时,先画下层,再画上层。这就是层叠上下文的底层实现。

Render Tree 的构建时机

Render Tree 什么时候开始构建?答案是:DOM 解析和 CSS 样式计算完成之后

具体流程是这样的:

  1. HTML 解析器边解析边构建 DOM 树
  2. CSS 解析器解析样式表,生成样式规则
  3. 每个 DOM 节点创建时,WebKit 会计算它的最终样式(Computed Style)
  4. 如果样式要求显示,就创建对应的 RenderObject
  5. RenderObject 之间建立父子关系,形成 Render Tree

注意,这个过程是渐进式的。不是等整个 DOM 树建完才开始构建渲染树,而是边解析边构建。这样可以尽早开始布局和绘制,提升首屏加载速度。

我曾经踩过的坑:有一次我写了一个很大的表格,里面塞了几千行数据。结果页面加载巨慢。后来发现,DOM 树构建很快,但渲染树构建时,每个单元格都要计算样式、创建 RenderObject,导致主线程卡死。解决方案是用虚拟滚动,只创建可见区域的 RenderObject。

Visibility 与 Display 的影响

这两个属性经常被混淆。我直接说结论:

属性 是否创建 RenderObject 是否占用布局空间 是否绘制
display: none
visibility: hidden
visibility: visible

看到区别了吗?

  • display: none 的元素,连 RenderObject 都不创建。它就像从 DOM 树里「消失」了一样,对布局毫无影响。
  • visibility: hidden 的元素,RenderObject 照常创建,布局也照常计算,只是最后一步「绘制」被跳过了。它占着位置,但你看不见它。

这个区别在实际开发中很重要。举个例子:

<!-- 这个 div 不会出现在渲染树中 -->
<div style="display: none;">我消失了</div>

<!-- 这个 div 会出现在渲染树中,但不会被绘制 -->
<div style="visibility: hidden;">我还在,但你看不见</div>

如果你用 JavaScript 去获取 offsetHeightdisplay: none 的元素返回 0,而 visibility: hidden 的元素返回正常高度。这就是因为前者没有 RenderObject,后者有。

避坑指南:如果你需要频繁切换元素的显示和隐藏,用 visibilitydisplay 性能更好。因为 display: none 会销毁 RenderObject,再次显示时要重新创建;而 visibility 只是控制绘制开关,RenderObject 始终存在。

核心逻辑图

下面这张 SVG 图展示了 DOM 树到渲染树的映射关系,以及 Visibility 和 Display 的影响:

DOM 树 → 渲染树 映射关系 DOM 树 html head title body div (display:none) p (visibility:hidden) 样式计算 渲染树 RenderView (对应 html) RenderBlock (对应 body) RenderBlock (对应 p) ✗ 未创建 (display:none) ✓ 已创建 (visibility:hidden) 已创建 未创建 已创建但不绘制

从图中可以清楚看到:display: none 的 div 在渲染树中完全没有对应节点;而 visibility: hidden 的 p 标签,渲染树中是有 RenderObject 的,只是绘制阶段被跳过了。

嗯,到这里你应该对 DOM 树和渲染树的关系有了清晰的认识。记住一句话:DOM 树管「有什么」,渲染树管「画什么」。搞懂这个,后面学布局和绘制就轻松多了。


公众号:蓝海资料掘金营,微信deep3321