在網頁動畫開發中,我們經常遇到一個挑戰:如何實現流暢的 60fps 動畫效果?當我們直接修改元素的 top、left、width 或 height 屬性時,會觸發昂貴的 Layout 變化,導致動畫卡頓。FLIP 技術就是解決這個問題的經典方法。
為什麼需要 FLIP 技術?
在了解 FLIP 之前,我們先看看傳統動畫的效能問題:
昂貴的 Layout 變化
當我們直接修改布局屬性時,瀏覽器必須執行以下步驟:
- Recalculate Style - 重新計算樣式
- Layout (Reflow) - 重新計算元素位置和尺寸
- Paint - 重新繪製元素
- Composite - 合成圖層
// ❌ 昂貴的動畫實現 - 會觸發 Layoutelement.style.left = '100px'; // 觸發 Layoutelement.style.top = '50px'; // 觸發 Layoutelement.style.width = '200px'; // 觸發 Layout高效的 Transform 動畫
相對的,transform 屬性能夠在獨立的 Composite Layer 上執行,避免 Layout 和 Paint:
// ✅ 高效的動畫實現 - 只有 Compositeelement.style.transform = 'translate(100px, 50px) scale(1.5)'; // 只觸發 CompositeFLIP 技術就是利用這個原理,將昂貴的 Layout 變化轉換成高效的 Transform 動畫。
FLIP 技術核心原理
FLIP 是 First, Last, Invert, Play 四個字的縮寫,代表一種高效的動畫優化技術:
1. First - 記錄初始狀態
在 FLIP 技巧中,我們需要先記錄下動畫元件的初始狀態:
const getElementRect = (element) => { const rect = element.getBoundingClientRect(); return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };};
// F: 記錄初始狀態const firstState = getElementRect(element);2. Last - 套用最終狀態並記錄
接著進行一些運算後,套用動畫的最終狀態在動畫元件上,並且將完成動畫後的狀態記錄下來:
// 套用最終狀態(例如:添加 CSS class)element.classList.add('target-position');// 或者直接修改屬性element.style.left = '300px';element.style.top = '100px';
// L: 記錄最終狀態const lastState = getElementRect(element);3. Invert - 最主要的 Hack
FLIP 最主要的 hack 就是發生在這個階段。根據前兩個步驟,我們可以知道該動畫物件在動畫期間的位置變化,接著利用 transform 與 scale,將物件從動畫結尾位置移動回初始狀態的地點:
// I: 計算位置和尺寸差異const deltaX = firstState.left - lastState.left;const deltaY = firstState.top - lastState.top;const scaleX = firstState.width / lastState.width;const scaleY = firstState.height / lastState.height;
// 用 transform 將元素拉回初始位置element.style.transformOrigin = 'top left';element.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${scaleX}, ${scaleY})`;這時候,元素看起來就像是在初始位置,但實際上 DOM 結構已經是最終狀態了!
4. Play - 消除昂貴的 Layout Change
在最後的步驟時,元件已經被我們 transform 回起始點了,這時只要將 transform 屬性移除,並加上 transition 的效果,我們就能完美的消除原先昂貴的 Layout change,改以能擁有獨自 Layer 的 transform 來處理動畫效果:
// P: 播放高效動畫requestAnimationFrame(() => { element.style.transition = 'transform 0.3s ease-out'; element.style.transform = 'none'; // 移除 transform,讓元素回到最終位置});這樣一來,整個動畫過程只有 Transform 屬性在變化,避免了昂貴的 Layout 和 Paint 操作!
FLIP 技術的效能優勢
渲染效能對比
讓我們用數據來看看 FLIP 技術的效能優勢:
傳統動畫(修改 Layout 屬性)
// 觸發的流程:// Style → Layout → Paint → Compositeelement.style.left = '300px'; // 每幀都要重新計算 Layoutelement.style.width = '200px'; // 每幀都要重新計算 Layout
// 效能消耗:~16.67ms 中可能有 8-10ms 用於 Layout 計算FLIP 動畫(使用 Transform)
// 觸發的流程:// Composite 唯// 只觸發 Composite,在 GPU 上執行element.style.transform = 'translate(300px, 0) scale(1.5)';
// 效能消耗:~16.67ms 中只有 1-2ms 用於 Composite為什麼 Transform 這麼快?
- 獨立的 Composite Layer:
transform會創建新的合成圖層 - GPU 加速:在顯示卡上執行,不占用 CPU 資源
- 無需重新計算:不影響其他元素的位置
實戰範例:元素位置變化動畫
以下是一個完整的 FLIP 動畫實現,展示如何將元素從一個位置平滑移動到另一個位置:
HTML 結構
<div class="container"> <div class="element" id="moveable-element"> 點擊我移動 </div> <button onclick="moveElement()">移動元素</button> <button onclick="resetElement()">重設位置</button></div>
<div class="performance-demo"> <h3>效能對比:</h3> <button onclick="badAnimation()">昂貴的動畫 (Layout)</button> <button onclick="goodAnimation()">高效的動畫 (FLIP)</button></div>CSS 樣式
.container { position: relative; width: 100%; height: 400px; border: 2px solid #ccc; margin: 20px 0;}
.element { position: absolute; width: 100px; height: 100px; background: linear-gradient(45deg, #3498db, #9b59b6); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; cursor: pointer;
/* 初始位置 */ left: 50px; top: 50px;}
/* 目標位置 */.element.moved { left: 300px; top: 200px; width: 150px; height: 150px;}
.performance-demo { margin: 20px 0; padding: 20px; background: #f8f9fa; border-radius: 8px;}
.performance-demo button { margin: 5px; padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer;}
.performance-demo button:first-of-type { background: #e74c3c; color: white;}
.performance-demo button:last-of-type { background: #27ae60; color: white;}JavaScript 實現
class FLIPAnimation { constructor() { this.isAnimating = false; }
// 獲取元素的邊界資訊 getElementRect(element) { const rect = element.getBoundingClientRect(); return { left: rect.left, top: rect.top, width: rect.width, height: rect.height }; }
// FLIP 動畫實現 flip(element, changeCallback, duration = 300) { if (this.isAnimating) return; this.isAnimating = true;
// F: First - 記錄初始狀態 const firstState = this.getElementRect(element);
// L: Last - 套用變化並記錄最終狀態 changeCallback(); // 執行狀態變化 const lastState = this.getElementRect(element);
// I: Invert - 計算並應用反轉變換 const deltaX = firstState.left - lastState.left; const deltaY = firstState.top - lastState.top; const scaleX = firstState.width / lastState.width; const scaleY = firstState.height / lastState.height;
// 用 transform 將元素拉回初始位置 element.style.transformOrigin = 'top left'; element.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${scaleX}, ${scaleY})`;
// P: Play - 播放動畫 requestAnimationFrame(() => { element.style.transition = `transform ${duration}ms cubic-bezier(0.4, 0, 0.2, 1)`; element.style.transform = 'none';
// 清理 setTimeout(() => { element.style.transition = ''; element.style.transformOrigin = ''; this.isAnimating = false; }, duration); }); }}
// 實例化 FLIP 動畫器const flipAnimator = new FLIPAnimation();let isMoved = false;
// 高效的 FLIP 動畫function moveElement() { const element = document.getElementById('moveable-element');
flipAnimator.flip(element, () => { element.classList.toggle('moved'); isMoved = !isMoved; });}
function resetElement() { const element = document.getElementById('moveable-element');
if (isMoved) { flipAnimator.flip(element, () => { element.classList.remove('moved'); isMoved = false; }); }}
// 效能對比範例function badAnimation() { const element = document.getElementById('moveable-element');
// ❌ 昂貴的動畫 - 直接修改 left/top 屬性 console.time('昂貴的動畫');
element.style.transition = 'left 300ms, top 300ms, width 300ms, height 300ms';
if (isMoved) { element.style.left = '50px'; element.style.top = '50px'; element.style.width = '100px'; element.style.height = '100px'; isMoved = false; } else { element.style.left = '300px'; element.style.top = '200px'; element.style.width = '150px'; element.style.height = '150px'; isMoved = true; }
setTimeout(() => { element.style.transition = ''; console.timeEnd('昂貴的動畫'); }, 300);}
function goodAnimation() { console.time('高效的 FLIP 動畫');
moveElement();
setTimeout(() => { console.timeEnd('高效的 FLIP 動畫'); }, 300);}進階技巧與優化
1. 強制觸發 Composite Layer
確保元素能夠在獨立的 GPU 圖層上執行:
// 強制創建 Composite Layerelement.style.willChange = 'transform';// 或者element.style.transform = 'translateZ(0)';// 或者element.style.backfaceVisibility = 'hidden';
// 動畫結束後記得移除setTimeout(() => { element.style.willChange = '';}, animationDuration);2. 選擇適合的動畫屬性
只使用不會觸發 Layout 和 Paint 的屬性:
// ✅ 只觸發 Composite 的屬性'transform' // translate, scale, rotate, skew'opacity' // 透明度'filter' // 濾鏡效果
// ❌ 會觸發 Layout 的屬性'left', 'top', 'right', 'bottom''width', 'height''margin', 'padding''border-width''font-size'
// ❌ 會觸發 Paint 的屬性'color', 'background-color''border-color', 'border-style''box-shadow', 'border-radius''text-decoration'3. 效能監控與分析
使用瀏覽器開發工具監控動畫效能:
class PerformanceMonitor { static measureFLIP(element, changeCallback) { // 開始效能記錄 performance.mark('flip-start');
const firstState = element.getBoundingClientRect();
changeCallback();
const lastState = element.getBoundingClientRect();
const deltaX = firstState.left - lastState.left; const deltaY = firstState.top - lastState.top;
element.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
requestAnimationFrame(() => { element.style.transition = 'transform 300ms'; element.style.transform = 'none';
// 結束效能記錄 performance.mark('flip-end'); performance.measure('FLIP Animation', 'flip-start', 'flip-end');
// 查看結果 const measures = performance.getEntriesByName('FLIP Animation'); console.log(`FLIP 動畫耗時:${measures[0].duration}ms`); }); }}
// 使用方式PerformanceMonitor.measureFLIP(element, () => { element.classList.toggle('moved');});4. 處理複雜場景
滾動偏移處理
class AdvancedFLIP { getElementRect(element) { const rect = element.getBoundingClientRect(); return { left: rect.left + window.scrollX, top: rect.top + window.scrollY, width: rect.width, height: rect.height }; }}多元素同時動畫
class BatchFLIP { animateMultiple(elements, changeCallback) { // 批量記錄初始狀態 const firstStates = elements.map(el => this.getElementRect(el));
changeCallback();
// 批量記錄最終狀態和應用變換 elements.forEach((element, index) => { const lastState = this.getElementRect(element); const firstState = firstStates[index];
const deltaX = firstState.left - lastState.left; const deltaY = firstState.top - lastState.top;
element.style.transform = `translate(${deltaX}px, ${deltaY}px)`; });
// 批量播放動畫 requestAnimationFrame(() => { elements.forEach(element => { element.style.transition = 'transform 300ms ease-out'; element.style.transform = 'none'; }); }); }}實際應用場景
FLIP 技術在以下場景中特別有用:
1. 列表重新排序
function reorderList(listElement, newOrder) { const items = Array.from(listElement.children);
// F: 記錄所有項目的初始位置 const firstStates = items.map(item => item.getBoundingClientRect());
// L: 重新排序 DOM newOrder.forEach(index => { listElement.appendChild(items[index]); });
// I & P: 對每個項目應用 FLIP items.forEach((item, index) => { const lastState = item.getBoundingClientRect(); const firstState = firstStates[index];
const deltaX = firstState.left - lastState.left; const deltaY = firstState.top - lastState.top;
if (deltaX || deltaY) { item.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
requestAnimationFrame(() => { item.style.transition = 'transform 300ms ease-out'; item.style.transform = 'none'; }); } });}2. 響應式布局變化
function handleResponsiveChange() { const elements = document.querySelectorAll('.responsive-element');
elements.forEach(element => { flipAnimator.flip(element, () => { // 將元素從 grid 改為 flexbox 布局 element.parentElement.classList.toggle('flex-layout'); }); });}
// 監聽視窗大小變化window.addEventListener('resize', debounce(handleResponsiveChange, 300));3. 模態窗動畫
function showModal(triggerElement, modalElement) { flipAnimator.flip(modalElement, () => { modalElement.style.display = 'block'; modalElement.classList.add('active'); });}
function hideModal(modalElement, targetElement) { flipAnimator.flip(modalElement, () => { modalElement.classList.remove('active'); setTimeout(() => { modalElement.style.display = 'none'; }, 300); });}框架整合
雖然純 JavaScript FLIP 很強大,但現代框架也提供了優異的解決方案:
Vue Transition
<transition-group name="flip-list" tag="ul"> <li v-for="item in items" :key="item.id"> {{ item.name }} </li></transition-group>
<style>.flip-list-move { transition: transform 0.3s;}</style>React with Framer Motion
import { motion, AnimatePresence } from 'framer-motion';
<AnimatePresence> {items.map(item => ( <motion.div key={item.id} layout initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} > {item.content} </motion.div> ))}</AnimatePresence>Vue 中的 FLIP 應用
Vue 框架為 FLIP 動畫提供了原生支援,讓開發者可以更簡單地實現複雜的動畫效果:
Vue 2/3 的 Transition Group
Vue 的 transition-group 元件內建了 FLIP 技術,特別適合列表動畫:
<template> <div class="list-container"> <button @click="shuffle">打亂順序</button> <transition-group name="list" tag="div" class="list"> <div v-for="item in items" :key="item.id" class="list-item" > {{ item.name }} </div> </transition-group> </div></template>
<script>export default { data() { return { items: [ { id: 1, name: '項目 A' }, { id: 2, name: '項目 B' }, { id: 3, name: '項目 C' }, { id: 4, name: '項目 D' } ] } }, methods: { shuffle() { this.items = this.items.sort(() => Math.random() - 0.5); } }}</script>
<style>.list { display: flex; flex-direction: column; gap: 10px;}
.list-item { padding: 15px; background: #f0f0f0; border-radius: 5px; transition: all 0.3s ease;}
/* FLIP 動畫 */.list-move { transition: transform 0.3s ease;}
.list-enter-active,.list-leave-active { transition: all 0.3s ease;}
.list-enter-from { opacity: 0; transform: translateX(30px);}
.list-leave-to { opacity: 0; transform: translateX(-30px);}
.list-leave-active { position: absolute;}</style>Vue 3 Composition API 中手動實現 FLIP
<template> <div class="container"> <button @click="toggleLayout">切換布局</button> <div :class="['grid', { 'flex-layout': isFlexLayout }]"> <div v-for="item in items" :key="item.id" :ref="el => setItemRef(item.id, el)" class="item" > {{ item.name }} </div> </div> </div></template>
<script>import { ref, nextTick } from 'vue';
export default { setup() { const isFlexLayout = ref(false); const items = ref([ { id: 1, name: 'A' }, { id: 2, name: 'B' }, { id: 3, name: 'C' }, { id: 4, name: 'D' } ]);
const itemRefs = ref(new Map());
const setItemRef = (id, el) => { if (el) { itemRefs.value.set(id, el); } };
const performFLIP = async () => { // F: First - 記錄初始位置 const firstStates = new Map(); itemRefs.value.forEach((el, id) => { firstStates.set(id, el.getBoundingClientRect()); });
// L: Last - 觸發變化 isFlexLayout.value = !isFlexLayout.value; await nextTick(); // 等待 DOM 更新
// 記錄最終位置並執行 FLIP itemRefs.value.forEach((el, id) => { const firstState = firstStates.get(id); const lastState = el.getBoundingClientRect();
const deltaX = firstState.left - lastState.left; const deltaY = firstState.top - lastState.top;
// I: Invert el.style.transform = `translate(${deltaX}px, ${deltaY}px)`; el.style.transition = 'none';
// P: Play requestAnimationFrame(() => { el.style.transition = 'transform 0.3s ease-out'; el.style.transform = 'none'; }); }); };
const toggleLayout = () => { performFLIP(); };
return { isFlexLayout, items, setItemRef, toggleLayout }; }}</script>
<style>.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; transition: all 0.3s ease;}
.grid.flex-layout { display: flex; flex-direction: row;}
.item { padding: 20px; background: linear-gradient(45deg, #42b883, #35495e); color: white; border-radius: 8px; text-align: center; font-weight: bold;}</style>Vue Router 中的頁面轉場 FLIP
<template> <div class="app"> <nav> <router-link to="/page1" @click="prepareTransition">頁面 1</router-link> <router-link to="/page2" @click="prepareTransition">頁面 2</router-link> </nav>
<transition name="page" mode="out-in" @before-enter="beforeEnter" @enter="enter" @leave="leave" > <router-view :key="$route.path" /> </transition> </div></template>
<script>export default { data() { return { transitionStates: new Map() } }, methods: { prepareTransition() { // 記錄共享元素的位置 const sharedElements = document.querySelectorAll('[data-shared-element]'); sharedElements.forEach(el => { const id = el.dataset.sharedElement; this.transitionStates.set(id, el.getBoundingClientRect()); }); },
beforeEnter(el) { // 準備進入動畫 el.style.opacity = '0'; },
enter(el, done) { // 執行進入動畫 el.offsetHeight; // 觸發重繪 el.style.transition = 'opacity 0.3s ease'; el.style.opacity = '1';
// 處理共享元素 FLIP this.handleSharedElementsEnter(el);
setTimeout(done, 300); },
leave(el, done) { // 執行離開動畫 el.style.transition = 'opacity 0.3s ease'; el.style.opacity = '0'; setTimeout(done, 300); },
handleSharedElementsEnter(container) { const sharedElements = container.querySelectorAll('[data-shared-element]');
sharedElements.forEach(el => { const id = el.dataset.sharedElement; const firstState = this.transitionStates.get(id);
if (firstState) { const lastState = el.getBoundingClientRect();
const deltaX = firstState.left - lastState.left; const deltaY = firstState.top - lastState.top; const scaleX = firstState.width / lastState.width; const scaleY = firstState.height / lastState.height;
// FLIP 動畫 el.style.transformOrigin = 'top left'; el.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${scaleX}, ${scaleY})`;
requestAnimationFrame(() => { el.style.transition = 'transform 0.4s cubic-bezier(0.4, 0, 0.2, 1)'; el.style.transform = 'none'; }); } }); } }}</script>
<style>.page-enter-active,.page-leave-active { transition: opacity 0.3s ease;}
.page-enter-from,.page-leave-to { opacity: 0;}</style>Vue 自定義 FLIP Directive
創建一個可重用的 FLIP 指令:
const flipDirective = { mounted(el, binding) { el._flipInstance = new FLIPAnimation(); },
updated(el, binding) { if (binding.value !== binding.oldValue) { // 觸發 FLIP 動畫 el._flipInstance.flip(el, () => { // 變化已經發生,這裡不需要做什麼 }); } },
unmounted(el) { if (el._flipInstance) { el._flipInstance = null; } }};
// main.jsapp.directive('flip', flipDirective);使用自定義指令:
<template> <div class="container"> <button @click="toggleClass">切換樣式</button> <div v-flip="hasSpecialClass" :class="{ 'special': hasSpecialClass }" class="box" > FLIP 動畫元素 </div> </div></template>
<script>export default { data() { return { hasSpecialClass: false } }, methods: { toggleClass() { this.hasSpecialClass = !this.hasSpecialClass; } }}</script>這些 Vue 整合範例展示了如何在 Vue 應用中充分利用 FLIP 技術,從簡單的列表動畫到複雜的頁面轉場,都能實現流暢的 60fps 動畫效果。
最佳實踐與注意事項
1. 效能優化檢查清單
✅ 使用正確的 CSS 屬性
// 好:只觸發 Compositeelement.style.transform = 'translateX(100px)';element.style.opacity = '0.5';
// 壞:觸發 Layout/Paintelement.style.left = '100px';element.style.backgroundColor = 'red';✅ 預先提示瀏覽器
// 動畫開始前element.style.willChange = 'transform';
// 動畫結束後(重要!)element.style.willChange = 'auto';✅ 避免動畫期間修改 DOM
// 不要在動畫過程中做這些事requestAnimationFrame(() => { element.style.transition = 'transform 300ms'; element.style.transform = 'none';
// ❌ 避免在這裡修改 DOM // document.body.appendChild(newElement);});2. 進階效能技巧
使用 Web Animation API
class ModernFLIP { flip(element, changeCallback, options = {}) { const firstState = element.getBoundingClientRect();
changeCallback();
const lastState = element.getBoundingClientRect(); const deltaX = firstState.left - lastState.left; const deltaY = firstState.top - lastState.top;
// 使用 Web Animation API 而非 CSS transition element.animate([ { transform: `translate(${deltaX}px, ${deltaY}px)` }, { transform: 'none' } ], { duration: options.duration || 300, easing: options.easing || 'ease-out', fill: 'both' }); }}動畫中斷與恢復
class InterruptibleFLIP { constructor() { this.currentAnimation = null; }
flip(element, changeCallback, duration = 300) { // 中斷當前動畫 if (this.currentAnimation) { this.currentAnimation.cancel(); }
const firstState = element.getBoundingClientRect(); changeCallback(); const lastState = element.getBoundingClientRect();
const deltaX = firstState.left - lastState.left; const deltaY = firstState.top - lastState.top;
this.currentAnimation = element.animate([ { transform: `translate(${deltaX}px, ${deltaY}px)` }, { transform: 'none' } ], { duration, easing: 'ease-out' });
this.currentAnimation.addEventListener('finish', () => { this.currentAnimation = null; }); }}3. 無障礙與用戶體驗
// 尊重用戶的動畫偏好const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
class AccessibleFLIP { flip(element, changeCallback, duration = 300) { const actualDuration = prefersReducedMotion ? 0 : duration;
if (actualDuration === 0) { // 直接切換,無動畫 changeCallback(); return; }
// 正常 FLIP 動畫 this.performFLIP(element, changeCallback, actualDuration); }}
// 提供螢幕讀取器友好的狀態更新element.setAttribute('aria-live', 'polite');element.setAttribute('aria-label', '內容正在動畫中');4. 錯誤處理與降級
class RobustFLIP { flip(element, changeCallback, duration = 300) { try { const firstState = element.getBoundingClientRect();
// 檢查元素是否可見 if (firstState.width === 0 || firstState.height === 0) { console.warn('FLIP: 元素不可見,跳過動畫'); changeCallback(); return; }
changeCallback();
const lastState = element.getBoundingClientRect(); const deltaX = firstState.left - lastState.left; const deltaY = firstState.top - lastState.top;
// 檢查是否真的有位置變化 if (Math.abs(deltaX) < 1 && Math.abs(deltaY) < 1) { return; // 無需動畫 }
this.performAnimation(element, deltaX, deltaY, duration);
} catch (error) { console.error('FLIP 動畫遇到錯誤:', error); // 降級到無動畫版本 changeCallback(); } }
performAnimation(element, deltaX, deltaY, duration) { element.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
requestAnimationFrame(() => { element.style.transition = `transform ${duration}ms ease-out`; element.style.transform = 'none';
setTimeout(() => { element.style.transition = ''; }, duration); }); }}總結
FLIP 技術是一個優雅的效能優化解決方案,它的核心思想是:用 Transform 取代昂貴的 Layout 變化。通過理解和實踐 FLIP,我們可以:
效能優勢
- 消除 Layout Thrashing:將昂貴的 16ms+ 的 Layout 計算減少到 1-2ms 的 Composite
- GPU 加速:利用硬體顯示卡的平行處理能力,釋放 CPU 資源
- 60fps 流暢體驗:確保動畫始終保持高幀速率
技術精髓
- F (First):記錄初始狀態
- L (Last):套用最終狀態
- I (Invert):用 Transform 拉回初始位置(最關鍵的 Hack)
- P (Play):移除 Transform,讓元素流暢動畫到最終位置
實用價值
- 理解渲染原理:深入理解瀏覽器的 Style → Layout → Paint → Composite 流程
- 優化思維:學會如何識別和避免效能瓶頸
- 框架理解:更好地理解 Framer Motion、Vue Transition 等框架的底層原理
何時使用 FLIP?
- 列表重新排序動畫
- 元素位置變化動畫
- 響應式布局變化
- 模態窗、抽屜等元件動畫
- 任何需要高效能位置/尺寸變化的場景
記住:FLIP 不只是一個動畫技巧,更是一種效能優化的思維方式。在現代網頁開發中,每一毫秒的效能提升都能為用戶帶來更好的體驗。掌握 FLIP 技術,就是掌握了高效能動畫開發的核心精髓。
回報錯字、失效連結,或告訴我你想看的延伸主題。