你或许听过别人说,transform
的性能更好,它不会触发重排,请使用 transfrom
来替代 position(left/right/top/bottom)
。
可他们之间的区别到底是什么?它为什么可以不触发重排呢?还有为什么 transform
会让其子元素的 z-index
失效呢?
希望本文可以解决你的这些疑问。
什么是重排和重绘
浏览器绘制图像的过程由两部分组成:
- 主线程计算所有HTML元素的CSS样式、绘制位图,将位图发给合成线程,执行这个过程一般称为重排(relayout/reflow)。
- 合成线程调用GPU将这些位图组合(compositor layer),绘制到屏幕上。执行这个过程一般称为重绘(repaint)。
什么情况下会触发重排
直接改变图形的样式会让浏览器频繁的进行重排(relayout)。重排意味着主线程需要计算所有HTML元素的位置,这是十分耗时的操作。
为什么要一定要进行重排呢?是因为默认的思路是这样的:一个图形的大小的变化会造成整个页面布局的变化。所以主线程应该计算整个页面的所有元素的位置,并将其发送给合成线程重新位置图像。
transform 做了什么?
从上面的解释我们可以看到,触发重排的条件是“重新布局”,如果一个元素位于它自己的作用域(即Stacking context层叠上下文)内,不影响其他任何元素的位置。那么理论上就可以避免重排,只将这个改变的元素进行重新绘制并和原有页面进行重新组合即可。
那么 position:absolute
可以实现让一个元素位于它自己的上下文内吗? 答案是不行,如果另一个元素与该元素的 z-index
相同,显然这个上下文并不是某个元素独有的。因此我们为一个绝对定位的元素添加动画时,依然会触发重排。例子见前端性能优化–transform与position .
终于,我们可以提到我们的主角 transform
了, transform
拥有自身独有的上下文。浏览器会为其单独创建一个复合层(composite layer),接下来页面的呈现就剩下合成线程与GPU的任务了。
具体情况下的 transform
表现
position:static
是元素不设置 position
时的默认布局,只设置 transform
的情况下,其功能类似于 position:relative
+ z-index : 0
,即使元素以及离开初始定位,依然会占据初始定位的位置:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
margin: 0;
padding: 0;
}
.a {
transition: all 2s ease;
transform: translate(0, 0);
background: red;
}
.animation {
transform: translate(100px, 100px);
}
</style>
</head>
<body>
<div class="a">a</div>
<div>b</div>
<button>click</button>
</body>
<script>
const btn = document.querySelector("button")
const a = document.querySelector(".a")
btn.addEventListener("click", () => {
a.classList.add("animation")
})
</script>
</html>
当 transform
元素和另一个绝对定位的元素重叠时,二者层级相同,可看作两者的层级均为 z-index : 0
,堆叠顺序由两个元素的先后顺序决定:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
margin: 0;
padding: 0;
}
.a {
transition: all 2s ease;
transform: translate(0, 0);
background: red;
}
.c {
position: absolute;
top: 100px;
left: 100px;
background: rebeccapurple;
}
.animation {
transform: translate(100px, 100px);
}
</style>
</head>
<body>
<div class="a">a</div>
<div class="c">c</div>
<button>click</button>
</body>
<script>
const btn = document.querySelector("button")
const a = document.querySelector(".a")
btn.addEventListener("click", () => {
a.classList.add("animation")
})
</script>
</html>
c
位于 a
之后,则其展示优先级高于 a
:
若 a
位于 c
之后,则 a
的展示优先级高于 c
:
<div class="c">c</div>
<div class="a">a</div>
如果拥有绝对布局的元素编写了 z-index:1
,则其层级高于 transform
元素:
<style>
...
.c {
...
z-index: 1;
}
</style>
<body>
<div class="c">c</div>
<div class="a">a</div>
</body>
给 a
元素加上 position : absolute
+ z-index : 2
,可以让其展示层级再次高于 c
:
<style>
.a {
...
position: absolute;
z-index: 2;
}
.c {
...
z-index: 1;
}
</style>
<body>
<div class="c">c</div>
<div class="a">a</div>
</body>
为什么 transform
子元素的 z-index
会失效?
假设我有一个 flex
布局的列表,每个元素执行了一些动画,动画结束后得到了 transform : translate(0,0)
的属性。元素内有个绝对定位的 tip
元素,它应该处于最高的层级,但实际上它会被右边的元素给遮挡:
我们可以用一些简单的代码来还原这种情况:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
ul {
list-style: none;
display: flex;
gap: 20px;
}
li {
width: 100px;
height: 100px;
background-color: red;
transform: translate(0, 0);
}
.tip{
position: absolute;
white-space: nowrap;
background: #000;
z-index: 9999;
}
</style>
</head>
<body>
<ul>
<li>
<div class="tip">Proident eu non aliquip consequat dolore id laboris ut aliquip mollit.</di>
</li>
<li>
<div class="tip"></div>
</li>
</ul>
</body>
<script>
</script>
</html>
这就是我们说的 z-index
“失效”,其实说是“失效”其实不准确,这是因为 transform
元素拥有自身的上下文,其子元素的 z-index
只在这个上下文内有效,而其他元素的 z-index
在另一个层级中,所以看起来好像是 transform
子元素的 z-index
“失效”了。
以上面这个例子举例,tip
的 z-index : 9999
的定位只在第一个 li
内有效,而第一个 li
的层级和第二个 li
是相同的,相同层级下,后一个 li
的展示优先级更高,因此最终导致了最终的展示层级变为:第二个 li
> 第一个 li
(包括其中的 tip).
要解决这个问题有两种办法:
- 最直接的,当动画结束后,图形回到原位时,此时设置
translate(0,0)
是一件多余的事情,去掉transform
属性即可。 - 给
transform
元素设置translateZ
属性,给其父元素设置transform-style: preserve-3d;
属性,将其转化为 3d 形式,此时absolute
定位的子元素会位于最高的层级上。(笔者没有找到原因,如果有知道的请在评论区告诉我,谢谢)
<style>
ul {
...
transform-style: preserve-3d;
}
li {
...
transform: translate(0, 0, 100px);
}
.tip {
...
position: absolute;
}
</style>
</head>
从 layouts
中看可以发现,即使 li
元素高于水平面100px, absolute
的 tip
元素依然位于其层级之上:
总结
transform
拥有自身独立上下文是渲染优化的关键,它可以轻易的与原视图组合重绘而不用花费主线程大量计算时间进行重排。transform
会使子元素z-index
失效的原因是子元素的z-index
位于transform
布局的父容器之内,和其他的元素相互隔离。
参考: