3D变换效果实现

张玥 2026年9月17日 阅读时间 40分钟
3D变换 perspective preserve-3d 立方体 性能优化
CSS高级技巧与最佳实践之3D变换效果实现

在 CSS 的诸多能力里,3D 变换是最容易让人“一眼惊艳”、也最容易让人“一眼翻车”的一项。几行 transform 就能让平面元素立起来、转起来、叠出空间纵深;但同样几行代码,也可能让文字糊成一团、让 z-index 彻底失效、让页面在低端手机上抖成幻灯片。3D 不是“加上去”的效果,而是一整套空间模型。本篇 40 分钟深度长文,从坐标系与透视投影讲起,穿过 perspective、transform-style、backface-visibility,一路走到立方体、翻转卡片、鼠标跟随倾斜、分层视差、3D 环形轮播与性能调优,中间穿插多个可亲手拖拽、亲手调参的交互实验台,帮你把 3D 变换从“抄代码能用”升级到“闭着眼睛也敢写”。

先建立空间感:CSS 的 3D 坐标系

在 2D 的世界里,我们只关心两个方向:X 向右为正,Y 向下为正。而一旦进入 3D,就多出了第三个轴:Z。CSS 的 Z 轴方向很关键——正方向指向观察者(朝屏幕外),负方向指向屏幕深处。

这个细节非常重要,因为它决定了 translateZ(100px) 到底是“往前推”还是“往后退”。在 CSS 中,translateZ(100px) 表示元素向观察者靠近 100px,视觉上会变大;translateZ(-100px) 则表示元素远离观察者,视觉上会缩小。

/* CSS 3D 坐标系速记 */

/* X 轴:水平方向,右为正 */
transform: translateX(100px);

/* Y 轴:垂直方向,下为正(注意与数学坐标相反) */
transform: translateY(100px);

/* Z 轴:垂直屏幕,朝观察者为正 */
transform: translateZ(100px); /* 靠近,变大 */
transform: translateZ(-100px); /* 远离,变小 */

/* 三合一简写 */
transform: translate3d(40px, 20px, 80px);

/* 绕轴旋转(右手法则) */
transform: rotateX(45deg); /* 绕水平轴翻转,像点头 */
transform: rotateY(45deg); /* 绕垂直轴旋转,像摇头 */
transform: rotateZ(45deg); /* 绕屏幕轴旋转,等于 2D rotate */
X 轴 水平向右为正,决定左右
Y 轴 垂直向下为正,决定上下
Z 轴 朝屏幕外为正,决定远近
关键 没有 perspective,Z 轴位移几乎看不出来

下面这个坐标系可以按住拖动,亲手转一转,建立直觉。红色是 X 轴,绿色是 Y 轴,蓝色是 Z 轴,底部网格是 z = 0 平面。

+X
+Y
+Z

按住鼠标(或手指)拖动,从不同角度观察三个坐标轴

为什么必须有透视?

如果你只写 transform: rotateY(45deg),元素看起来只会“变窄”,而不是“转过去”。因为浏览器默认使用的是正交投影——远处的物体和近处的物体一样大,没有近大远小的感觉。

只有加上 perspective,浏览器才会切换到透视投影,让元素呈现出符合人眼习惯的纵深感。这是 3D 与 2D 之间最关键的一道分水岭。

perspective:为 3D 场景架起一台相机

perspective 定义的是观察者眼睛到 z = 0 平面之间的距离,单位是像素。它决定了 3D 场景的“畸变强度”,作用相当于摄影里的焦距:

  • 数值越小(如 200px):相机离得近,透视感强烈,畸变大,画面有冲击力,但容易变形。
  • 数值越大(如 2000px):相机离得远,透视感微弱,画面接近正交投影,更“平”。
  • 常用区间:卡片类 600~1000px,立方体 800~1200px,整页场景 1200~2000px。
/* 写法一:把 perspective 放在父容器上(推荐) */
.scene {
  perspective: 800px;
  perspective-origin: 50% 50%;
}
.card {
  transform: rotateY(45deg);
}

/* 写法二:把 perspective() 写进 transform(只影响自身) */
.card {
  transform: perspective(800px) rotateY(45deg);
}

