FLIP 是前端实现高性能布局动画的核心原则,通过先计算、后反向、再播放,把昂贵的布局重排转为 GPU 友好的 transform 动画,解决列表重排、卡片展开、视图切换等场景的卡顿问题。
一、FLIP 是什么
FLIP 是 First / Last / Invert / Play 的缩写,是一套标准化的动画实现流程。
二、核心原理(一句话)
先让元素瞬间跳到最终布局 ,再用 transform 反向拉回起点,最后播放 transform 回到 0 的过渡------全程只触发合成(Composite),不触发重排(Layout)。
三、四步详解(代码逻辑)
1. First(记录初始态)
读取元素动画前的位置、宽高、偏移 等几何信息(getBoundingClientRect)。
javascript
// 记录初始位置
const first = el.getBoundingClientRect();
2. Last(更新到最终态)
先修改 DOM/样式(如排序、展开、切换),让浏览器完成布局计算,再读取最终几何信息。
javascript
// 触发布局变化(如列表重排、添加/删除元素)
list.appendChild(newItem);
// 记录最终位置
const last = el.getBoundingClientRect();
3. Invert(反向偏移)
计算初始与最终的差值,用 transform 把元素"拉回"初始位置,视觉上看起来没动。
javascript
// 计算偏移量
const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
const deltaW = first.width / last.width;
const deltaH = first.height / last.height;
// 应用反向 transform
el.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${deltaW}, ${deltaH})`;
4. Play(播放动画)
在下一帧(requestAnimationFrame)移除反向 transform,开启 transition,让元素平滑过渡到最终态。
javascript
// 启用过渡
el.style.transition = 'transform 0.3s ease';
// 下一帧清除反向 transform
requestAnimationFrame(() => {
el.style.transform = 'none';
});
四、为什么 FLIP 性能更好
- 避免重排(Layout) :直接修改
top/left/width/height会触发全页面重排,成本极高。 - 只走合成(Composite) :
transform/opacity由 GPU 处理,不影响布局树,帧率更稳。 - 批量处理:一次计算所有元素差值,统一动画,减少浏览器开销。
五、适用场景
- 列表拖拽排序、增删动画
- 卡片展开/折叠、模态框切换
- 网格布局重排、视图切换
- 任何需要位置/尺寸平滑过渡的场景
六、关键优化点
- 用
will-change: transform提前提示浏览器优化。 - 动画期间不触发重排 (避免读取
offsetTop、clientWidth等)。 - 时长建议 200--500ms,兼顾流畅与感知。
- 复杂场景可用 GSAP Flip 插件简化代码。
七、完整示例(列表重排)
html
<ul class="list">
<li class="item">1</li>
<li class="item">2</li>
<li class="item">3</li>
</ul>
<button onclick="shuffle()">打乱</button>
<script>
function shuffle() {
const list = document.querySelector('.list');
const items = Array.from(list.children);
// 1. First:记录初始位置
const firstRects = items.map(el => el.getBoundingClientRect());
// 2. Last:打乱 DOM(触发布局)
items.sort(() => Math.random() - 0.5);
items.forEach(el => list.appendChild(el));
// 3. Invert:反向偏移
items.forEach((el, i) => {
const first = firstRects[i];
const last = el.getBoundingClientRect();
const dx = first.left - last.left;
const dy = first.top - last.top;
el.style.transform = `translate(${dx}px, ${dy}px)`;
});
// 4. Play:播放动画
requestAnimationFrame(() => {
items.forEach(el => {
el.style.transition = 'transform 0.3s ease';
el.style.transform = 'none';
});
});
}
</script>