做前端动画的同学大概都遇到过这样的情况:精心调好的一组关键帧参数,只存在于内存里的一个配置对象中,页面一刷新就没了;想把这些动画数据保存下来给下次使用,除了塞进localStorage似乎没有更好的选择。其实换一个思路,用SQLite在浏览器端做结构化存储,再配合Animation Worklet做高性能动画渲染,可以搭出一套既灵活又稳定的离线动画数据方案。本文就从原理讲到实战,完整走一遍这套组合的实现过程。

Animation Worklet是什么,为什么它适合做动画渲染
Animation Worklet是Houdini体系下的一个API,它允许开发者用JavaScript编写自定义的动画逻辑,并且让这段逻辑运行在一个独立于主线程的Worklet线程中。这一点是它和普通requestAnimationFrame方案最大的区别:主线程再怎么卡顿,动画的插值计算和更新依然可以按帧稳定执行,不会出现掉帧或者动画卡成一帧一帧跳的情况。
从写法上看,Animation Worklet的核心是定义一个继承自Animator的类,实现animate方法,然后注册给Worklet运行时。下面是一个最小的示例:
// animation-worklet.js
registerAnimator('scale-animator', class {
// currentTime当前时间,effect动画效果
animate(currentTime, effect) {
const t = currentTime / 1000;
// 根据时间计算缩放值,产生呼吸灯效果
const scale = 1 + 0.1 * Math.sin(t * 2 * Math.PI);
effect.localTime = scale * 1000;
}
});
// 主线程中的注册与使用
// await CSS.animationWorklet.addModule('animation-worklet.js');
// const animation = new WorkletAnimation(
// 'scale-animator',
// new KeyframeEffect(el, [{ transform: 'scale(1)' }, { transform: 'scale(1.2)' }], { duration: 1000, iterations: Infinity })
// );
// animation.play();
需要注意的是,Worklet环境是隔离的,没有DOM、没有window对象,能用的API非常有限。这意味着动画的“逻辑代码”和“配置数据”天然需要分离:逻辑写在Worklet里,数据由主线程注入。这个特性恰好为SQLite登场埋下了伏笔——配置数据需要持久化、需要结构化管理,而localStorage只能存字符串,查询和关联能力几乎为零。
浏览器端运行SQLite的两种主流方式
在浏览器里跑SQLite,目前主要有两条路。第一条是sql.js,它是SQLite编译成WebAssembly后的纯JS封装,整个数据库跑在内存里,需要自己手动导出二进制数据做持久化。第二条是官方的SQLite WASM版本配合OPFS(Origin Private File System),可以直接把数据库文件落在浏览器的私有文件系统中,支持真正的文件级读写。
两者各有取舍。sql.js接入简单,兼容性好,缺点是每次修改后都要手动export一次完整数据,数据量大了会有性能压力。SQLite WASM加OPFS则是真正的增量写入,体验接近本地应用,但对浏览器版本要求较高。对于动画配置这种数据量不大、但读写频繁的场景,本文实战部分采用sql.js配合IndexedDB存二进制的方案,兼顾兼容性和实现难度。
先把基础的数据库操作封装好,建一张动画配置表:
// db.js 数据库初始化与基础操作
import initSqlJs from 'sql.js';
let db = null;
export async function initDB() {
const SQL = await initSqlJs({ locateFile: f => `/${f}` });
// 实际项目中可从IndexedDB读取上次保存的数据恢复
db = new SQL.Database();
db.run(`
CREATE TABLE IF NOT EXISTS animation_presets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
animator_type TEXT NOT NULL,
keyframes TEXT NOT NULL,
duration INTEGER DEFAULT 1000,
iterations INTEGER DEFAULT 1,
created_at TEXT DEFAULT (datetime('now'))
)
`);
return db;
}
export function savePreset(preset) {
const stmt = db.prepare(
'INSERT OR REPLACE INTO animation_presets (name, animator_type, keyframes, duration, iterations) VALUES (?, ?, ?, ?, ?)'
);
stmt.run([
preset.name,
preset.animatorType,
JSON.stringify(preset.keyframes),
preset.duration,
preset.iterations
]);
stmt.free();
}
这里有一个容易被忽略的细节:KeyframeEffect的关键帧对象没法直接存进SQLite,所以存入前要JSON序列化,读取后再反序列化传给主线程的动画构造逻辑。结构化字段(如时长、循环次数)单独拆成列,是为了后续可以用SQL按条件筛选,比如只查某个animator类型下的所有预设,这是localStorage的纯字符串存储做不到的。
实战:把SQLite配置喂给Animation Worklet
现在把两部分串起来。整体数据流是:页面加载时初始化SQLite并读出预设列表展示给用户,用户选中某条预设后,主线程反序列化关键帧数据,构造KeyframeEffect和WorkletAnimation并播放;用户在界面上调整参数后,点击保存,主线程调用savePreset写回SQLite并同步到IndexedDB完成持久化。
核心的加载与播放代码如下:
// player.js 从数据库读取预设并播放
export async function playPreset(presetName, targetEl) {
await CSS.animationWorklet.addModule('animation-worklet.js');
const stmt = db.prepare('SELECT * FROM animation_presets WHERE name = ?');
stmt.bind([presetName]);
let preset = null;
if (stmt.step()) preset = stmt.getAsObject();
stmt.free();
if (!preset) throw new Error('预设不存在');
const keyframes = JSON.parse(preset.keyframes);
const effect = new KeyframeEffect(targetEl, keyframes, {
duration: preset.duration,
iterations: preset.iterations
});
const animation = new WorkletAnimation(preset.animator_type, effect);
animation.play();
return animation;
}
// 持久化到IndexedDB,防止刷新丢失
export async function persistToFile() {
const data = db.export(); // Uint8Array二进制
const req = indexedDB.open('anim-db', 1);
req.onupgradeneeded = e => e.target.result.createObjectStore('files');
req.onsuccess = e => {
const tx = e.target.result.transaction('files', 'readwrite');
tx.objectStore('files').put(data, 'animation.sqlite');
};
}
这套方案跑起来之后,动画数据的管理能力会有明显提升。举例来说,产品想加一个“按创建时间排序显示最近使用的动画预设”的功能,只需要一条ORDER BY created_at DESC的SQL就能实现;如果之前用的是localStorage存JSON字符串,就得把整个数组读出来再手动排序,数据结构一变还得写迁移逻辑。
性能与方案的取舍建议
任何方案都不是完美的,这套组合也有它的边界。首先,Animation Worklet目前只在Chromium内核浏览器中有完整支持,Safari和Firefox的落地情况需要持续关注,正式上线前务必做特性检测:'animationWorklet' in CSS不成立时要降级到Web Animations API。其次,sql.js的全量导出在数据库超过几MB后会成为瓶颈,如果预设数量膨胀到上千条,建议迁移到SQLite WASM加OPFS的方案,享受增量写入的好处。
和几个常见方案对比一下会更清晰。localStorage的问题是只能存字符串、容量小、同步阻塞主线程;IndexedDB直接存对象的问题是查询能力弱,复杂条件筛选要靠遍历;而SQLite方案换来了真正的SQL查询、事务保证和与本地开发习惯一致的建模方式,代价是需要引入约1MB左右的WASM文件。对于动画配置这类需要结构化管理和频繁条件查询的数据,这个代价通常是值得的。
最后总结一下实践中的三个要点:一是Worklet环境隔离决定了动画逻辑与配置必须分层,这层分离反而让数据持久化设计变得更清晰;二是序列化边界要划在主线程一侧,Worklet里不做任何JSON解析;三是特性检测和降级路径要在项目初期就规划好,别等上线才发现部分用户的浏览器根本不支持Worklet。掌握这些,SQLite加Animation Worklet的组合就能稳定地支撑起一套离线友好的前端动画系统。
SQLiteAnimation Worklet前端动画持久化修改时间:2026-09-12 10:58:44