display: none 和 visibility: hidden 的区别:重排、重绘与合成
Category(分类): Browser, CSS Status: 未知
原文把
display: none、visibility: hidden、opacity: 0与浏览器渲染流水线放在一起讨论。本文保留这些主线,修正“任何改变都会整页重排”“opacity 一定不重绘”“隐藏元素无法绑定事件”等绝对说法,并补充现代 CSS 过渡和可访问性注意事项。
一、三种隐藏方式的核心区别
1. display: none
display: none 不生成布局盒子,因此元素不占据页面空间。元素仍然存在于 DOM 中,JavaScript 仍然可以查询它、修改它、绑定事件,或者通过代码手动触发事件;但它不会成为用户鼠标、触摸或键盘焦点的目标。
它的后代也不会被渲染。子元素即使写了 display: block,也不能绕过祖先的 display: none。
2. visibility: hidden
visibility: hidden 让元素不可见,但通常保留其布局空间。visibility 是可继承属性,子元素可以显式设置 visibility: visible 来显示自己:
.parent {
visibility: hidden;
}
.parent .child {
visibility: visible;
}
隐藏的元素通常不能获得焦点,也不会作为普通用户指针事件的目标。子元素显式可见时,具体的绘制和命中测试还要结合布局、pointer-events 和浏览器实现判断。
3. opacity: 0
opacity: 0 让元素完全透明,但元素仍然占据布局空间。它通常仍在可访问性树中,也可能获得键盘焦点并响应鼠标事件,因此如果它代表“不可用”或“已关闭”,不能只设置透明度:
.is-hidden {
opacity: 0;
pointer-events: none;
}
opacity 不是简单的继承属性,但父元素的透明度会作用于整个渲染组,子元素设置 opacity: 1 也不能恢复被父元素整体透明化的内容。
二、对比表
| 特性 | display: none | visibility: hidden | opacity: 0 |
|---|---|---|---|
| 是否占据布局空间 | 否 | 是 | 是 |
| 是否生成布局盒子 | 否 | 是 | 是 |
| 后代能否单独显示 | 否 | 可以设置 visibility: visible | 不能抵消父元素整体透明 |
| 用户鼠标命中 | 否 | 通常否 | 通常可以,除非设置 pointer-events: none |
| 键盘焦点 | 否 | 通常否 | 仍可能可以 |
| DOM 事件监听 | 监听器仍存在,用户无法点击 | 监听器仍存在,用户通常无法点击 | 仍可接收事件 |
| 是否可直接渐变 | 传统上不能插值 | 离散切换,可与 opacity 配合 | 可以渐变 |
| 是否影响布局 | 会改变布局 | 不改变布局 | 不改变布局 |
| 可访问性 | 通常从可访问性树移除 | 通常从可访问性树移除 | 通常仍可被辅助技术感知 |
对于弹窗、菜单、抽屉等组件,隐藏不仅是视觉问题,还要同步处理焦点、键盘、屏幕阅读器、滚动和交互状态。
三、浏览器文档与渲染流程
原文将浏览器过程分为提交文档和渲染阶段,这个模型仍有助于理解,但不同浏览器的进程和线程实现会变化。可以用下面的高层流程理解:
- 浏览器从网络进程获得响应,创建或复用渲染进程;
- HTML 解析为 DOM;
- CSS 解析为 CSSOM,并计算每个节点的样式;
- 根据样式生成布局树和布局盒子;
- 计算几何位置和尺寸(Layout);
- 把可见内容绘制成绘制记录(Paint);
- 必要时进行栅格化,把绘制记录转换为位图;
- 合成线程组合图层并提交到屏幕(Composite)。
display: none 的节点仍可能存在于 DOM 中,但不会为它生成参与页面布局的盒子。不能简单理解为“浏览器完全不处理这个节点”或“所有 DOM 节点都会进入布局树”。

实际浏览器会进行局部失效、缓存、并行栅格化和图层复用。所谓“重排”“重绘”“合成”是便于沟通的概念,不代表每次修改都从头执行整页完整流程。
四、重排、重绘与合成
1. 布局(Layout,传统上也叫回流/重排)
当元素的几何属性发生变化,浏览器需要重新计算一个区域内元素的位置和尺寸。例如:
box.style.width = '400px'
box.style.padding = '24px'
box.style.display = 'none'
布局影响范围取决于文档结构、包含块、字体和布局算法。修改一个元素不一定导致整页布局,但可能使它的兄弟节点、父级或后续区域重新计算。
常见可能触发布局的变化包括:
width、height、padding、margin、border-width;top、left、right、bottom(对参与布局的定位方式而言);font-size、line-height、字体加载和文本内容;display、position、flex、grid相关属性;- 读取布局信息时触发浏览器同步完成之前积累的样式修改。

