那个让我抓狂的"弹性动画"
2019 年,我接了一个需求:做一个购物车侧边栏,点击"结算"按钮后,侧边栏从右侧滑入,并且要有一个"回弹"效果------滑入时略微超过最终位置,再回到原位,像弹簧一样。
设计师给了我一段 After Effects 动画的缓动曲线图,说:"你照着这个曲线调一下参数就行。"
我用 transition: transform 0.3s ease-out,根本做不出回弹。换了 cubic-bezier(),手动调了半小时,不是弹过头就是没弹性。最后我用了 @keyframes,定义 0%、80%、100% 三个关键帧,模拟出回弹。但这样代码又长又不灵活------回弹幅度是固定的,无法根据拖拽距离动态变化。
后来我读到一篇文章,讲如何用自定义贝塞尔曲线实现真正的物理弹性效果,比如 cubic-bezier(0.68, -0.55, 0.265, 1.55)。一尝试,果然一个 transition 搞定,而且性能比关键帧更好。那一刻我意识到,贝塞尔曲线不只是那几个预设值,它是一扇通往高级动效的大门。
另一个坑是动画卡顿。移动端上,我用 transition 改变 left 和 top 做滑动菜单,总是掉帧,特别是在低端 Android 上。后来知道了硬件加速,改用 transform: translateX(),丝般顺滑。从此,"能用 transform 绝不用 top/left"成了我的铁律。
今天这篇文章,我想把 CSS 过渡(transition)的高级知识------贝塞尔曲线的奥秘、硬件加速的原理与实战------一次讲透。读完你会明白:如何做出"物理感"的动效,如何让动画永远保持 60fps。
第一章:过渡基础回顾------不止是"变化"
1.1 过渡的"三要素"
过渡的核心是在两个状态之间插入中间帧。实现一个平滑的过渡,你需要三个条件:
- 一个可过渡的属性(数值型、颜色等)
- 两个不同的状态 (例如
:hover前后) - 一个过渡时间 (
transition-duration)
css
.box {
width: 100px;
transition: width 0.3s;
}
.box:hover {
width: 200px;
}
1.2 过渡的"四维参数"
简写 transition: property duration timing-function delay;
property:要过渡的属性,可以是all或逗号分隔列表。duration:时间,单位s或ms。timing-function:缓动函数,决定动画速度曲线。delay:延迟时间。
多个属性分别指定:
css
.btn {
transition: background-color 0.2s ease, transform 0.1s linear;
}
1.3 那些"不能过渡"的属性与替代方案
不能过渡的属性比如 display(可以用 visibility + opacity 模拟),position(但 left/top 数值可以过渡),z-index(也只能瞬间变化)。关键在于寻找可替代的数值属性。
第二章:贝塞尔曲线------控制速度的魔法
2.1 标准缓动函数背后的数学
缓动函数描述的是"时间"与"属性值"之间的关系。transition-timing-function 默认有五个关键字:
linear:匀速,直线。ease:慢-快-慢,默认。ease-in:加速。ease-out:减速。ease-in-out:先加速后减速。
这些预设值本质上是三次贝塞尔曲线 的特殊案例。贝塞尔曲线由四个点定义:起点 (0,0),终点 (1,1),以及两个控制点 (x1,y1) 和 (x2,y2)。
cubic-bezier(x1, y1, x2, y2) 允许你自定义中间两个点。x 值必须在 [0,1] 之间,y 可以超出这个范围(实现弹跳效果)。
常见预设值的等价贝塞尔曲线:
linear:cubic-bezier(0,0,1,1)ease:cubic-bezier(0.25,0.1,0.25,1)ease-in:cubic-bezier(0.42,0,1,1)ease-out:cubic-bezier(0,0,0.58,1)ease-in-out:cubic-bezier(0.42,0,0.58,1)
2.2 自定义贝塞尔曲线------做出"弹性"效果
当你想要一个动画结束时"过头"一点再回来(弹性),你需要让曲线在终点附近超过 1。例如 cubic-bezier(0.68, -0.55, 0.265, 1.55)。
解读:
- 起点 (0,0)
- 第一个控制点 x1=0.68, y1=-0.55(负值!,表示一开始会反向运动)
- 第二个控制点 x2=0.265, y2=1.55(超过1,表示结束时超过目标值)
最终效果:元素先稍微向后(负方向),然后快速向前并超过终点,最后回到终点。
这种曲线可用于模拟物理弹簧、滑出回弹等效果。
2.3 实战:制作一个"摇晃"的提示框
css
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.notification {
animation: shake 0.3s ease-in-out;
}
用关键帧实现很方便。但如果只用 transition,比如让按钮在 :active 时缩小并带有弹跳感,可以用贝塞尔曲线:
css
.button:active {
transform: scale(0.9);
transition: transform 0.1s cubic-bezier(0.5, -0.5, 0.5, 1.5);
}
按下时按钮缩小到 0.9,但因为有弹跳曲线,它会稍微缩过头再弹回 0.9,感知上更有"按压感"。
2.4 工具与调试
- Chrome DevTools:在 Elements 面板的 Styles 栏,点击
transition旁边的紫色小方块,可以打开可视化贝塞尔曲线编辑器,拖动控制点实时预览。 - 在线工具:cubic-bezier.com,可以直接设计曲线并复制代码。
- 浏览器的"动效检查器"可以显示动画速率曲线。
2.5 steps()------阶跃函数
除了贝塞尔曲线,还有 steps(number, position) 用于分步动画,比如实现数字时钟、打字机效果。它不是平滑过渡,而是突然跳跃。
css
.counter {
transition: all 1s steps(10, end);
}
2.6 贝塞尔曲线的局限与替代
复杂物理效果(如重力、摩擦力)最好用 JavaScript 库(如 Popmotion 或 GSAP),因为贝塞尔曲线只能描述一条固定的路径,无法根据动态速度响应交互。但绝大多数 UI 动效,贝塞尔足够。
第三章:硬件加速------让动画飞起来
3.1 为什么动画会卡顿?
浏览器的渲染流水线大致为:JavaScript → Style → Layout → Paint → Composite。
- Layout(重排) :计算元素的位置和大小。改变
width、height、margin、left、top等几何属性会触发 Layout,开销最大。 - Paint(重绘) :填充像素,改变
color、background、box-shadow等视觉属性触发 Paint,开销中等。 - Composite(合成) :将多个图层合并到屏幕。只触发 Composite 的属性包括
transform和opacity,开销最小,通常在 GPU 中进行。
所以,动画卡顿的根本原因是频繁触发 Layout 或 Paint,导致主线程繁忙,无法在 16.6ms 内完成一帧。
3.2 硬件加速原理:GPU 来帮忙
GPU(图形处理单元)擅长并行处理图像合成。当元素应用 transform 或 opacity 时,浏览器会将该元素提升为独立的合成层,后续动画只需要在 GPU 中改变该层的矩阵或透明度,无需主线程参与 Layout/Paint。
因此"硬件加速"其实是指利用合成层进行动画,让 GPU 分担计算。
3.3 哪些属性触发合成?
transform(包括translate、scale、rotate、skew)opacityfilter(部分浏览器可能触发合成,但性能不如前两者)
为了强制提升合成层,可以使用 will-change: transform; 或 transform: translateZ(0);(又称"hack")。
3.4 will-change 的正确用法
will-change 提醒浏览器某个元素将要发生某种变化,提前创建合成层。
css
.element {
will-change: transform;
}
但不要滥用!每一个合成层都会占用内存。如果大量元素都有 will-change,内存爆满,反而性能下降。
最佳实践:
- 在交互即将发生时(如
:hover或 JS 添加类),才设置will-change,动画结束后移除。 - 不要静态写在样式里,尤其对于大量元素。
js
element.addEventListener('mouseenter', () => {
element.style.willChange = 'transform';
});
element.addEventListener('transitionend', () => {
element.style.willChange = 'auto';
});
3.5 实战:从卡顿到丝滑
错误示范(卡顿):
css
.box {
transition: left 0.3s;
position: relative;
left: 0;
}
.box:hover {
left: 100px;
}
每次 left 变化都会触发 Layout,卡顿。
正确示范(硬件加速):
css
.box {
transition: transform 0.3s;
transform: translateX(0);
}
.box:hover {
transform: translateX(100px);
}
只有 Composite,流畅。
3.6 隐式合成与层爆炸
有时你明明只给了一个元素加 transform,但浏览器发现它与其他元素重叠,且其他元素有特殊属性(比如 z-index、position),就会强行将那些元素也提升为合成层,导致层爆炸。
避免方式:
- 尽量避免复杂的重叠层级。
- 使用
contain: layout等属性限制范围。 - 在 Chrome DevTools 的 Layers 面板中查看有多少合成层,排查多余层。
第四章:实战案例------高级动效
4.1 案例一:弹性侧边栏菜单
html
<div class="menu">
<ul>...</ul>
</div>
<button class="toggle">打开菜单</button>
css
.menu {
position: fixed;
top: 0;
left: 0;
width: 250px;
height: 100%;
background: #333;
transform: translateX(-100%);
transition: transform 0.4s cubic-bezier(0.34, 1.2, 0.64, 1);
}
.menu.open {
transform: translateX(0);
}
js
document.querySelector('.toggle').addEventListener('click', () => {
document.querySelector('.menu').classList.toggle('open');
});
使用的贝塞尔曲线 cubic-bezier(0.34, 1.2, 0.64, 1) 会略微超过终点再弹回,产生弹性感。同时由于使用 transform,硬件加速。
4.2 案例二:交错动画(stagger)利用 transition-delay
css
.list-item {
transition: all 0.3s ease-out;
transition-delay: calc(var(--index) * 0.05s);
opacity: 0;
transform: translateY(20px);
}
.list-item.show {
opacity: 1;
transform: translateY(0);
}
HTML 中为每个 li 设置 style="--index: 1" 等。transition-delay 基于 CSS 变量计算,实现逐个入场。
4.3 案例三:3D 翻转卡片(硬件加速 + 贝塞尔)
css
.card {
width: 200px;
height: 300px;
transition: transform 0.6s cubic-bezier(0.23, 1, 0.32, 1);
transform-style: preserve-3d;
}
.card:hover {
transform: rotateY(180deg);
}
transform-style: preserve-3d 使子元素在 3D 空间,但注意backface-visibility。
4.4 案例四:通知条自动滑出并消失
css
.notice {
transform: translateY(-100%);
transition: transform 0.3s cubic-bezier(0.5, -0.3, 0.3, 1.2), opacity 0.2s 0.2s;
opacity: 0;
}
.notice.show {
transform: translateY(0);
opacity: 1;
}
配合 JavaScript 添加 show 类,并在几秒后移除。
第五章:性能调试与优化技巧
5.1 Chrome DevTools 的 Performance 面板
录制一段动画,查看火焰图,寻找长任务。如果看到紫色的"Rasterize"或绿色的"Paint",说明触发了绘画;如果是黄色"Layout",则触发了重排。
勾选"FPS meter"实时查看帧率,红色条代表掉帧。
5.2 使用 Layers 面板
点击"更多工具" → "Layers",可以看到页面上的所有合成层。检查是否有意外的层(例如被 z-index 强行提升),减少不必要的层。
5.3 强制 GPU 加速的稳妥做法
transform: translateZ(0); 或 will-change: transform; 都可以将元素提升到合成层。但注意:在低端设备上,过多层会降低性能。最好动态启用,用完即弃。
5.4 避免 transition: all
all 会监听所有属性的变化,可能触发不必要的计算。明确指定要过渡的属性,如 transition: transform 0.3s。
5.5 移动端特别注意事项
- iOS Safari 上,
position: fixed的元素在滚动时可能会抖动,加transform: translateZ(0)可以缓解。 -webkit-overflow-scrolling: touch可提升滚动流畅度。
第六章:未来趋势
6.1 更复杂的缓动函数规范
CSS 缓动函数 Level 2 正在考虑引入 spring() 函数,允许基于物理参数的弹性动画,如 spring(1.2, 80, 12),届时再也不用手动调贝塞尔曲线了。
6.2 视图过渡 + 硬件加速
视图过渡动画已经利用硬件加速,未来可能会更紧密地与过渡属性结合,实现更复杂的效果。
6.3 滚动驱动动画中的缓动
滚动驱动动画(animation-timeline: scroll())同样支持自定义缓动函数,实现非线性滚动联动。
让每一帧都丝滑
从理解贝塞尔曲线的数学意义,到巧妙运用 cubic-bezier 制造弹性效果,再到利用硬件加速保证 60fps,CSS 过渡的高级用法是一门兼顾艺术与工程的技术。你不再受限于预设的 ease,也不再畏惧动画卡顿。
下次设计师给你一套复杂的动效参数,你可以自信地写出一个 transition 属性,然后告诉他:"这不仅能实现你的曲线,而且性能还很好。"
最后,记住三条黄金法则:
- 能使用
transform和opacity的动画,绝不改变几何属性。 - 自定义贝塞尔曲线可以产生生动的物理效果,但不要滥用导致用户眩晕。
- 使用
will-change要谨慎,只在对性能敏感的关键动画上使用,并且动态添加/移除。