阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 动画性能优化

动画性能优化

作者:陈川 阅读数:26903人阅读 分类: Vue.js

动画性能优化的核心思路

动画性能优化关键在于减少浏览器重绘和回流。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

上一篇:长列表渲染方案

下一篇:打包体积优化

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