2. 绘制(Paint,传统上也叫重绘)
如果几何位置没有变化,只改变颜色、阴影、边框或其他绘制属性,浏览器可能跳过布局,重新绘制受影响区域:
box.style.backgroundColor = '#f00'
box.style.boxShadow = '0 4px 12px rgb(0 0 0 / 20%)'
绘制成本与面积、阴影、滤镜、文字、图片和遮罩有关。重绘通常比重新布局少一个阶段,但不能简单说“重绘一定很便宜”。

3. 合成(Composite)
某些变换可以在布局和绘制完成后,只改变已经准备好的图层位置或透明度。例如:
.card {
transform: translateY(20px);
opacity: 0.8;
}
transform 和 opacity 经常适合动画,因为浏览器有机会在合成阶段处理它们,减少主线程上的布局和绘制工作。但这不是绝对保证:
- 元素可能没有被提升为独立合成层;
filter、阴影、混合模式、复杂裁剪可能仍然需要绘制;- 图层过多会增加内存、上传和合成成本;
- 动画中的 JavaScript、布局读取或长任务仍会阻塞帧;
- 合成线程也有成本,并不是“完全不占资源”。
不要把“硬件加速”当成无条件的优化目标,应通过 DevTools Performance 和 Rendering 面板验证实际效果。
五、display、visibility 和 opacity 的过渡
1. 常用的淡入淡出写法
opacity 可以插值,visibility 是离散状态。把二者结合,可以让元素淡出后再变得不可见,避免透明元素继续挡住页面:
.fade {
opacity: 0;
visibility: hidden;
pointer-events: none;
transition:
opacity 200ms ease,
visibility 0s linear 200ms;
}
.fade.is-visible {
opacity: 1;
visibility: visible;
pointer-events: auto;
transition-delay: 0s;
}
如果元素需要从可访问性树中移除,还要同步使用 aria-hidden="true",并确保隐藏状态下没有可聚焦后代。对于模态框、抽屉等复杂组件,可以使用 inert 阻止隐藏区域接收焦点和交互:
<section class="drawer" aria-hidden="true" inert>
<!-- 隐藏时不能让焦点进入这里 -->
</section>
aria-hidden 不能加在仍包含当前焦点的元素上,也不能用来掩盖仍然可以操作的内容。
2. display 的离散过渡
传统 CSS Transition 不能把 display: none 平滑插值为 display: block,所以以前常见做法是使用 opacity + visibility,动画结束后再通过 JavaScript 设置 display。
现代 CSS 正在支持离散属性过渡,例如 transition-behavior: allow-discrete 和 @starting-style。示意写法如下,但需要根据目标浏览器兼容性验证:
.dialog {
display: none;
opacity: 0;
transition:
opacity 200ms ease,
display 200ms allow-discrete;
}
.dialog.is-open {
display: block;
opacity: 1;
}
@starting-style {
.dialog.is-open {
opacity: 0;
}
}
@starting-style 为首次从 display: none 进入显示状态提供起始样式;即使加入它,仍应在目标浏览器中验证打开和关闭动画。如果项目需要兼容旧浏览器,不应只依赖这套新语法。原生 <dialog>、框架过渡组件或 visibility 配合 opacity 往往更稳妥。
六、浏览器的样式队列与强制同步布局
浏览器通常会把一批样式修改合并后再布局和绘制,而不是每执行一行 JavaScript 就立刻刷新画面。但如果代码在写入样式后立即读取布局信息,浏览器可能被迫同步完成样式计算和布局:
box.style.width = '400px'
// 读取布局信息,可能强制浏览器立即完成布局
console.log(box.getBoundingClientRect().width)
常见的布局读取 API 包括:
offsetLeft、offsetTop、offsetWidth、offsetHeight;clientWidth、clientHeight、clientTop、clientLeft;scrollWidth、scrollHeight,部分场景下的scrollTop/scrollLeft;getBoundingClientRect()、getComputedStyle()的部分属性。
这些 API 并不是每次调用都必然造成昂贵布局,是否强制刷新取决于之前是否有未提交的样式变化和当前浏览器实现。真正需要避免的是反复交替读写造成的 layout thrashing:
// 不推荐:读写交替,可能反复触发布局
for (const item of items) {
item.style.width = `${container.offsetWidth}px`
}
可以先集中读取,再集中写入:
const width = container.getBoundingClientRect().width
for (const item of items) {
item.style.width = `${width}px`
}
对于动画和批量更新,还可以把写操作放入 requestAnimationFrame,但它不是解决所有布局问题的万能函数:
const width = container.getBoundingClientRect().width
requestAnimationFrame(() => {
for (const item of items) {
item.style.width = `${width}px`
}
})
七、如何减少布局、绘制和合成成本?
CSS 方面
- 对位移动画优先考虑
transform,对淡入淡出优先考虑opacity; - 不要频繁修改会影响大量后代和兄弟节点的几何属性;
- 使用 CSS class 一次性切换状态,而不是逐条设置内联样式;
- 复杂动画元素可结合
contain限制布局或绘制影响范围; content-visibility: auto可延迟屏幕外内容的布局和绘制,长列表需要配合contain-intrinsic-size避免滚动跳动;will-change只在确实需要且经过测量时使用,长期给大量元素设置会增加内存和合成层压力;- 表格不应一概禁止,确有表格数据时可根据场景使用
table-layout: fixed、虚拟列表或分页; - 图片和字体的尺寸应提前声明,减少布局偏移;
- 不要用透明元素挡住交互,隐藏状态同步处理
pointer-events、焦点和 ARIA。
JavaScript 方面
1. 使用 class 集中修改
panel.classList.toggle('is-open', shouldOpen)
.panel {
opacity: 0;
visibility: hidden;
}
.panel.is-open {
opacity: 1;
visibility: visible;
}
2. 批量创建 DOM 使用 DocumentFragment
const fragment = document.createDocumentFragment()
for (const item of data) {
const li = document.createElement('li')
li.textContent = item.label
fragment.append(li)
}
list.replaceChildren(fragment)
3. 临时隐藏元素再批量修改
原文建议把元素设为 display: none 后修改,再恢复显示。这在某些批量更新中可以减少中间布局,但会改变布局、影响焦点和可能触发一次较大的重新布局。优先考虑:
- 一次性切换 class;
- 在文档外的
DocumentFragment中构建新节点; - 将读取和写入分组;
- 使用
replaceChildren()替换完整子树; - 对大列表使用虚拟化;
- 通过
contain或content-visibility缩小影响范围。
如果确实使用 display: none,要保存并恢复当前焦点,避免用户正在输入的控件消失。
八、常见属性和场景的细化说明
display: none 是否会引起重排?
把一个已显示元素改为 display: none 通常会让它从布局中移除,可能影响后续内容的位置,因此会触发布局计算,并可能带来绘制和合成更新。再次显示也可能需要重新布局和绘制。但影响范围不一定是整页。
visibility: hidden 是否只触发重绘?
它通常保留布局盒子,改变可见性主要影响绘制和命中测试,常常不需要重新计算几何布局。但具体成本取决于元素、图层和浏览器实现,不能把“只重绘”当作标准保证。
opacity: 0 是否一定只合成?
不一定。若元素已经适合合成,动画可能只更新合成属性;首次创建图层、改变其他样式或涉及复杂效果时,仍可能发生布局和绘制。opacity: 0 还可能继续接收焦点和指针事件。
隐藏元素上的事件监听是否存在?
事件监听器不会因为 display: none 或 visibility: hidden 自动移除。区别在于用户通常无法通过鼠标或键盘触发它。代码调用 element.dispatchEvent() 仍然可以触发监听器,因此不要用“无法绑定事件”描述这两种状态。
九、原文示例的结果整理
是否占据页面位置
display: none:不占据;visibility: hidden:通常占据;opacity: 0:占据。



