如何减少 HTML 的 DOM 节点数量来提升渲染性能?
把 body 内 DOM 节点压到 1500 以下、嵌套不超过 32 层、单父节点子元素不超过 60 个可提升渲染性能,具体做法是用伪元素替代装饰 div、合并 PC 与移动双份 DOM、用 SVG 或字体图标、长列表上虚拟滚动。
结论先行:把 body 内的 DOM 节点总数压到 1500 以下、最大嵌套深度不超过 32 层、单个父元素的直接子元素不超过 60 个,这是 Chrome 团队给出的三条经验阈值;节点数超过这个量级后,样式重算(Recalculate Style,浏览器重新计算每个元素最终样式)和布局(Layout)的耗时会随节点数接近线性增长,首屏渲染和滚动都会肉眼可见地变慢。
DOM 节点多少算多?
结论:Chrome 官方建议的经验值是总节点数 1500 以内、最深 32 层、单父节点子元素 60 个以内,Lighthouse 的「Avoid an excessive DOM size」审计就是按这三条评估的。
节点多的代价集中在三处:样式重算、布局、内存。每次 JS 修改样式或触发重排,浏览器都要遍历样式树,节点数翻倍,这一步的耗时也接近翻倍。同时每个节点都是一个常驻的 JS 对象,占据内存。注意 display: none 只让节点不参与布局,节点对象和内存占用一点没少。
怎么统计当前页面的 DOM 节点数?
结论:控制台一行 document.getElementsByTagName('*').length 拿到当前总数,用 Performance 面板看它随交互怎么涨。
- F12 打开控制台,输入
document.getElementsByTagName('*').length,返回的是包含html、head在内的全部元素节点数。 - 打开 Performance 面板点录制,做一遍真实操作(滚列表、展开菜单、开弹窗),停止后看顶部的 DOM Nodes 曲线。滚动一次列表节点数涨几百上千,说明没做虚拟滚动或 DOM 没复用。
- 跑一次 Lighthouse,DOM size 那栏会直接点名「Max Depth」和「Max Children」落在哪个元素上,按它给的选择器去改最省力。
怎么在不改设计的前提下砍掉一半节点?
结论:砍节点最有效的三个方向是装饰用的空 div、PC/移动双份 DOM、一层套一层的无语义包装。
装饰元素一律用伪元素替代。一条分割线、一个小圆点、一个箭头写成 .card::after,比多挂一个 <div class="line"> 少一个 DOM 节点——伪元素参与渲染,但不产生节点、不产生 JS 对象。原来的 <div class="card"><div class="title">x</div><div class="line"></div></div> 是 3 个节点,改成 <article class="card"><h2>x</h2></article> 配 .card::after,只剩 2 个。
PC/移动双份 DOM 合成一套。同时输出两套导航、再用媒体查询藏掉一套的写法,节点数直接翻倍,改成一套结构配 flex/grid 重排,节点减半,顺带解决读屏软件重复朗读。
图标别用 div 拼。20 个 div 拼出来的图标换成内联 <svg> 或 sprite 里一个 <use>,就是 1 个节点,图标多的页面这项能省几百个。纯文本的加粗、变色用 font-weight、color 解决,别给每个词套 <span>。<ul><li>、<table> 这种语义标签自带层级,外面不用再套布局 div。
长列表怎么处理?
结论:列表超过 100 行就必须上虚拟滚动(windowing,只渲染可视区域内的行),这是所有手段里收益最大的一项,优先做。
react-window、vue-virtual-scroller、TanStack Virtual 都是现成方案。10000 行的表格不做虚拟滚动是 10000+ 节点,做了之后通常只渲染可视区 20-30 行加少量 overscan,节点数稳定在百位数以内,滚动时也不增长。
content-visibility 和 display: contents 能减少节点数吗?
结论:都不能。它们优化的是渲染成本,不是节点数量,别指望用它们通过 DOM size 审计。
content-visibility: auto(Chrome 85 起支持)让屏幕外的子树跳过布局和绘制,能显著降低首次渲染耗时,但节点仍然存在于 DOM 中,getElementsByTagName('*').length 的数字不变。display: contents 让包装元素不生成盒子(LayoutObject),减少的是布局对象,DOM 节点照旧,而且给 <button>、<a> 加 display: contents 会破坏无障碍语义,慎用。
真正「不占节点」的只有 <template>:里面的内容存在独立文档片段里,不进入主文档、不参与样式计算和布局,需要时再克隆插入。折叠面板、弹窗、懒加载模块都可以先塞进 template。
改完怎么验证?
结论:跑一次 Lighthouse 加一次 Performance 录制,确认 DOM size 审计通过、Recalculate Style 耗时下降,就是改对了。
验收标准就是那三条硬指标:总节点数 1500 以内、深度不超过 32、单父节点子元素不超过 60。如果列表页仍然超标,检查是不是隐藏的弹窗或下拉菜单在初始化时就把整棵子树渲染好了。
总结一句:先量后改——用控制台一行代码和 Performance 面板定位节点大户;能省的层次用伪元素和语义标签省掉,双份 DOM 合成一套,图标换成 SVG;长列表交给虚拟滚动;content-visibility 和 display: contents 只改善渲染成本,节点数该超还是超。
转载请注明出处,版权归原作者所有。