常见的动态图片(GIF、APNG、WEBP)及Electron使用webpmux合成动态WebP

前言

以下是 GIF、APNG、WebP(动态) 三种常见动态图片格式的简明对比,适用于前端、设计、开发等场景快速选型:

特性 GIF APNG WebP(动态)
全称 Graphics Interchange Format Animated Portable Network Graphics Web Picture (Animated)
颜色支持 最多 256 色(8-bit) 真彩色(24-bit) + 8-bit Alpha 透明 真彩色(24-bit) + 8-bit Alpha 透明
透明度 1-bit(全透/不透) ✅ 支持半透明 ✅ 支持半透明
压缩方式 LZW(无损,但效率低) Deflate(无损) VP8/VP9(支持有损/无损
文件体积 ❌ 最大(尤其复杂动画) 中等(比 GIF 小,比 WebP 大) 最小(通常比 GIF 小 30%~70%)
画质 ❌ 色彩失真、锯齿明显 ✅ 高保真、无损 ✅ 可控(有损/无损),色彩丰富
浏览器兼容性 ✅ 全平台 100% 支持 ✅ 现代浏览器支持(Chrome、Firefox、Safari ≥16.4、Edge) ✅ 广泛支持(Chrome、Edge、Firefox、Android;Safari ≥14) ❌ IE 不支持
是否开放标准 是(老旧) ✅ 是(PNG 官方扩展,2025 年正式纳入标准) 否(Google 主导,但事实标准)
典型用途 表情包、简单动效、邮件动图 UI 微交互、高保真图标动画、需无损场景 网页 Banner、商品动图、性能敏感场景

快速选型建议:

  • 要最大兼容性(如邮件、老旧系统) → 用 GIF
  • 要无损画质 + 透明 + 开放标准 → 用 APNG
  • 要最小体积 + 最佳性能 + 现代 Web → 用 WebP

    实践中常采用 WebP 为主 + GIF 为 fallback 的优雅降级策略:

1
2
3
4
<picture>
<source srcset="anim.webp" type="image/webp">
<img src="anim.gif" alt="动画">
</picture>

总结一句话

GIF 是“兼容之王”,APNG 是“画质之选”,WebP 是“性能之王”。

webpmux

Windows 环境下安装 webpmux(Google 官方 WebP 工具集的一部分),有以下几种可靠方式。截至 2026 年,最简单、推荐的方法是使用 预编译二进制包 或通过 包管理器 安装。

Google 官方为 Windows 提供了现成的 .exe 工具包,包含 cwebp.exedwebp.exewebpmux.exe 等。

下载与安装

下载地址
https://storage.googleapis.com/downloads.webmproject.org/releases/webp/index.html

下载连接

https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-1.6.0-windows-x64.zip

解压 ZIP 文件
解压后进入 bin/ 目录,你会看到:

1
2
3
4
5
6
bin/
├── cwebp.exe
├── dwebp.exe
├── gif2webp.exe
├── webpmux.exe ← 这就是你需要的!
└── ...

bin/ 目录加入系统 PATH(可选但推荐)

  • Win + S 搜索 “环境变量” → “编辑系统环境变量”
  • 点击“环境变量” → 在“系统变量”中找到 Path → 编辑 → 新建
  • 添加你解压后的 bin 文件夹完整路径,例如:
    D:\Tools\libwebp-1.6.0-windows-x64\bin
  • 重启终端(CMD / PowerShell)

验证安装

1
webpmux -version

应输出类似:

1
1.6.0

完成!现在你可以在任何目录使用 webpmux 命令。

使用示例

假设你有 frame1.png, frame2.png,想合成动画 WebP:

1
2
3
4
5
6
7
8
9
10
:: 先转成单帧 WebP
cwebp frame1.png -o f1.webp
cwebp frame2.png -o f2.webp
cwebp frame3.png -o f3.webp
cwebp frame4.png -o f4.webp

:: 合成动画(每帧 200ms,无限循环)
webpmux -frame f1.webp +200+0+0+1 -frame f2.webp +200+0+0+1 -frame f3.webp +200+0+0+1 -frame f4.webp +200+0+0+1 -loop 0 -o anim1.webp

webpmux -frame f1.webp +100+0+0+0 -frame f2.webp +100+0+0+0 -frame f3.webp +100+0+0+0 -frame f4.webp +100+0+0+0 -loop 0 -o anim2.webp

参数说明

+200+0+0+1 表示:

  • 200:显示时间(毫秒)
  • 0,0:X/Y 偏移(通常为 0)
  • 1:混合模式(0= 后一帧和前一帧叠加 ,1=后一帧完全替换前一帧) ,所以一般我们都用1

Electron

复制文件

1
2
3
4
5
6
7
8
9
your-electron-app/
├── main.js ← 主进程
├── package.json
├── assets/ ← 存放你的 .exe 工具(不会被打包进 asar)
│ └── win/
│ ├── webpmux.exe
│ └── cwebp.exe
├── src/ ← 渲染器代码
└── ...

设置资源目录

新版本

electron-builder.json5中添加

1
2
3
4
5
{
"extraResources": [
"assets/win/**"
],
}

旧版本

package.json 中添加:

1
2
3
4
5
6
7
{
"build": {
"extraResources": [
"assets/win/**"
]
}
}

路径获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// main.js(主进程)
const { app, BrowserWindow } = require('electron');
const { spawn } = require('child_process');
const path = require('path');

function getExePath(exeName) {
if (app.isPackaged) {
// 打包后:exe 在 resources/assets/win 目录
return path.join(process.resourcesPath, 'assets', 'win', exeName);
} else {
// 开发环境:从项目根目录找
return path.join(app.getAppPath(), 'assets', 'win', exeName);
}
}

// 调用示例
function runWebPMux(args) {
const exePath = getExePath('webpmux.exe');
console.log('Executing:', exePath, args);

const child = spawn(exePath, args, {
cwd: app.getPath('temp') // 或指定工作目录
});

child.stdout.on('data', (data) => console.log(data.toString()));
child.stderr.on('data', (data) => console.error(data.toString()));
child.on('close', (code) => console.log(`webpmux exited with code ${code}`));
}

调用EXE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const {spawn} = require('child_process');

async function runExe(exePath: String, args: String[], showlog: boolean = false) {
return new Promise<boolean>((resolve, _) => {
const child = spawn(exePath, args);

child.stdout.on('data', (data: String) => {
if (showlog) {
console.log(`stdout: ${data}`);
}
});

child.stderr.on('data', (data: String) => {
if (showlog) {
console.error(`stderr: ${data}`);
}
});

child.on('close', (code: number) => {
if (code == 0) {
resolve(true)
} else {
resolve(false)
}
});
})
}

export {runExe}

调用示例

1
2
3
4
5
6
7
8
9
10
let cwebp_exe = getExePath("cwebp.exe")

async function test() {
let sourcePath = "C:\\Users\\DELL\\Pictures\\icon_book_dili.png"
let outputPath = "C:\\Users\\DELL\\Pictures\\icon_book_dili.webp"
let result = await runExe(cwebp_exe, [sourcePath, "-o", outputPath])
console.log(result)
}

test()

WebP转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import {runExe} from "./z_run_exe_utils.ts";
import path from "node:path";

const os = require('os');
const fs = require('fs');

// 单张图片转换成webp
async function imgToWebp(cwebpPath: String, sourcePath: String, outputPath: String) {
return await runExe(cwebpPath, [sourcePath, "-o", outputPath])
}

// 批量转换图片为webp动画
async function imgArrToWebp(
cwebpPath: String,
webpmuxPath: String,
sourcePathArr: String[],
outputPath: String,
delay: number = 200
) {
let webpArr = []
// 新生成的WebP文件
let webpNewArr = []


const tempDir = os.tmpdir();
for (let i = 0; i < sourcePathArr.length; i++) {
let sourcePath = sourcePathArr[i]
if (sourcePath.endsWith(".webp")) {
webpArr.push(sourcePath)
} else {
let outputPath = path.join(tempDir, `${Date.now()}_${i}.webp`)
let result = await imgToWebp(cwebpPath, sourcePathArr[i], outputPath)
if (result) {
webpArr.push(outputPath)
webpNewArr.push(outputPath)
}
}

}

let args = []
for (let i = 0; i < webpArr.length; i++) {
args.push("-frame")
args.push(webpArr[i])
args.push(`+${delay}+0+0+1`)
}

args.push("-loop")
args.push("0")
args.push("-o")
args.push(outputPath)

// console.info("webpmux args:", args)
let result = await runExe(webpmuxPath, args)
for (let i = 0; i < webpNewArr.length; i++) {
fs.unlink(webpNewArr[i], (err: any) => {
if (err) {
console.error('删除失败:', err);
return;
}
});
}
return result
}

export {imgToWebp, imgArrToWebp}