是否能被用户点击
display: none:不能被用户点击;visibility: hidden:通常不能被用户点击;opacity: 0:可能可以被点击,推荐隐藏时同时设置pointer-events: none。
子元素能否恢复显示
/* display: none 的祖先被移除布局,子元素无法绕过 */
.box-display-none .child {
display: block;
}
/* visibility 允许子元素显式恢复可见 */
.box-visibility-hidden .child {
visibility: visible;
}
/* opacity 作用于父级渲染组,子元素不能恢复父级透明度 */
.box-opacity-zero .child {
opacity: 1;
}

十、总结
display: none不生成布局盒子,通常会影响布局;visibility: hidden保留布局空间,子元素可以显式恢复可见;opacity: 0只是透明,可能仍然占据空间、获得焦点和响应事件;- 隐藏交互组件时,要同时考虑
pointer-events、焦点、inert、aria-hidden和滚动; - 布局、绘制和合成是高层模型,实际影响范围由浏览器实现和页面结构决定;
transform和opacity经常适合动画,但不是无条件的“硬件加速”;- 避免写读交替造成强制同步布局,使用批量更新、class、
requestAnimationFrame、contain和content-visibility; - 性能优化应通过 DevTools、PerformanceObserver 和真实用户数据验证,而不是背诵“某个属性一定触发回流或重绘”。
参考资料
- MDN:display
- MDN:visibility
- MDN:opacity
- MDN:CSS Transitions
- MDN:transition-behavior
- web.dev:高性能 CSS 动画
- Chrome Developers:Analyze runtime performance