/* 两者的区别非常大: */
/* 父级 perspective → 多个子元素共享同一个消失点,*/
/* 像同一个场景里的多个物体,空间关系正确。 */
/* transform: perspective() → 每个元素各有一个相机,*/
/* 各自独立,做立方体时会“面飞出去”。 */
父级 perspective 共享消失点,适合多元素 3D 场景
transform 函数 独立透视,适合单元素快速效果
perspective-origin 控制消失点位置,默认 50% 50%
经验值 卡片 800px 起步,先试再调

动手实验:拖动滑块改变 perspective

下面这张卡片固定旋转 rotateY(45deg),唯一变化的是 perspective 的值。仔细体会:数值越小时,卡片的左右边缘被拉伸得越夸张;数值越大时,卡片越接近一个被压扁的平面矩形。

800px perspective

perspective-origin 决定的是“消失点”在舞台上的位置。改成 left 之后,你会看到卡片朝着左前方“收缩”,就像你站在它的左侧看它。这是做出“视角跟随”效果的基础。

一个容易踩的坑:perspective 会创建包含块

给元素加上 perspective 之后,它会成为后代 position: fixed 元素的包含块。也就是说,如果舞台上有一个固定定位的元素,它会被限制在舞台内部,而不是相对视口定位。做全屏 3D 场景时,这一点务必留意。

transform 函数全景:3D 工具箱

CSS 提供的 3D 变换函数并不多,但组合起来威力巨大。下面这张表可以当作速查手册。

函数 作用 参数 典型用途
translate3d(x, y, z) 三轴位移 长度值 空间中的位置移动
translateZ(z) 沿 Z 轴位移 长度值 分层、景深、视差
rotate3d(x, y, z, a) 绕任意向量旋转 向量 + 角度 斜轴旋转、自定义转轴
rotateX(a) 绕水平轴旋转 角度 上下翻转、卡片俯仰
rotateY(a) 绕垂直轴旋转 角度 左右翻转、环形排布
rotateZ(a) 绕屏幕轴旋转 角度 等价 2D 旋转
scale3d(sx, sy, sz) 三轴缩放 数字 整体缩放(sz 影响很小)
matrix3d(…) 4×4 变换矩阵 16 个数字 JS 计算、复杂组合、动画插值

顺序决定一切

多个变换函数是从右往左依次应用的,等价于矩阵连乘。顺序不同,结果完全不同:

/* A:先位移,再旋转 —— 元素被“推远”后绕自身轴转 */
transform: rotateY(45deg) translateZ(100px);

/* B:先旋转,再位移 —— 位移方向也跟着旋转了 */
transform: translateZ(100px) rotateY(45deg);

/* 两者视觉结果完全不同: */
/* A 的结果是元素沿旋转后的 Z 轴方向移动,会跑到侧面;*/
/* B 的结果是元素先平移再自转,位置更可控。*/

/* 排列卡片环时的经典写法: */
transform: rotateY(90deg) translateZ(240px);
/* 先转到对应角度,再沿旋转后的 Z 轴推出去 —— 正好围成圆环 */

动手实验:3D 变换调参台

下面把 rotateX、rotateY、rotateZ、translateZ、scale 与 perspective 全部暴露成滑块。拖动任意一项,方块立刻响应,下方同步生成对应的 CSS 代码。

3D

观察要点:

  • 把 rotateX 调到 ±90° 附近,方块的“厚度”会消失,因为它被压成了一条线;
  • translateZ 调大时方块会变大——这并不是 scale,而是真的“靠近了相机”;
  • perspective 很小时,translateZ 的变化会被极度放大,甚至让元素“撞到镜头”消失;
  • 旋转顺序固定为 X → Y → Z,所以调参数值时要注意顺序带来的差异。

transform-style:让子元素留在同一个空间里

这是 3D 变换里最容易被忽略、也最关键的一个属性。默认情况下,一个元素的 transform-style 是 flat——意思是:它的所有子元素最终都会被压扁到它自己的平面上。无论子元素写了多少 translateZ,看起来都只是简单的缩放。

