一个值得关注的现象:如果将大型页面的平均 DOM 嵌套深度从 8 层削减至 4 层,其在用户手机上的可交互时间(TTI)将提前 120 到 180 毫秒。这一数据来源于 Chrome 120+ 在中端移动设备上的实际测试,而非实验室理想值。而实现这一优化的代价,仅仅是去掉几层无意义的 div 标签。
浏览器解析 HTML 是一个单线程、自上而下、深度优先的过程。每遇到一个开始标签,就会创建一个节点并压入栈,遇到结束标签时弹出。嵌套层级越深,栈操作越频繁,layout dirty 标记也会随之增加——尤其在首屏渲染阶段,这种开销会被集中放大。说得更直白一些:连续 10 层 div 嵌套与 4 层相比,浏览器解析器所需的 CPU 工作时间存在实质性的差异。
这并非停留在理论层面的优化,它直接影响首屏可交互时间(TTI)。为什么许多大型页面尽管内容并不复杂,首屏加载却异常缓慢?问题往往不在于资源加载,而在于 DOM 结构在不知不觉中被“包裹得过于厚重”。UI 框架的默认 wrapper 嵌套、CSS-in-JS 的 scope 隔离层,以及开发中无意识的“为了安全再多包一层”——这些习惯叠加在一起,硬生生将页面结构堆成了深度超过 8 层的“深井”。

哪些结构属于高代价嵌套,必须扁平化
以下三种模式在大型页面中频繁出现,且其解析成本被严重低估——我在性能审计中经常遇到它们:
- 框架栅格系统的三层包裹 ——
。Bootstrap 和 Ant Design 的栅格体系往往引入三层无语义的…
div,而实际上使用 CSS Grid 仅保留class="col"一层即可实现完全一致的布局效果。 - SSR 模板中的同质化容器 ——
。多余的… class="wrapper"以及缺乏语义需求的,均属于纯粹的冗余节点。 - 三层同根容器 —— SSR 模板中常见的
++组合,这三层容器的功能往往完全重叠,却各自贡献了一次节点创建和栈操作。
用 CSS 替代 DOM 节点的典型场景
许多为了实现布局、样式或状态隔离而引入的 DOM 层,实际上完全可以交由 CSS 来处理:
- 为了实现内边距或背景隔离而添加一层
—— 直接改用padding与background-clip: padding-box,简化一步。 - 为了控制溢出而添加一层
—— 直接在目标元素上设置overflow: auto和max-height,避免额外生成 stacking context。 - 为了条件显示而添加
—— 直接将class="spinner"挂载到宿主元素上,使用visibility: hidden或display: none控制显示即可。
还有一个值得留意的 CSS 属性:display: contents。在 Chrome 65+、Firefox 63+ 等较新版本浏览器中,该属性能使父元素不生成自己的盒子,从而在 DOM 树中“消失”。但它会导致父元素上的事件监听以及部分 CSS 属性(例如 border)失效,因此对交互容器需谨慎使用。
如何快速定位并清理冗余 DOM 节点
不要指望仅凭肉眼逐行扫描代码来发现冗余节点——很快就会眼花缭乱。利用 DevTools 暴露问题更为高效:
- 在 Elements 面板按 Ctrl+F(Win)或 Cmd+F(Mac),搜索
。如果大量div不带class、id或data-属性,那么它们 90% 是冗余的。 - 右键任意节点 → Break on → subtree modifications,然后触发页面加载或组件挂载。观察哪些 wrapper 被频繁插入又删除——SSR hydration 阶段这类问题尤为常见。
- 运行以下脚本检查 DOM 深度:
Array.from(document.querySelectorAll('*')).reduce((max, el) => Math.max(max, el.compareDocumentPosition(document.body) & Node.DOCUMENT_POSITION_CONTAINED_BY ? 1 : 0), 0)不过更实用的方式是直接查看节点链长度:el.closest('body')?.parentElement?.parentElement。
说实话,技术层面的识别与修改并不困难。真正的挑战在于说服协作方接受“去掉这层 div 不会影响功能”。我的建议是:将对比数据——First Paint、First Contentful Paint、DOM 节点数、可交互时间——直接附在 PR 描述中。数据比原理更具说服力。
