CSS 2D/3D 变换
为什么 transform 是 CSS 动画的黄金属性
在前端性能优化领域,有一个广泛认可的原则:动画应该只改变 transform 和 opacity。这个建议的背后原因是这两个属性的变化不会触发布局重计算,只在合成阶段处理,因此性能极高。
transform 是 CSS 中最强大的属性之一。它不仅能做位移、旋转、缩放、倾斜,还能创建 3D 效果。但 3D 变换也是最容易被误解和误用的 CSS 特性之一。transform-style: preserve-3d 和 flat 的区别、perspective 的作用位置、backface-visibility 的实际行为——这些细节决定了你的 3D 效果是炫酷还是翻车。
面试定位:transform 在面试中主要考察对 3D 变换机制的理解。perspective 和 transform-style 的关系、backface-visibility 的实际效果、以及为什么 transform 适合做动画都是常见问题。
2D 变换函数
transform 的基本语法
.box {
transform: translate(100px, 50px); /* 位移 */
transform: scale(1.5, 0.8); /* 缩放 */
transform: rotate(45deg); /* 旋转 */
transform: skew(10deg, -5deg); /* 倾斜 */
/* 组合变换:按顺序执行 */
transform: translate(100px, 50px) rotate(45deg) scale(1.5);
}
translate —— 位移
.box {
transform: translate(100px); /* 仅 X 轴 */
transform: translate(100px, 50px); /* X 和 Y 轴 */
transform: translateX(100px); /* 仅 X 轴 */
transform: translateY(50px); /* 仅 Y 轴 */
transform: translate3d(0, 0, 100px); /* 3D 位移 */
}
百分比位移:相对于元素自身尺寸
.box {
width: 200px;
transform: translateX(50%); /* 向右移动 100px(200px * 50%) */
}
scale —— 缩放
.box {
transform: scale(2); /* X 和 Y 都放大 2 倍 */
transform: scale(2, 1); /* X 放大 2 倍,Y 保持 */
transform: scaleX(0.5); /* X 缩小一半 */
transform: scaleY(1.5); /* Y 放大 1.5 倍 */
transform: scale3d(1, 2, 3); /* 3D 缩放 */
}
/* 负值:镜像翻转 */
.flip {
transform: scaleX(-1); /* 水平镜像 */
}
rotate —— 旋转
.box {
transform: rotate(45deg); /* 顺时针旋转 45 度 */
transform: rotate(-90deg); /* 逆时针旋转 90 度 */
transform: rotateX(45deg); /* 绕 X 轴旋转(3D) */
transform: rotateY(45deg); /* 绕 Y 轴旋转(3D) */
transform: rotateZ(45deg); /* 绕 Z 轴旋转(2D 效果同 rotate) */
transform: rotate3d(1, 1, 1, 45deg); /* 绕自定义轴旋转 */
}
skew —— 倾斜
.box {
transform: skew(10deg); /* X 轴倾斜 10 度 */
transform: skew(10deg, 5deg); /* X 和 Y 轴分别倾斜 */
transform: skewX(10deg); /* 仅 X 轴 */
transform: skewY(10deg); /* 仅 Y 轴 */
}
3D 变换函数
perspective —— 透视效果
perspective 定义观察者与 z=0 平面的距离,产生近大远小的透视效果:
.scene {
perspective: 500px; /* 观察者距离屏幕 500px */
}
.box {
transform: translateZ(100px); /* 物体移近 100px,看起来更大 */
}
透视消失点:perspective-origin 定义消失点的位置:
.scene {
perspective: 500px;
perspective-origin: center center; /* 默认:中心 */
perspective-origin: top left; /* 左上角 */
perspective-origin: 100% 100%; /* 右下角 */
}
transform-style —— 3D 空间模式
/* preserve-3d:子元素保持 3D 空间位置 */
.parent-3d {
transform-style: preserve-3d;
}
/* flat:子元素被扁平化到父元素平面 */
.parent-flat {
transform-style: flat;
}
实际效果对比:
.scene {
perspective: 500px;
}
.card {
transform: rotateY(45deg);
}
.card-front,
.card-back {
position: absolute;
backface-visibility: hidden;
}
.card-back {
transform: rotateY(180deg); /* 初始时翻转朝后 */
}
/* 如果父元素没有 preserve-3d:
* card-front 和 card-back 会被扁平化到同一平面
* rotateY(180deg) 的背面会显示在正面"里面" */
.scene-3d {
transform-style: preserve-3d;
}
/* 有了 preserve-3d:子元素在 3D 空间中正确排列 */
backface-visibility —— 背面可见性
.card {
backface-visibility: visible; /* 背面可见 */
backface-visibility: hidden; /* 背面隐藏(常用) */
}
/* 3D 翻转卡片 */
.flip-card {
position: relative;
transform-style: preserve-3d;
}
.flip-card-front,
.flip-card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
}
.flip-card-front {
/* 正面 */
}
.flip-card-back {
transform: rotateY(180deg); /* 初始时翻转朝后 */
}
.flip-card:hover {
transform: rotateY(180deg); /* hover 时翻转 */
}
变换的顺序与原点
transform-origin —— 变换原点
默认变换原点是元素中心,但可以自定义:
.box {
transform-origin: center center; /* 默认:中心 */
transform-origin: top left; /* 左上角 */
transform-origin: 50px 50px; /* 具体坐标 */
transform-origin: 100% 100%; /* 右下角 */
transform-origin: bottom center; /* 底部中心 */
}
/* 围绕右下角旋转 */
.rotate-corner {
transform-origin: right bottom;
transform: rotate(45deg);
}
变换顺序的影响
/* 顺序不同,结果不同 */
.box1 {
transform: translate(100px, 0) rotate(90deg);
/* 先向右移动 100px,再绕新位置的中心旋转 */
}
.box2 {
transform: rotate(90deg) translate(100px, 0);
/* 先旋转,再向当前 right 方向(原来的下方)移动 100px */
}
will-change 与性能
will-change 的作用
will-change 提示浏览器提前为某些属性变化做准备:
.box {
will-change: transform;
/* 浏览器会:
* 1. 为这个元素创建一个新的层叠上下文
* 2. 将这个元素的渲染提升到新的 Layer
* 3. 提前准备好变换矩阵计算
*/
}
滥用 will-change 的问题
/* 滥用示例 */
.bad {
will-change: transform, opacity, left, top, width, height;
/* 问题:
* 1. 创建太多 Layer,消耗 GPU 内存
* 2. 每个 Layer 都需要独立管理
* 3. 可能导致层爆炸(Layer explosion)
*/
}
正确使用模式
/* 模式一:动画开始前声明 */
.animated-box {
will-change: transform;
}
.animated-box:hover {
transform: scale(1.1);
}
/* 模式二:JavaScript 控制的动画 */
element.style.willChange = 'transform';
// 动画开始
element.style.transform = 'translateX(100px)';
// 动画结束
element.addEventListener('transitionend', () => {
element.style.willChange = 'auto';
});
实战场景
场景一:3D 翻转卡片
.flip-card {
width: 200px;
height: 260px;
position: relative;
transform-style: preserve-3d;
transition: transform 0.6s;
cursor: pointer;
}
.flip-card-front,
.flip-card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.flip-card-front {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.flip-card-back {
background: linear-gradient(135deg, #f093fb, #f5576c);
color: white;
transform: rotateY(180deg);
}
.flip-card:hover {
transform: rotateY(180deg);
}
场景二:透视卡片堆叠
.card-stack {
perspective: 1000px;
}
.card {
transform-style: preserve-3d;
transition: transform 0.3s ease;
}
.card:nth-child(1) { transform: translateZ(0); }
.card:nth-child(2) { transform: translateZ(-20px); }
.card:nth-child(3) { transform: translateZ(-40px); }
.card-stack:hover .card:nth-child(1) { transform: translateZ(20px) rotateY(10deg); }
.card-stack:hover .card:nth-child(2) { transform: translateZ(0) rotateY(5deg); }
.card-stack:hover .card:nth-child(3) { transform: translateZ(-20px); }
场景三:按钮点击效果
.btn-press {
transition: transform 100ms ease;
will-change: transform;
}
.btn-press:active {
transform: scale(0.95);
/* 性能好,因为只触发 Composite */
}
面试高频问题
Q: transform-style: preserve-3d 和 flat 有什么区别?
回答要点:preserve-3d 保持子元素的 3D 位置,子元素可以出现在父元素的"前面"或"后面"。flat 将子元素扁平化到父元素所在平面,子元素的 3D 变换会被投影到 2D 平面。对于需要多层叠加的 3D 效果,必须使用 preserve-3d。
Q: perspective 应该设置在哪个元素上?
回答要点:perspective 应该设置在包含 3D 变换子元素的父容器上,而不是直接应用 3D 变换的元素上。perspective 创造的是一个"观察者视角",决定了子元素 3D 变换的透视效果。
Q: 为什么说 transform 和 opacity 适合做动画?
回答要点:因为这两个属性的变化只触发 Composite(合成)阶段,不触发布局(Layout)和绘制(Paint)阶段。浏览器可以将这些动画提升到独立的 Layer(层),使用 GPU 进行合成,效率极高。
Q: backface-visibility: hidden 的实际行为是什么?
回答要点:当元素背面朝向观察者时(通过 rotateY 180deg 或 rotateX 180deg 等),该面不可见。这常用于 3D 翻转卡片,正面和背面各占一面,背面初始时翻转朝后,hover 时翻转朝前。