只有把父元素设为 transform-style: preserve-3d,子元素才能保留自己在 3D 空间中的位置,真正实现“面与面立起来组成一个立方体”。

/* ❌ 默认 flat:子元素的 3D 被压平 */
.cube {
  transform-style: flat;
}

/* ✅ preserve-3d:子元素保留在 3D 空间 */
.cube {
  transform-style: preserve-3d;
}

/* 完整的三层结构: */
/* 第一层:提供透视 */
.scene {
  perspective: 900px;
}

/* 第二层:保留 3D 空间并整体旋转 */
.cube {
  transform-style: preserve-3d;
  transform: rotateX(-20deg) rotateY(30deg);
}

/* 第三层:六个面各自定位 */
.face {
  position: absolute;
  inset: 0;
}
.face-front  { transform: translateZ(60px); }
.face-right  { transform: rotateY(90deg) translateZ(60px); }
.face-back   { transform: rotateY(180deg) translateZ(60px); }
.face-left   { transform: rotateY(-90deg) translateZ(60px); }
.face-top    { transform: rotateX(90deg) translateZ(60px); }
.face-bottom { transform: rotateX(-90deg) translateZ(60px); }
flat(默认) 子元素的 Z 位移被压扁成缩放
preserve-3d 子元素保留真实的空间位置
必须逐层设置 中间任何一层是 flat,3D 都会断掉
常见错误 只在最外层写,忘了旋转层也要写

动手实验:切换 preserve-3d

下面有三张卡片,分别位于 translateZ(-70px)、0、70px,父元素整体旋转了 42 度。点击按钮切换 transform-style,观察它们的空间关系如何瞬间“塌陷”。

Z: -70px
Z: 0
Z: +70px

你会清楚地看到:preserve-3d 时三张卡片在空间中前后错开,形成真实的纵深;切换成 flat 后,它们被压回同一个平面,只剩下大小差异。

破坏 preserve-3d 的“隐形杀手”

即使你写了 preserve-3d,某些属性也会强制把它“打回” flat,这是很多人百思不得其解的问题:

  • overflow 为 hidden、auto、scroll 时;
  • opacity 小于 1;
  • filter 不为 none;
  • clip-path 不为 none;
  • mask / mask-image 不为 none;
  • mix-blend-mode 不为 normal;
  • isolation: isolate;
  • contain: paint;
  • will-change 声明了上述某些属性。
常见翻车写法
.cube { transform-style: preserve-3d; overflow: hidden; }
加了 overflow: hidden 之后,3D 立刻失效,六个面被压平重叠在一起。
正确做法
.scene { overflow: hidden; }
把裁剪放到更外层的场景容器上,preserve-3d 的层级保持干净。

backface-visibility:背面要不要露出来

每个元素都有正反两面。默认情况下,当元素旋转超过 90 度,你看到的其实是它背面的镜像。backface-visibility: hidden 可以让背面完全不渲染。

这个属性是做翻转卡片的核心:正面和背面两张卡片叠在一起,背面那张预先旋转 180 度。不翻转时正面可见、背面被隐藏;翻转后正好相反。

/* 翻转卡片完整实现 */
.flip-scene {
  perspective: 1000px;
}

.flip-inner {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform 0.7s cubic-bezier(0.34, 1.3, 0.64, 1);
}

.flip-scene.flipped .flip-inner {
  transform: rotateY(180deg);
}

.flip-face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
}

.flip-face.back {
  transform: rotateY(180deg);
}
visible(默认) 背面会以镜像方式显示出来
hidden 背面完全不渲染,节省绘制开销
配合使用 必须配合 preserve-3d 才有效果
性能小贴士 不需要看到背面时,hidden 更省

动手实验:点击翻转

正面 · Front
背面 · Back

点击卡片翻转,再点一次翻回来

注意翻转的缓动:这里用了 cubic-bezier(0.34, 1.3, 0.64, 1),第二个控制点大于 1,所以卡片在翻到位之前会轻微“过冲”一下,手感更像真实物体。

实战一:纯 CSS 3D 立方体

立方体是检验 3D 理解是否到位的试金石。它需要你把前面所有知识串起来:透视、preserve-3d、六个面的旋转与位移。

核心思路

  1. 场景容器设置 perspective;
  2. 立方体容器设置 transform-style: preserve-3d,尺寸为 120×120;
  3. 六个面绝对定位铺满容器,各自先旋转到朝向正确方向,再沿旋转后的 Z 轴推出半个边长(60px);
  4. 立方体容器整体旋转,就能看到立体效果。
/* 六个面的推导逻辑(边长 120px,半边长 60px) */

/* 前面:不需要旋转,直接往前推 */
.f-front  { transform: translateZ(60px); }

/* 后面:先转 180° 让它背对镜头,再推出去 */
.f-back   { transform: rotateY(180deg) translateZ(60px); }

/* 右面:先转 90°,原来的“右侧”就朝向了前方 */
.f-right  { transform: rotateY(90deg) translateZ(60px); }

/* 左面:反方向转 90° */
.f-left   { transform: rotateY(-90deg) translateZ(60px); }

/* 上面:绕 X 轴转 90° */
.f-top    { transform: rotateX(90deg) translateZ(60px); }

/* 下面:绕 X 轴反方向转 90° */
.f-bottom { transform: rotateX(-90deg) translateZ(60px); }

/* 自动旋转 */
@keyframes spin {
  from { transform: rotateX(-20deg) rotateY(0deg); }
  to   { transform: rotateX(-20deg) rotateY(360deg); }
}
.cube { animation: spin 12s linear infinite; }

下面这个立方体由 JS 驱动,按住拖动可以自由旋转,松手后会继续自动缓慢旋转。

前
后
右
左
上
下

按住拖动旋转立方体,松手后自动恢复旋转

立方体的三个易错点

  • 忘记给立方体容器设 preserve-3d:六个面会全部重叠在同一个平面上。
  • 用了 transform: perspective() 而不是父级 perspective:每个面各自透视,立方体会“炸开”。
  • 面的尺寸与 translateZ 不匹配:半边长应该是边长的一半,否则会露出缝隙或互相穿插。

实战二:鼠标跟随的 3D 倾斜卡片

这是现代落地页里最常见的 3D 效果之一:鼠标移到卡片上,卡片会根据指针位置朝不同方向轻微倾斜,仿佛能“摸到”它。

数学原理

假设鼠标在卡片内的相对坐标为 (px, py),取值范围 0~1:

  • rotateY 与水平位置相关:(px - 0.5) * 2 * maxAngle
  • rotateX 与垂直位置相反相关:-(py - 0.5) * 2 * maxAngle
  • 最大倾角一般取 8°~15°,超过 20° 就会显得夸张且容易露出背面。
/* JS:跟随鼠标计算倾角 */
card.addEventListener('pointermove', (e) => {
  const rect = card.getBoundingClientRect();
  const px = (e.clientX - rect.left) / rect.width;  /* 0 ~ 1 */
  const py = (e.clientY - rect.top) / rect.height; /* 0 ~ 1 */

  const max = 12;
  const ry = (px - 0.5) * 2 * max;
  const rx = -(py - 0.5) * 2 * max;

  card.style.transform =
    `rotateX(${rx}deg) rotateY(${ry}deg)`;
});

/* 离开时复位 */
card.addEventListener('pointerleave', () => {
  card.style.transform = 'rotateX(0) rotateY(0)';
});

/* CSS:内容层沿 Z 轴抬起,制造悬浮感 */
.tilt-inner {
  transform: translateZ(46px);
}

动手实验:把鼠标移到卡片上

3D Tilt Card

鼠标在卡片上移动,感受倾角与高光

三个让效果更高级的细节

  • 内容层抬高:给内部文字加 translateZ(40~60px),倾斜时文字会有明显的悬浮感,而不是贴在卡片上。
  • 高光跟随:用 radial-gradient 加 --mx / --my 变量,让高光跟着指针移动,金属质感立现。
  • 过渡要分两段:移动时 transition: transform 0.1s 保证跟手,离开时改成 0.4s 让它缓缓复位。

