动画性能优化
动画性能优化的核心思路
动画性能优化关键在于减少浏览器重绘和回流。Vue.js 中动画性能问题通常出现在频繁的 DOM 操作、复杂的 CSS 属性和不当的动画触发时机。优化方向主要包括:使用 transform 和 opacity 属性、减少布局抖动、合理使用 will-change、避免强制同步布局等。
CSS 硬件加速
// 不好的写法
<div class="box" :style="{ left: x + 'px', top: y + 'px' }"></div>
// 优化写法
<div class="box" :style="{ transform: `translate(${x}px, ${y}px)` }"></div>
transform 和 opacity 属性不会触发重排,浏览器会将这些元素的渲染交给 GPU 处理。在 Vue 中动态修改元素位置时,优先使用 transform 而不是 top/left。对于需要硬件加速的元素,可以添加:
.optimized {
transform: translateZ(0);
backface-visibility: hidden;
perspective: 1000px;
}
减少不必要的响应式数据
Vue 的响应式系统会在数据变化时触发更新,频繁变化的动画数据可能导致性能问题:
// 不好的写法
data() {
return {
position: { x: 0, y: 0 } // 整个对象会变为响应式
}
}
// 优化写法
data() {
return {
position: { x: 0, y: 0 }
}
},
created() {
this.nonReactivePosition = { ...this.position } // 非响应式副本用于动画
}
对于纯动画数据,可以考虑使用 Object.freeze() 或直接使用普通对象避开 Vue 的响应式追踪。
合理使用 requestAnimationFrame
在 Vue 中实现 JavaScript 动画时,避免使用 setTimeout/setInterval:
methods: {
animate() {
this._animationId = requestAnimationFrame(() => {
this.x += 1
if (this.x < 100) {
this.animate()
}
})
},
beforeDestroy() {
cancelAnimationFrame(this._animationId)
}
}
对于复杂场景,可以考虑使用动画库如 GSAP,它内置了 RAF 优化:
import gsap from 'gsap'
methods: {
runAnimation() {
gsap.to(this.$refs.box, {
x: 100,
duration: 1,
ease: "power2.out"
})
}
}
列表动画优化
使用 <transition-group>
时,大量元素动画可能导致性能问题:
<!-- 优化方案1:禁用部分元素的动画 -->
<transition-group name="list" tag="ul">
<li
v-for="(item, index) in items"
:key="item.id"
:data-index="index"
:class="{ 'no-transition': index > 50 }"
>
{{ item.text }}
</li>
</transition-group>
<style>
.no-transition {
transition: none !important;
}
</style>
<!-- 优化方案2:使用 FLIP 技术 -->
methods: {
shuffle() {
const before = Array.from(this.$refs.list.children)
.map(el => el.getBoundingClientRect())
// 改变数据顺序
this.items = _.shuffle(this.items)
this.$nextTick(() => {
const after = Array.from(this.$refs.list.children)
.map(el => el.getBoundingClientRect())
before.forEach((beforeRect, i) => {
const afterRect = after[i]
const deltaX = beforeRect.left - afterRect.left
const deltaY = beforeRect.top - afterRect.top
const child = this.$refs.list.children[i]
child.style.transform = `translate(${deltaX}px, ${deltaY}px)`
child.style.transition = 'transform 0s'
requestAnimationFrame(() => {
child.style.transform = ''
child.style.transition = 'transform 500ms'
})
})
})
}
}
组件销毁时的动画处理
组件卸载时执行动画需要注意内存泄漏问题:
// 安全执行卸载动画
beforeDestroy() {
const el = this.$el
el.style.opacity = 1
const animation = el.animate(
[{ opacity: 1 }, { opacity: 0 }],
{ duration: 300 }
)
animation.onfinish = () => {
el.remove()
}
// 防止内存泄漏
this.$once('hook:destroyed', () => {
animation.cancel()
animation.onfinish = null
})
}
滚动动画性能优化
实现视差滚动等效果时,避免直接在 scroll 事件中修改 DOM:
// 优化滚动处理
mounted() {
this._scrollHandler = () => {
this._scrollY = window.scrollY
this._rafId = requestAnimationFrame(this.updatePositions)
}
window.addEventListener('scroll', this._scrollHandler, { passive: true })
},
methods: {
updatePositions() {
this.$refs.parallaxElements.forEach(el => {
const speed = parseFloat(el.dataset.speed)
el.style.transform = `translateY(${this._scrollY * speed}px)`
})
}
},
beforeDestroy() {
window.removeEventListener('scroll', this._scrollHandler)
cancelAnimationFrame(this._rafId)
}
SVG 动画优化
Vue 中使用 SVG 动画时:
<template>
<svg>
<!-- 不好的写法:直接操作 path 的 d 属性 -->
<path :d="complexPath" />
<!-- 优化写法:使用 CSS transform -->
<g transform="scale(1.2)">
<path d="M10 10 L20 20" />
</g>
<!-- 高性能动画方案 -->
<circle
cx="50"
cy="50"
r="10"
style="transform-box: fill-box; transform-origin: center;"
:style="{ transform: `scale(${scale})` }"
/>
</svg>
</template>
动画性能监测工具
在开发过程中集成性能监测:
// 自定义性能追踪指令
Vue.directive('perf-track', {
inserted(el, binding) {
const startTime = performance.now()
const stopTracking = () => {
const duration = performance.now() - startTime
if (duration > 16) {
console.warn(`[Performance] ${binding.value} took ${duration.toFixed(2)}ms`)
}
}
el._transitionend = stopTracking
el.addEventListener('transitionend', stopTracking)
el.addEventListener('animationend', stopTracking)
},
unbind(el) {
el.removeEventListener('transitionend', el._transitionend)
el.removeEventListener('animationend', el._transitionend)
}
})
// 使用方式
<div
v-perf-track="'Box animation'"
class="animated-box"
:class="{ 'animate': shouldAnimate }"
></div>
动画与 Vue 生命周期协调
确保动画与组件生命周期正确协调:
export default {
data() {
return {
isMounted: false
}
},
mounted() {
// 等待下一个tick确保DOM已更新
this.$nextTick(() => {
this.isMounted = true
// 强制重绘触发动画
void this.$el.offsetHeight
})
},
methods: {
leaveAnimation(done) {
const el = this.$el
const height = el.offsetHeight
el.style.height = `${height}px`
el.style.overflow = 'hidden'
requestAnimationFrame(() => {
el.style.height = '0'
el.style.paddingTop = '0'
el.style.paddingBottom = '0'
el.addEventListener('transitionend', done)
})
}
}
}
动态组件过渡优化
动态组件切换时的性能考虑:
<template>
<!-- 使用 mode="out-in" 避免同时渲染两个组件 -->
<transition name="fade" mode="out-in" appear>
<component :is="currentComponent" :key="componentKey" />
</transition>
</template>
<script>
export default {
data() {
return {
currentComponent: 'ComponentA',
componentKey: 0
}
},
methods: {
async switchComponent() {
// 预加载组件
const ComponentB = await import('./ComponentB.vue')
// 在动画开始前准备新组件
this.$nextTick(() => {
this.currentComponent = ComponentB
this.componentKey++ // 强制重新创建组件实例
})
}
}
}
</script>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>
动画节流与防抖
处理频繁触发的动画事件:
// 使用 lodash 的节流
import { throttle } from 'lodash'
export default {
data() {
return {
scrollPosition: 0
}
},
mounted() {
this.throttledScroll = throttle(this.handleScroll, 16) // ~60fps
window.addEventListener('scroll', this.throttledScroll)
},
methods: {
handleScroll() {
this.scrollPosition = window.scrollY
this.updateAnimations()
},
updateAnimations() {
// 基于滚动位置更新动画
}
},
beforeDestroy() {
window.removeEventListener('scroll', this.throttledScroll)
}
}
动画资源预加载
对于需要加载资源的动画:
export default {
methods: {
preloadAssets() {
const images = [
require('@/assets/anim-frame1.jpg'),
require('@/assets/anim-frame2.jpg')
]
images.forEach(src => {
new Image().src = src
})
// 预加载字体
const font = new FontFace('Animation Font', 'url(/fonts/animation-font.woff2)')
font.load().then(() => {
document.fonts.add(font)
})
}
},
mounted() {
this.preloadAssets()
}
}
复杂动画的状态管理
对于复杂动画状态,使用 Vuex 可能导致性能问题:
// 专用动画状态模块
const animationStore = {
state: () => ({
timeline: 0,
isPlaying: false
}),
mutations: {
UPDATE_TIMELINE(state, value) {
state.timeline = value
}
},
actions: {
updateTimeline({ commit }, value) {
commit('UPDATE_TIMELINE', value)
}
}
}
// 在组件中使用
computed: {
...mapState('animation', ['timeline'])
},
methods: {
...mapActions('animation', ['updateTimeline']),
animate() {
this._rafId = requestAnimationFrame(() => {
const now = performance.now()
const delta = now - this._lastTime
this._lastTime = now
// 直接修改本地副本,批量提交
this._localTimeline += delta * 0.001
if (this._localTimeline - this.timeline > 0.1) {
this.updateTimeline(this._localTimeline)
}
this.animate()
})
}
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn