Skip to content

前端动图与动效方案选择与最佳实践

一、 动图(Animated Image)方案

动图方案指使用图片格式来展示动画效果。

1. 主流动图格式对比

格式体积 (同等质量)质量兼容性透明背景备注
GIF最大最低极佳 (包括IE)不支持最古老的格式,色彩失真严重。
APNG比 GIF 小 50%-70%现代浏览器支持质量和体积优于 GIF。
WebP比 APNG 小约 20%较好支持在 iOS 上的支持晚于 APNG,但总体优于 APNG。
AVIF最小最高较差支持最新格式,性能最优,但兼容性是主要短板 (Chrome 85+, iOS 16+)。

2. 方案选择与最佳实践

结论

  • 首选:WebP。在体积、质量和兼容性之间取得了最佳平衡。
  • 放弃 APNGWebP 在各项指标上几乎全面优于 APNG
  • 暂不主用 AVIF:虽然性能最强,但现阶段兼容性太差,不适合作为主要方案。

最佳实践:降级方案 (Fallback)

为了在保证最优体验的同时覆盖所有用户,推荐使用带降级处理的方案。

核心逻辑:优先使用最高级的格式,如果浏览器不支持,则依次降级。

加载顺序AVIFWebPGIF / PNG (兜底)

3. 降级方案代码实现

(1) 能力检测 (Feature Detection)

在应用挂载前,异步检测浏览器对 AVIFWebP 的支持情况,并将结果挂载到全局对象(如 window)上。

核心要点:

  1. 异步检测:图片加载是异步的,必须使用 Promise 确保在能力检测完成后再挂载主应用,否则会获取到错误的初始值。
  2. 使用微型 Base64 图片:为了让检测速度足够快,不阻塞应用渲染,应使用一个 1x1 像素的、真实的、经过 Base64 转码的图片进行测试。
  3. 禁止修改后缀名:直接修改 .png 后缀为 .webp 是无效的,必须使用工具(如 EZGIF)进行真实格式转换。
javascript
// utils/feature-detect.js

// 1. 准备 1x1 像素的、真实的、经过 Base64 转码的图片
const base64Avif = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpeGkAAAAAAIAAQAAAAAAFAAAaaWxvYwAAAAAAAAAEAAAABAAEAAEAAQAAAAAAABgAAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAEAAAABAAAAEHBpeGEAAAAAAAMAAAAuaXJvdgEAAAAIAAAAAAAAYXYxQ7kQAAAAAAhjb2xybmNseAABAAEAAQAAAAAAGGF2MU NAOBAAAAAAABQUAAAAAAAAPG1kYXQ=';
const base64Webp = 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAQAcJaACdLoB+AAETgD//03gAAAA';

export const checkImageSupport = () => {
  return new Promise((resolve) => {
    // 默认兜底方案
    window.pictureSupports = 'low';

    const avif = new Image();
    avif.src = base64Avif;

    avif.onload = () => {
      // 支持 AVIF
      window.pictureSupports = 'avif';
      resolve();
    };

    avif.onerror = () => {
      // 不支持 AVIF,继续检测 WebP
      const webp = new Image();
      webp.src = base64Webp;
      webp.onload = () => {
        window.pictureSupports = 'webp';
        resolve();
      };
      webp.onerror = () => {
        // 两者都不支持,使用兜底方案
        resolve();
      };
    };
  });
};
javascript
// main.js

import { createApp } from 'vue';
import App from './App.vue';
import { checkImageSupport } from './utils/feature-detect';

// 先执行能力检测
checkImageSupport().then(() => {
  // 检测完毕后再挂载 Vue 应用
  createApp(App).mount('#app');
});

(2) 封装 AnimationImage 组件

创建一个组件,根据能力检测的结果自动选择最合适的图片源。

vue
<template>
  <img :src="imageUrl" alt="animated-image" />
</template>

<script setup>
import { ref, onBeforeMount } from 'vue';

const props = defineProps({
  avifSrc: String,
  webpSrc: String,
  lowSrc: {
    type: String,
    required: true,
  },
});

const imageUrl = ref(props.lowSrc); // 默认使用兜底图片

onBeforeMount(() => {
  if (window.pictureSupports === 'avif' && props.avifSrc) {
    imageUrl.value = props.avifSrc;
  } else if (window.pictureSupports === 'webp' && props.webpSrc) {
    imageUrl.value = props.webpSrc;
  }
});
</script>

(3) 使用组件

vue
<template>
  <AnimationImage
    avif-src="/path/to/animation.avif"
    webp-src="/path/to/animation.webp"
    low-src="/path/to/animation.gif"
  />
</template>

<script setup>
import AnimationImage from './components/AnimationImage.vue';
</script>

二、 动效(Dynamic Effect)方案

动效方案指通过代码(CSS, JS)实现的、通常由几何图形和文字构成的动画。

1. 主流动效方案对比