性能上,这个效果只改 transform,全程走合成,几乎不占主线程。唯一需要注意的是:不要在 pointermove 里读取会引起重排的属性(如 offsetWidth),必要时用 getBoundingClientRect() 并缓存结果。

实战三:分层视差与景深

把不同元素放在不同的 translateZ 上,再整体轻微旋转,就能得到天然的视差效果——近处的层移动快,远处的层移动慢。这是“景深”的视觉本质。

/* 四层结构,Z 值依次递增 */
.parallax-space {
  transform-style: preserve-3d;
  transform: rotateX(var(--rx)) rotateY(var(--ry));
}

.p1 { transform: translate(-50%, -50%) translateZ(-90px); }
.p2 { transform: translate(-50%, -50%) translateZ(0px); }
.p3 { transform: translate(-50%, -50%) translateZ(90px); }
.p4 { transform: translate(-50%, -50%) translateZ(180px); }

/* JS:鼠标位置映射到 ±10° 的旋转范围 */
scene.addEventListener('pointermove', (e) => {
  const r = scene.getBoundingClientRect();
  const x = (e.clientX - r.left) / r.width - 0.5;
  const y = (e.clientY - r.top) / r.height - 0.5;
  space.style.transform =
    `rotateX(${-y * 12}deg) rotateY(${x * 16}deg)`;
});

/* 用 CSS 变量更优雅 */
space.style.setProperty('--ry', (x * 16) + 'deg');
Z = -90px 最远层,移动最慢,感觉最深
Z = 0 参考平面,不放大也不缩小
Z = +90px 近层,移动明显加快
Z = +180px 最近层,移动最快,注意别太夸张

动手实验:把鼠标移到下面的场景里

Z: -90px
Z: 0
Z: +90px
Z: +180px

分层视差的关键在于克制:Z 值跨度不要太大(一般不超过 ±200px),旋转角度不要超过 ±15°。过度的视差会让用户产生眩晕感,尤其在移动端。

另外注意:移动端没有鼠标,这类效果应该退化为陀螺仪驱动或干脆关闭。这也是后面要讲的 prefers-reduced-motion 的适用范围之一。

实战四:3D 环形轮播

环形排布的核心只有一行公式:先 rotateY 到对应角度,再沿旋转后的 Z 轴推出去。N 个元素均匀分布时,每个元素的角度是 360 / N 度。

/* 8 个元素,每个间隔 45° */
.carousel {
  transform-style: preserve-3d;
  animation: ringSpin 26s linear infinite;
}

.carousel-item {
  position: absolute;
  inset: 0;
  /* --i 由 JS 或内联样式提供:0, 1, 2, ... 7 */
  transform: rotateY(calc(var(--i) * 45deg)) translateZ(240px);
  backface-visibility: hidden;
}

@keyframes ringSpin {
  from { transform: rotateY(0deg); }
  to   { transform: rotateY(360deg); }
}

/* 半径公式:为了不重叠,translateZ 的距离应满足 */
/* 2 × R × sin(180° / N) ≥ 卡片宽度 */
/* N = 8、宽 140px 时,R ≥ 183px,取 240px 比较宽松 */

动手实验:把鼠标移到轮播上暂停

让轮播更实用的几个改进

  • 鼠标悬停暂停:.carousel:hover { animation-play-state: paused; },方便用户看清内容。
  • 手动切换:用 JS 累加角度而非依赖 CSS 动画,这样可以实现“上一张 / 下一张”。
  • 控制背面渲染:加上 backface-visibility: hidden,转过去的卡片不再绘制,节省开销。
  • 降低元素数量:8 个已经不少了,12 个以上在移动端就要谨慎。

3D 变换的性能真相

好消息是:transform 相关的属性(包括所有 3D 变换)只触发合成,不触发布局与绘制,这是它最大的性能优势。坏消息是:3D 变换往往会创建合成层,而合成层是要吃显存的。

/* 3D 变换触发的流水线阶段 */
Style → Composite

/* 跳过 Layout 与 Paint,这是巨大的优势 */
/* 但代价是: */
/* 1. 元素会被提升为合成层 */
/* 2. 图层需要 GPU 显存 */
/* 3. 图层数量过多时,合成本身也会变慢 */

