资讯动态

纯CSS 3D实现星空隧道Loading动画,不依赖Three.js的轻量方案

发布时间:2026/9/18 21:52:11 来源:尧图企业网站定制
之前做一个小产品的时候上线前一天后台接口突然变得很慢首页要等好几秒才出数据。那个点临时去引 Three.js 做加载动画显然不现实CDN 一抽风反而更糟。我翻了翻手头常用的几个动画库要么风格太跳要么为了一个 Loading 引入一堆依赖最后干脆自己写。折腾了一晚上用纯 CSS 3D 做了一个能看的星空隧道 Loading全程没有请 Three.js 过来帮忙。今天把这个思路和踩坑过程整理出来给需要在项目里快速塞一个轻量加载动画的同学做个参考。这个效果适合谁适合那种不想为了一个加载动画增加依赖、又希望页面看起来有点质感的场景。它不依赖任何第三方库一个 CSS 文件加几行结构就能跑对新人理解 CSS 3D 也很有帮助。做完你会发现所谓“3D 效果”不一定非要 WebGLCSS 的 transform 和 perspective 组合起来能折腾出不错的东西。1. 先想清楚隧道 Loading 的“3D 感”到底从哪来1.1 近大远小不是特效是透视在起作用很多人一开始做 CSS 3D 容易懵是因为把“3D 效果”理解成了一种魔法。其实在浏览器里所谓 3D 只是 CSS 帮我们把一个物体经过透视投影算好后画在平面上和你用相机拍一条隧道是同一个道理隧道口近处的圆环大远处的圆环小中间还有层层叠叠的遮挡关系这就产生了纵深感。CSS 里打开这个功能的开关就是perspective。你可以把它理解成一个虚拟相机的焦距数值越小代表镜头越广角近大远小的变形越剧烈数值越大透视越接近人眼的正常视野。我常用的范围是 600px 到 900px。做隧道 Loading 这类有强烈纵深的效果一般用 600px 到 700px透视感明显一点视觉冲击力强。很多新手写 CSS 3D 发现“怎么完全没有立体感”绝大多数情况就是perspective没写或者写在了错误的位置。这个属性不是加到某个元素上就万事大吉它决定的是“哪个容器的子元素们共享同一个观察视角”。1.2 和 Three.js、Canvas 对比纯 CSS 的优势和边界先说结论我不是反对 Three.js它做复杂的 3D 场景是利器但一个 Loading 动画真的犯不上。Three.js 的核心是基于 WebGL要初始化场景、相机、渲染器还要管理动画循环和着色器即使一个简单的星空粒子系统也要写不少代码。而且它本身的体积在那里哪怕用 Tree Shaking 也压不到特别小对首屏加载来说有点奢侈。Canvas 2D 也是可选项但星空隧道的核心是“空间感”你要手动计算每个点的位置、纵深比例、遮挡关系本质是自己实现一个简单 3D 引擎。CSS 3D 就不一样浏览器帮我们处理了投影计算我们只需要告诉它“这个圆环在哪个平面上、离我多远”视觉结果自动就出来了。纯 CSS 的边界也很清楚如果场景里有几十万个粒子、需要动态生成复杂几何体、或者要做精细的材质光照那还是老实上 Three.js。但如果你的需求是“一个好看的 Loading 动画”“几个圆环加星光”“鼠标移入后加速旋转”这类轻交互纯 CSS 不仅够用而且性能和代码可维护性都不错。2. 三个关键 CSS 配置把隧道骨架立起来2.1 perspective给页面装一个“相机”这个属性要放在所有 3D 元素的最近公共父级上。比如我页面里有一个.scene容器所有隧道相关的元素都放在它里面那么perspective就写在.scene上。.scene { width: 360px; height: 360px; perspective: 600px; perspective-origin: 50% 50%; }perspective-origin默认是 50% 50%也就是从容器正中央看出去。如果想让视角有点偏移比如“蹲在隧道口下方往上看”可以调整成50% 30%之类的值。做星空隧道这类对称场景保持默认的居中视角就行改了反而容易让人晕。还有一个小细节有些时候你会看到perspective: none或者perspective: 0那基本就等于关掉了 3D。而且这个属性对元素自身是无效的它影响的是子元素的 3D 呈现记住这一条能少踩很多坑。2.2 preserve-3d让所有环待进同一个三维空间光有透视还不够如果子元素的transform只是自己转自己的它们之间没有形成一个统一的 3D 空间效果就是“每个元素各自扁扁地变个形”。想让所有圆环像真实隧道一样在空间里前后排列、互相遮挡需要在它们共同的父容器上加.tunnel { transform-style: preserve-3d; }这个属性的意思是“保留子元素的 3D 空间位置”默认值是flat也就是把子元素全部拍平到当前平面上。可以这么理解perspective是相机preserve-3d是告诉相机“你不要把物体拍扁把它们在空间里的纵深位置保留下来”。这里有个隐藏的坑如果父容器同时设置了overflow: hidden或者某些 filter、opacity 属性浏览器可能会强制把transform-style降级为flat。我之前就遇到过在隧道容器上加了opacity: 0.99想解决边缘锯齿结果整个 3D 空间瞬间拍平排查了很久才反应过来。2.3 translateZ rotateY环形排列的核心公式隧道看起来是一圈一圈的圆环每一个环在空间里的位置怎么定我的做法是用百分比控制角度然后让每个环围绕同一个中心点排列成一圈。.ring { position: absolute; left: 50%; top: 50%; width: 280px; height: 280px; margin: -140px 0 0 -140px; border: 2px solid rgba(120, 190, 255, 0.7); border-radius: 50%; box-shadow: 0 0 20px rgba(120, 190, 255, 0.3); }这里用left/top加负外边距把每个环的中心点放到容器正中。接下来通过transform: rotateY(角度) translateZ(半径)让圆环绕 Y 轴排列成一个圆圈。rotateY决定环朝向哪个方向translateZ让它离开中心点一段距离。.ring:nth-child(1) { transform: rotateY(0deg) translateZ(140px); } .ring:nth-child(2) { transform: rotateY(45deg) translateZ(140px); } .ring:nth-child(3) { transform: rotateY(90deg) translateZ(140px); } .ring:nth-child(4) { transform: rotateY(135deg) translateZ(140px); } /* 以此类推 */8 个环平均分布就是360 / 8 45deg的间隔。同理 12 个环就是 30deg 间隔。这个公式是整个隧道效果的地基理解它比记代码重要得多。3. 实操拼一个能跑的星空隧道 Loading3.1 HTML 结构比想象中更简单结构上大致是三层外层.scene提供透视中间.tunnel承载 3D 空间里面放一堆.ring圆环再加一组星星背景层和光晕层。div classloading-mask div classscene div classtunnel !-- 圆环和星星层都在这里 -- /div div classglow/div /div /div手写 12 个环也不是不行但太笨了。我一般用一个简单脚本批量生成生成完就不再动了渲染和动画完全交给 CSSconst tunnel document.querySelector(.tunnel); const count 12; for (let i 0; i count; i) { const ring document.createElement(div); ring.className ring; ring.style.transform rotateY(${(360 / count) * i}deg) translateZ(140px); tunnel.appendChild(ring); }这里用了一点 JS但不是为了做动画只是避免重复写 12 段几乎一样的 HTML。动画部分全程走 CSS核心思路仍然是不依赖任何库。3.2 关键帧动画旋转、前进与无限循环隧道动起来我最终选定的方案是“环组旋转 星星背景滚动”。为什么不直接让隧道轴向移动因为隧道是一个个环排成的筒如果整体往一个方向移动移出画面后立刻重置位置循环接缝处会非常明显。旋转就不同环是中心对称的转完一圈完美回到原点天然适合无限循环。.tunnel { transform-style: preserve-3d; animation: tunnelSpin 16s linear infinite; } keyframes tunnelSpin { from { transform: rotateX(0deg) rotateZ(0deg); } to { transform: rotateX(0deg) rotateZ(360deg); } }等等直接让.tunnel旋转的话环是绕着各自圆环中心在转视觉效果可能更像“滚筒”而不是“前进的隧道”。为了让观感更像穿越我会把整个隧道稍微倾斜一点比如先让隧道本身躺在空间里再让视线从一端看进去。这个调整不需要改 JS只需要在 keyframes 里加一个rotateXkeyframes tunnelSpin { from { transform: rotateX(65deg) rotateZ(0deg); } to { transform: rotateX(65deg) rotateZ(360deg); } }rotateX(65deg)的意思是让隧道像水管一样斜躺在屏幕里我们从管道口斜看进去既有纵深感又能看到环圈在转动。这个角度我试过 60 到 75 度感觉 65 度左右最舒服既保留“隧道”那种延伸感又不会因为太正而显得像一只盯着你的眼睛。星星背景层的循环更简单用一个内容足够高的背景层让它沿 Z 轴从远处移动到近处再重置。重置的瞬间因为在视野里位置跨越很大肉眼不会注意到突兀跳动。3.3 星空质感渐变、阴影和闪烁节奏星空隧道如果只靠几根圆环边框还是有点寡淡。我给隧道加了两层“星星”。第一层是散布在隧道内壁的光点第二层是背景里大小不一的星点。CSS 里造星星最简单的方式是radial-gradient生成多个小亮点再叠加一层background-repeat。比如.stars { position: absolute; inset: -20%; background-image: radial-gradient(1px 1px at 20% 30%, rgba(255,255,255,0.9), transparent), radial-gradient(1px 1px at 40% 70%, rgba(180,220,255,0.8), transparent), radial-gradient(1.5px 1.5px at 70% 20%, rgba(255,255,255,0.7), transparent), radial-gradient(1px 1px at 85% 60%, rgba(200,230,255,0.9), transparent); background-size: 240px 240px; animation: starScroll 6s linear infinite; }background-size控制星星稀疏程度200 到 300px 左右比较自然太密会糊成一片噪点。我还会让星星层做缓慢的位移模拟飞船穿行时星点掠过的感觉。注意星星层放在.scene里面、.tunnel外面这样它不受隧道旋转的牵连只做背景滚动。闪烁节奏我用一个独立的opacity动画让光晕层在 0.5 到 0.9 之间呼吸.glow { position: absolute; left: 50%; top: 50%; width: 200px; height: 200px; margin: -100px 0 0 -100px; background: radial-gradient(circle, rgba(100,180,255,0.35), transparent 70%); filter: blur(10px); animation: breathe 2.4s ease-in-out infinite alternate; } keyframes breathe { from { opacity: 0.5; transform: scale(0.95); } to { opacity: 0.9; transform: scale(1.05); } }4. 性能与兼容性别让 Loading 动画反噬页面性能4.1 元素数量和动画分层控制纯 CSS 3D 虽然没有 WebGL 那么猛但如果无节制堆元素照样能让中低端手机变暖手宝。我第一版为了追求密度的星空效果放了几百个星点 DOM 元素结果在旧一点的安卓机上直接掉到 20 帧以内。后来把星点压到 60 个以内背景星星全部改用radial-gradient和background-position实现体感明显顺滑。这里的原则是能在背景图里做的效果就不要用 DOM能用 transform 动画就不要用 top/left。我最终的 DOM 元素数量控制在 20 个左右12 个环、3 层星星背景、1 个光晕、外加几个高光片。这个数量级下CSS 3D 的渲染压力非常小。4.2 will-change 与 GPU 合成的正确打开方式will-change: transform可以提前告诉浏览器“这个元素要做 transform 动画请把它放到单独的合成层”减少动画开始时的卡顿感。但千万别给所有元素都加否则会消耗大量内存。我只给真正持续运动的元素加比如.tunnel、.stars、.glow。.tunnel, .stars, .glow { will-change: transform; }还有一个经验动画结束后如果不再需要合成层最好用 JS 移除will-change或者用 CSSanimation结束后通过事件监听把类名摘掉。Loading 页这种常驻动画倒无所谓但如果做的是页面里的装饰动画一直挂着合成层对低端设备不友好。另外filter: blur()很耗性能能少用就少用。我那个光晕的模糊实际是用一层半透明 radial-gradient 模拟出来的在没有 blur 的情况下效果也差不多但帧率差了一大截。4.3 降级与兼容老浏览器也别直接放弃CSS 3D 这事的兼容性比想象中好现代浏览器基本都支持transform-style: preserve-3d。但如果你要覆盖非常老的浏览器或者公司内部还在用旧内核的 webview就得考虑降级。我的做法是在不支持的时候把 3D 场景隐藏换成一个纯渐变的旋转圆环表示“至少加载中”这个状态还在。判断怎么做一行supports就够supports (transform-style: preserve-3d) { /* 正常 3D 效果 */ } supports not (transform-style: preserve-3d) { /* 简单降级效果 */ }5. 踩坑记录我从这个项目里学到的几件事5.1 动画接缝处为什么突然闪一下我想让星星沿 Z 轴匀速朝镜头移动做了一半发现循环接缝处总是“咔”地闪一下。后来意识到星星层从远到近移动过程中头尾内容必须无缝衔接否则重置位置那一下就很突兀。解决方案是给星星层做两个完全相同的副本前后衔接或者让背景层足够大重置点在视野之外完成。我最后选了后者因为更省心。5.2 环怎么“变扁”了——transform 顺序的坑CSS 里transform的执行顺序是自右向左的这个顺序直接决定结果。rotateY(45deg) translateZ(140px)和translateZ(140px) rotateY(45deg)是完全不同的效果。前者是“先转一圈再推远”环会围绕中心点分布在某个圆环上后者是“先推远再转”每个环会绕着自身中心转画面就乱了。这个问题我自己踩过一次排查时一定要先检查 transform 的写法顺序。5.3 移动端掉帧和黑屏怎么排查移动端出现掉帧优先检查三件事是不是用了大面积filter是不是元素过多是不是整个页面在滚动时触发了重排。黑屏问题多半是 GPU 合成的坑尤其是 Android 内核的 webview可以尝试把受影响的容器加一个transform: translateZ(0)强制开启合成层通常能解决。我还遇到过一个奇怪的现象动画跑一会儿后突然变卡。后来发现是某个元素的box-shadow范围太大阴影部分每帧都在重绘。把阴影改成用半透明边框或渐变模拟之后问题消失。最后再分享一个小技巧如果你也想做类似的 Loading不要急着写代码先在纸上把隧道拆成“几个圆环 几个层次”想清楚哪些元素动、哪些静、运动方向是什么。这个思考过程比上手写代码重要得多思路对了代码只是翻译。做完之后把这个 Loading 放在一个小项目里试用一阵你会发现纯 CSS 3D 能做的比你想象中更多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价