paint worklet 必须在主线程注册,其 paint() 在合成线程执行但初始化绑定主线程;动画逻辑应通过 sharedarraybuffer/atomics 或 postmessage 解耦,inputproperties + registerproperty() 实现跨线程状态同步,且 paint() 仅做轻量数学计算。

Paint Worklet 必须注册在主线程,Worker 中无法直接使用
很多人误以为 paintWorklet 可以像 Web Worker 一样在独立线程里注册和运行,但事实是:浏览器强制要求 worklet.addModule() 只能在主线程调用,且注册后的 class 实例(即 paint() 函数)虽在合成线程(Compositor Thread)执行,但**初始化、模块解析、类定义仍绑定主线程**。试图在 Worker 中调用 CSS.paintWorklet.addModule() 会直接抛出 DOMException: Failed to execute 'addModule' on 'PaintWorklet': Cannot be called in a Worker。
所以“在 Worker 中实现 Paint Worklet”这个前提不成立——你不能绕过主线程注册,但可以做到「动画逻辑与主线程解耦」。
- 主线程只负责一次性的
addModule()注册和 CSS 属性设置(如background-image: paint(anim)) - 动画的时序控制、参数计算(如时间戳、噪声值、颜色插值)应交给
SharedArrayBuffer + Atomics或postMessage驱动,避免在paint()内做耗时运算 - Paint Worklet 的
paint()函数本身必须轻量:只读共享内存或inputProperties,不做循环、浮点运算密集操作
用 inputProperties + registerProperty() 把动画状态从主线程“喂”进 Paint Worklet
registerProperty() 是让主线程可控更新、Paint Worklet 安全读取的关键桥梁。它比直接传 canvas 或 ImageData 更高效,且天然跨线程同步(底层由 Blink 管理)。
例如你想实现一个随时间流动的波纹背景:
// 主线程
CSS.registerProperty({
name: '--wave-time',
syntax: '<number>',
inherits: false,
initialValue: 0
});
<p>CSS.paintWorklet.addModule('wave-paint.js');
</p></number>
然后在 wave-paint.js 中:
// wave-paint.js(Paint Worklet 模块)
class WavePainter {
static get inputProperties() { return ['--wave-time']; }
paint(ctx, geom, props) {
const t = props.get('--wave-time').value;
// 仅用 t 做简单三角函数,不查表、不 new Array
for (let y = 0; y
- 主线程用
requestAnimationFrame或Web Worker + Atomics持续更新element.style.setProperty('--wave-time', t) - Paint Worklet 自动收到新值,无需手动同步;
inputProperties是只读代理,无 GC 开销 - 避免在
paint()中调用Date.now()或performance.now()—— 时间必须由主线程提供,否则会因合成线程无事件循环而读到错误时间
高频更新 --wave-time 时,主线程仍可能卡顿?用 Worker + Atomics 控制节奏
如果每帧都走 requestAnimationFrame + setProperty,高频更新(如 60fps)本身就会触发样式重计算,尤其当大量元素监听同一自定义属性时,主线程压力不小。这时可用 SharedArrayBuffer 让 Worker 承担计时和写入责任。
主线程只需一次分配共享内存,并监听变化:
// 主线程
const sab = new SharedArrayBuffer(4);
const timeView = new Float32Array(sab);
<p>// 启动 Worker 控制时间流
const worker = new Worker('time-worker.js');
worker.postMessage({ sab });</p><p>// 在 rAF 中轮询(轻量!)
function tick() {
const t = Atomics.load(timeView, 0);
element.style.setProperty('--wave-time', t);
requestAnimationFrame(tick);
}
tick();
</p>
Worker 内部:
// time-worker.js
self.onmessage = ({ data }) => {
const timeView = new Float32Array(data.sab);
let t = 0;
const interval = setInterval(() => {
t += 0.02; // 控制动画速率
Atomics.store(timeView, 0, t);
}, 16);
};
- 主线程不再主动计算
t,只做原子读取 + 属性设置,rAF 负载极低 - Worker 独立计时,不受主线程帧率抖动影响(比如主线程卡顿时,Wave 动画仍匀速)
- 注意:Safari 目前不支持
SharedArrayBuffer(需cross-origin-isolated上下文),生产环境需降级为postMessage,但频率建议压到 ≤30fps 避免消息队列积压
Paint Worklet 的 canvas 上下文不是完整 2D API,别依赖 fillText 或 getImageData
ctx 在 Paint Worklet 中是受限的:fillText、getImageData、createPattern、filter 等均不可用,且 ctx.canvas 为 null。它只暴露最基础的绘图能力,目标是保证合成线程能快速光栅化。
- 支持的操作:
fillRect、strokeRect、clearRect、beginPath、lineTo、stroke、fill、save/restore、translate、scale - 不支持:
drawImage(无法传入外部图片)、createLinearGradient(但可用ctx.fillStyle = 'rgb(...)'动态设色) - 性能陷阱:在
paint()中反复调用ctx.beginPath()+ 大量lineTo()渲染复杂路径,会显著拖慢光栅化——应尽量用fillRect或预计算顶点数组(存在inputProperties中)
真正高性能的背景动画,往往靠数学生成(正弦/噪声/分形),而非像素操作。一旦开始想“怎么把 Canvas 图片塞进去”,就说明已经偏离了 Paint Worklet 的设计意图。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