/* 显存粗略估算:宽 × 高 × 4 字节 */
/* 300 × 200 的卡片 ≈ 240 KB */
/* 100 张这样的卡片 ≈ 24 MB —— 还可接受 */
/* 但如果每张都 1200 × 800,就会直接爆掉 */

/* ✅ 推荐:只对真正参与 3D 的元素开启 */
.cube, .flip-inner, .tilt-card {
  transform-style: preserve-3d;
  will-change: transform;
}

/* ✅ 动画结束后释放 */
el.addEventListener('transitionend', () => {
  el.style.willChange = 'auto';
});

/* ❌ 危险:给长列表的每一项都加 3D 变换 */
.list-item { transform: translateZ(0); } /* × 500 项 */
优势
零布局重绘

不触发 Layout / Paint,主线程负担极小

代价
显存占用

图层越大越多,GPU 压力越高

建议
按需开启

只给真正 3D 的元素用

检查
Layers 面板

查看图层数量与内存占用

动手实验:布局属性 vs 3D 合成变换

下面两条跑道同时播放“从左到右”的动画。上面那条动画 left(触发布局),下面那条动画 transform: translate3d + rotateY(只走合成)。点击开始,观察右上角的实时帧率。

① animation: left 0.6s → 2.4s 触发 Layout + Paint + Composite
② animation: translate3d + rotateY 仅触发 Composite

关于 will-change 与 3D

很多教程会建议“做 3D 动画前加 will-change: transform”。这在大多数情况下是对的,但有三个前提:

  • 只加在真正会动的元素上,不要加在容器或通配上;
  • 动画开始前加,结束后移除,避免长期占用显存;
  • 不要同时声明多个属性,will-change: transform, opacity, filter 会让浏览器为每种可能性都预留资源。

另一个更现代的替代方案是 contain: layout paint:它不创建合成层,但能把布局与绘制的范围锁死在元素内部,对列表项这类重复组件收益明显。

3D 变换的六个经典陷阱

陷阱一:文字模糊

3D 变换后的文字会被 GPU 当作纹理处理,如果变换过程中像素没有对齐,就会出现发虚。常见解法:

/* 让 GPU 使用更清晰的采样方式 */
.card {
  backface-visibility: hidden;
  transform: translateZ(0);
  -webkit-font-smoothing: antialiased;
}

/* 尽量让旋转角度落在整数度上,减少采样偏移 */
/* 如果文字很小,考虑用 SVG 或位图替代 */

陷阱二:z-index 失效

开启 3D 变换后,元素的层叠顺序不再由 z-index 决定,而是由它在 3D 空间中的 Z 位置决定。距离观察者更近的元素永远在上面。这既是能力也是坑:原本靠 z-index 堆叠的下拉菜单、提示框,可能因为 Z 位置不同而跑到后面去。

陷阱三:父级 overflow 裁掉了 3D 元素

Z 值为正的元素在视觉上会“变大”,可能超出容器边界。如果父容器有 overflow: hidden,超出部分会被裁掉——更糟的是,它还会破坏 preserve-3d。建议把裁剪放在更外层,或者干脆不用裁剪。

陷阱四:闪烁与抖动

在部分 Android 浏览器上,3D 元素在动画过程中会出现闪烁。通常是因为两个面在同一个 Z 平面上产生了深度冲突(Z-fighting)。解法:

  • 让相邻面之间有明确的 Z 间距,不要完全重合;
  • 给元素加上 backface-visibility: hidden;
  • 避免在动画过程中改变 transform-style。

陷阱五:3D 变换创建了新的包含块

transform 不为 none 的元素会成为后代 position: fixed 的包含块。这意味着弹窗、悬浮按钮如果被放在 3D 舞台内部,它们的固定定位会失效。正确做法是把这类元素移到舞台外面。

陷阱六:移动端过度使用

移动端 GPU 性能差异极大,且设备发热会触发降频。中低端机型上,多个大尺寸合成层同时动画,很容易掉到 30fps 以下。建议:

  • 移动端减少同时进行 3D 动画的元素数量;
  • 降低图层尺寸,避免全屏大图层;
  • 用媒体查询针对小屏降级为 2D 变换。