方案体积质量性能兼容性开发复杂度备注
CSS 动画最小无损较好 (CSS3)中等适合简单几何动效,需要手写 keyframes
Canvas 动画较小无损吃内存较好 (Canvas)需用 JS 逐帧绘制和清除,开发成本极高。
Lottie 动画较大无损可能卡顿尚可 (SVG/ES5)主流方案。需要引入播放库,但开发工作极简。

2. Lottie 方案详解与最佳实践

Lottie 是目前业界(阿里、字节、百度等)处理复杂动效的主流方案。

  • 工作流程:设计同学使用 AE (Adobe After Effects) 制作动效 ➔ 导出为 .json 文件 ➔ 前端使用 Lottie 播放库加载并渲染该 .json 文件。
  • 核心优势:将复杂的动画制作工作交还给专业的设计师,前端只需调用库即可,极大降低了开发成本。

(1) Lottie 的使用 (三步走)

  1. 安装库:安装 Lottie 播放库,如 lottie-web
  2. 准备容器:在 HTML 中准备一个 <div> 作为动画的渲染容器。
  3. 加载动画:通过 JS 请求 .json 文件,然后调用库的方法进行渲染。

核心要点:

  1. 使用 ref 获取 DOM:在 Vue/React 中,应使用 ref 来获取容器 DOM,绝对不要使用 id,以避免在同一页面多次使用组件时产生 id 冲突。
  2. 先请求再播放lottie-webloadAnimation 方法需要的是 JSON 数据对象,而不是 JSON 文件的 URL。因此,必须先用 axiosfetch 请求 URL,获取到 JSON 数据后,再将其传入 animationData 参数。
javascript
// 示例:封装一个 LottiePlayer 组件
// 1. 安装库
// npm install lottie-web axios

// 2. 封装组件
import { ref, onMounted, watch } from 'vue';
import lottie from 'lottie-web';
import axios from 'axios';

const props = defineProps({
  url: { // lottie.json 文件的 URL
    type: String,
    required: true,
  },
  canPlay: { // 是否可以播放(用于降级)
    type: Boolean,
    default: true,
  },
  fallbackSrc: String // 兜底静态图
});

// 使用 ref 获取 DOM 容器
const container = ref(null);
let anim = null; // 保存 lottie 实例

onMounted(async () => {
    if (props.canPlay && container.value) {
        try {
            // 必须先请求,获取到 JSON 数据
            const response = await axios.get(props.url);
            const animationData = response.data;

            if (anim) {
                anim.destroy();
            }

            // 加载动画
            anim = lottie.loadAnimation({
                container: container.value,
                renderer: 'svg', // 推荐使用 svg 模式
                loop: true,
                autoplay: true,
                animationData: animationData, // 传入 JSON 数据
            });
        } catch (error) {
            console.error('Lottie animation failed to load:', error);
        }
    }
});

(2) Lottie 降级策略

Lottie 同样存在兼容性问题(如不支持 SVG、requestAnimationFrame 的旧浏览器)和性能问题(在低端机型上可能卡顿),因此也需要降级方案。

降级条件

  1. 浏览器不支持 SVG
  2. 浏览器不支持 requestAnimationFrame
  3. 是低端机型(如 iOS 9 以下, Android 4.x 以下)。

降级方案:在不满足播放条件的设备上,不渲染 Lottie 动画,而是显示一张静态的 PNGGIF 作为替代。

(3) Lottie 降级代码实现

与动图检测类似,在应用挂载前进行全局的能力检测。

提示:iOS 版本检测较为复杂,可借助 mobile-detect.js 等第三方库来简化处理。

javascript
// utils/feature-detect.js
import MobileDetect from 'mobile-detect';

// Lottie 播放所需的基本能力
function supportsSvg() {
  return !!(document.createElementNS && document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect);
}

function supportsRequestAnimationFrame() {
  return 'requestAnimationFrame' in window;
}

// 检测是否为低端机型
function isLowEndDevice() {
  const md = new MobileDetect(window.navigator.userAgent);
  const isOldAndroid = md.os() === 'AndroidOS' && parseFloat(md.version('Android')) < 5.0;
  const isOldIOS = md.os() === 'iOS' && parseFloat(md.version('iOS')) < 10.0;
  return isOldAndroid || isOldIOS;
}


export const checkLottieSupport = () => {
  return new Promise((resolve) => {
    // 综合判断
    const canPlayLottie = supportsSvg() && supportsRequestAnimationFrame() && !isLowEndDevice();
    window.canPlayLottie = canPlayLottie;
    resolve();
  });
};

在组件中的应用

通过一个 v-if 指令,根据全局标志位 (window.canPlayLottie) 决定渲染 Lottie 容器还是兜底图片。

vue
<template>
  <div v-if="canPlay" ref="container"></div>
  <img v-else :src="fallbackSrc" alt="fallback-image" />
</template>

<script setup>
// ... (之前的 Lottie 加载逻辑)
// `canPlay` 属性的值应来自全局检测结果
const props = defineProps({
  // ...
  canPlay: {
    type: Boolean,
    default: true,
  },
  fallbackSrc: String
});
</script>