不推荐
.card:hover { transform: rotateY(60deg) scale(1.3); filter: blur(2px); }
旋转、缩放、滤镜同时上,还会破坏 preserve-3d,在低端机上必卡。
推荐
.card:hover { transform: rotateY(8deg); }
角度克制、只动 transform,既有效果又有性能。

用 DevTools 调试 3D 场景

3D 效果光看代码很难判断问题出在哪一层。Chrome DevTools 提供了一套非常直观的 3D 调试工具。

/* 调试 3D 的推荐流程 */

/* Step 1:打开 Elements 面板的 3D 视图 */
/* 点击工具栏中的立方体图标,进入 3D View */
/* 可以旋转整个 DOM 树,直观看到图层堆叠关系 */

/* Step 2:用 Composited Layers 查看分层 */
/* 每个合成层都会显示为一张独立的“卡片” */
/* 如果层数异常多,说明需要减少 3D 元素数量 */

/* Step 3:Layers 面板查看内存 */
/* 每个图层都标注了内存占用,注意总量 */

/* Step 4:Rendering 面板辅助 */
/* ✅ Paint flashing —— 检查是否有意外重绘 */
/* ✅ Layer borders —— 显示图层边界 */
/* ✅ FPS meter —— 实时帧率 */

/* Step 5:Performance 录制 */
/* 观察合成线程的耗时,确认是否落在预算内 */
3D View 旋转 DOM 树,看清图层堆叠
Layers 查看每个合成层的内存占用
Paint flashing 绿色闪烁 = 正在重绘
Performance 帧级时间线,定位长任务

常见症状与诊断表

  • 3D 效果完全看不出来 → 检查父级有没有 perspective,中间层有没有 preserve-3d。
  • 立方体“炸开”成六个飞散的方块 → 用了 transform: perspective() 而不是父级透视。
  • 层级关系不对 → 检查是否有 overflow: hidden、filter、opacity < 1 破坏了 3D 空间。
  • 动画卡顿 → 打开 Layers 面板看图层数量,超过 30 个就要警惕。
  • 低端机闪退 → 显存不足,减少大尺寸合成层。

无障碍:3D 效果也需要“安全出口”

3D 效果往往伴随着大幅度位移、旋转与缩放,这对前庭功能障碍、眩晕症用户非常不友好。操作系统提供“减少动态效果”开关,CSS 通过 prefers-reduced-motion 读取它。

/* 基础方案:关闭所有 3D 变换与动画 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* 更优雅:保留淡入淡出,去掉旋转与位移 */
@media (prefers-reduced-motion: reduce) {
  .cube, .carousel, .tilt-card {
    animation: none !important;
    transform: none !important;
  }
  .flip-inner {
    /* 翻转改为淡入淡出,不做旋转 */
    transition: opacity 0.2s linear;
  }
}

/* JS 侧同步读取偏好 */
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
if (mq.matches) {
  /* 跳过鼠标跟随倾斜、视差等 JS 驱动效果 */
  skipTiltEffect();
}
必须关闭 自动旋转、视差、大幅度位移
谨慎保留 小角度倾斜、短距离缩放
可以保留 淡入淡出、颜色变化、状态反馈
原则 降低“空间运动感”,而不是让页面失去反馈

除了动效偏好,3D 效果还要注意信息可访问性:如果某段内容只在立方体的某个面上,屏幕阅读器用户可能无法感知。关键信息应该始终在 DOM 中以可读形式存在,3D 只是它的视觉呈现方式。

兼容性与渐进增强

3D 变换的基础能力支持度非常好,但一些新特性仍需注意退化。

/* 兼容性速查 */
/* transform: translate3d / rotate3d —— 全平台 */
/* perspective / perspective-origin —— 全平台 */
/* transform-style: preserve-3d —— 全平台 */
/* backface-visibility —— 全平台 */
/* transform-box —— 现代浏览器 */
/* contain / content-visibility —— 较新 */
/* @supports 特性检测 —— 全平台 */

/* 渐进增强:先保证 2D 可用,再叠加 3D */
.card {
  /* 基础状态:2D 变换,所有浏览器都正常 */
  transition: transform 0.35s ease;
}
.card:hover {
  transform: translateY(-6px);
}

/* 支持 3D 时才升级为立体效果 */
@supports (transform-style: preserve-3d) {
  .scene {
    perspective: 900px;
  }
  .card:hover {
    transform: rotateY(10deg) translateZ(20px);
  }
}

/* JS 特性检测 */
const has3d = CSS.supports('transform-style', 'preserve-3d');
if (has3d) { init3DCube(); } else { showStaticPoster(); }
基础 3D
99%+

translate3d / rotate3d / preserve-3d

透视相关
99%+

perspective / perspective-origin

性能隔离
95%+

will-change / contain

滚动驱动 3D
70%+

animation-timeline,需退化

实践建议:3D 效果应当被视为“增强层”。页面在没有任何 3D 支持的环境下,依然要能正常阅读与操作。做到了这一点,你就可以放心大胆地用 3D 去提升体验。

3D 变换最佳实践清单

把全文的结论浓缩成一份可以贴在工位上的清单。

  • 三层结构要清晰:外层 perspective,中层 preserve-3d,内层写具体变换。
  • perspective 放在父级,不要用 transform: perspective() 去搭多元素场景。
  • preserve-3d 必须逐层设置,中间任何一层断了,3D 就没了。
  • 警惕破坏 3D 的属性:overflow、filter、opacity、clip-path 等。
  • 旋转角度要克制:卡片类效果一般不超过 15°,超过就容易露出背面。
  • 善用 backface-visibility:不需要看到背面时设为 hidden,既正确又省性能。
  • Z 值跨度不要太大:视差层一般控制在 ±200px 内,避免眩晕。
  • 环形排布用 rotateY + translateZ,顺序不能反。
  • 只动画 transform 与 opacity,绝不动画 left、width、top。
  • 3D 元素数量要控住:一屏内同时动画的合成层不超过 20~30 个。
  • will-change 按需添加、及时释放,不要常驻、不要批量声明。
  • 移动端主动降级:小屏减少 3D 元素、降低图层尺寸。
  • 尊重 prefers-reduced-motion:关闭旋转与视差,保留淡入淡出。
  • 用 DevTools 的 3D View 与 Layers 面板验证,而不是凭感觉。
  • 在真机上测试,尤其是中低端安卓设备。
/* 一套可直接复用的 3D 基线 */

/* 1. 全局 3D 变量 */
:root {
  --persp-card: 900px;
  --persp-cube: 1000px;
  --persp-scene: 1600px;
  --tilt-max: 12deg;
  --flip-dur: 0.7s;
  --ease-spring: cubic-bezier(0.34, 1.3, 0.64, 1);
}

/* 2. 标准 3D 场景骨架 */
.scene-3d {
  perspective: var(--persp-card);
  perspective-origin: 50% 50%;
}
.space-3d {
  transform-style: preserve-3d;
  transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transition: transform 0.2s linear;
}

/* 3. 翻转卡片 */
.flip-inner {
  transform-style: preserve-3d;
  transition: transform var(--flip-dur) var(--ease-spring);
}
.flip-face {
  backface-visibility: hidden;
}

/* 4. 局部隔离,限制重绘范围 */
.card-3d, .cube, .carousel-item {
  contain: layout paint;
}

/* 5. 无障碍降级 */
@media (prefers-reduced-motion: reduce) {
  .space-3d, .cube, .carousel {
    transform: none !important;
    animation: none !important;
  }
  .flip-inner {
    transition: opacity 0.2s linear;
  }
}

3D 变换的魅力,在于它把“平面网页”变成了“可以触摸的空间”。但它同时也是一把双刃剑:用得克制,它是画龙点睛;用得泛滥,它就是性能灾难。当你能够在写下每一行 transform 之前,清楚地知道它会创建几个图层、走哪几个流水线阶段、在低端机上表现如何时,你才算真正掌握了它。

建议把上面的实验台反复玩几遍:拖一拖坐标系、拉一拉 perspective 滑块、转一转立方体、开一开 preserve-3d 的开关。空间感这件事,永远是“动手”比“读书”学得快。