不是给你操作 DOM 的“CSS 插件”
要理解 Paint Worklet,需要先拆掉两个惯性思维。

第一,Worklet 不是 Service Worker。它没有完整的 JS 运行时环境,不能访问 DOM、不能发起请求、不能使用大部分浏览器 API。它的运行环境里只有有限的输入:绘制区域尺寸、CSS 自定义属性,以及合成线程传递的状态。好处是,绘制逻辑可以被渲染机制调用,甚至可能被挪到合成线程执行,从而不阻塞主线程。
第二,Paint Worklet 不是让 JS 去控制 CSS 绘制结果,而是让 JS 成为 CSS 绘制算法的一部分。你写的 paint 函数会在浏览器需要绘制元素背景时被调用,绘制结果作为一张位图交给渲染管线。正因为如此,它和 Canvas 2D 共享同一套绘图 API,但使用场景被限定在“当 CSS 需要一个图像值”的时候。
一个最小可用的 Paint Worklet
下面是一个最简示例,绘制一段斜线纹理。Worklet 文件只需要一个 registerPaint 调用:
registerPaint('diagonal-stripes', class {
paint(ctx, size) {
ctx.strokeStyle = 'rgba(0, 0, 0, 0.15)';
ctx.lineWidth = 4;
for (let x = -size.width; x < size.width + size.height; x += 12) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x + size.height, size.height);
ctx.stroke();
}
}
});
然后在 CSS 中引用它:
.article-box {
background: paint(diagonal-stripes);
}
要让这段代码真正跑起来,还需要在页面入口注册 Worklet 模块:
if ('paintWorklet' in CSS) {
CSS.paintWorklet.addModule('/paint/diagonal-stripes.js');
}
可以看到,代码形态和 Canvas 2D 脚本非常像,但它没有被绑定到某个 canvas 元素,而是被浏览器直接当作 CSS 背景图来使用。paint 函数接收两个核心参数:绘制上下文 ctx 和绘制区域 size。size 是元素所需绘制区域的像素尺寸,当元素大小变化时会自动更新。
什么情况下才真的需要 Paint Worklet
聊完语法,回到工程问题:什么场景会真的想用 Paint Worklet?
我印象比较深的场景有两个。第一个是设计系统团队。团队维护的基础组件需要支持主题一键切换,按钮纹理也要跟着换。用雪碧图的话,要维护多套主题多套图片,不同分辨率下还会发虚。用 Paint Worklet 生成纹理,主题切换只需要改变 CSS 自定义属性,例如 –pattern-color: #ff6a00,绘制函数会自动响应。
第二个场景是数据可视化大屏。大屏往往需要 1920×1080 甚至更高的分辨率,动态网格背景如果用背景图,要么图片体积大,要么缩放后线条变得模糊。而 Paint Worklet 可以根据元素实际像素尺寸绘制,线条始终保持清晰的 1px,数据刷新时还能实时调整网格密度。
这两个场景的共同点是:绘制逻辑需要跟随状态变化、需要像素级控制、并且对跨尺寸和分辨率有稳定要求。反过来,如果只是想画个圆角渐变,或者某个静态纹理,用原生 CSS 或 SVG 往往更省事。
和直接画 Canvas 有什么区别
很多人会问:既然都是 Canvas 2D API,为什么不直接用一个 canvas 元素,而要折腾 Worklet?
区别在于接入成本和渲染语义。使用 canvas 元素,你需要手动把它放到布局里、处理它的尺寸和坐标、还要考虑它和元素内容之间的层叠关系。而 Paint Worklet 直接绑在元素的背景或边框上,它天然参与 CSS 的盒模型,可以和 border-radius、box-shadow、mask 等无缝组合。对于一个想要“多用 CSS 完成装饰效果”的场景,Paint Worklet 的代码侵入性明显更低。
从性能上看,Paint Worklet 的位图可能被缓存,如果绘制参数没有变化,重复绘制时不一定需要重新执行 paint。而手动管理 canvas 时,每一帧绘制都在你的控制中,成本很难交给渲染引擎优化。当然,这并不表示 Paint Worklet 自动快,触发机制和绘制复杂度仍然需要自己把握。
以下是几种方案在我心中的定位:
| 方案 | 实现复杂度 | 性能上限 | 主要场景 |
|---|---|---|---|
| CSS 渐变/滤镜 | 低 | 高 | 规则渐变、圆角、简单纹理 |
| SVG 背景图 | 中 | 中 | 矢量图案、图标型背景 |
| 独立 Canvas | 高 | 受绘制频率影响 | 实时交互、复杂动画 |
| Paint Worklet | 中 | 高(但受触发机制影响) | 与 CSS 属性绑定的纹理、边框、进度条 |
注意,我刻意没有把 Paint Worklet 描述成替代方案。它更像是一个“补充层”,专门处理那些用 CSS 写不出来、又不想引入一个完整 Canvas 组件的装饰性需求。
三个容易掉进去的坑
实际使用中,下面三个问题最容易在项目里爆发。
坑一:试图读取布局和 DOM。 Paint Worklet 的绘制环境里没有 document,也没有 layout 信息。你无法知道文字区域在哪里,也无法根据内容高度调整绘制。它只适合“与内容无关”的装饰绘制。如果业务上真的需要内容和背景互相作用,应该考虑 Canvas 覆盖或 SVG 滤镜。
坑二:忽略重绘触发条件。 paint 函数并不是每帧都会被调用。元素尺寸变化、特定 CSS 属性变化、元素首次渲染时才会触发。但如果你在 paint 函数内部使用了 Math.random(),每次触发都会生成一个新图案,造成肉眼可见的闪烁。稳妥的做法是引入“随机种子”参数,比如一个自定义属性,让随机结果稳定可复现。
坑三:把浏览器兼容性简单化。 Chrome 和 Edge 很早就支持,Safari 和 Firefox 的支持情况目前仍不完整。这并不意味着完全不能用,而是要把 Paint Worklet 当增强特性处理:支持的环境享受细节,不支持的回到基础背景。用特性检测来分步加载,而不是写一堆 hack。
下面这个例子演示了如何把颜色、尺寸都通过自定义属性传入,保证绘制输出可控:
registerPaint('gradient-texture', class {
static get inputProperties() {
return ['--pattern-color', '--pattern-size'];
}
paint(ctx, size, properties) {
const color = properties.get('--pattern-color').toString();
const patternSize = properties.get('--pattern-size').value || 32;
ctx.fillStyle = color;
for (let x = 0; x < size.width; x += patternSize) {
for (let y = 0; y < size.height; y += patternSize) {
ctx.fillRect(x, y, 1, 1);
}
}
}
});
工程落地:把渐进增强做在前面
现在落地,我认为需要把它当“渐进增强层”而不是基础依赖。具体的做法可以总结为四点:
- 先把绘制参数用 CSS 自定义属性暴露出来,这样 Worklet 内部可以保持稳定。
- 在 CSS 中准备好降级样式,例如普通的渐变背景。
- 在 JS 里通过 ‘paintWorklet’ in CSS 检测支持性,再动态注册模块。
- Worklet 文件单独打包发布,用并行脚本加载,避免阻塞首屏。
在页面入口处,代码可以这样组织:
function initPaint() {
if (!('paintWorklet' in CSS)) return;
CSS.paintWorklet.addModule('/paint/gradient-texture.js')
.then(() => document.body.classList.add('paint-ready'));
}
当 paint-ready 这个类出现后,再通过一条仅在高支持环境生效的样式规则启用 paint,而不是一开始就用。这样即使加载失败,用户也只会看到降级的渐变背景,不会影响核心内容。
性能与调试要提前设计
最后聊一下性能和调试。我第一次在项目里用 Paint Worklet 时,就遇到过一个卡顿问题:后台定时器每秒更新一个自定义属性,期望纹理微妙变化,结果整个页面在低端手机上掉帧明显。原因很简单,每次属性变化都会触发 paint,而 paint 内部又做了大量循环。
优化思路有三类。第一是降低绘制频率,比如把对自定义属性的更新从 1 秒 1 次改成只在数据环比变化时更新。第二是降低单次绘制复杂度,把循环次数和绘制面积解耦,例如限制最大循环次数,用固定步长覆盖大区域。第三是合理使用输入属性列表,只有通过 inputProperties 显式声明的属性变化才会触发重绘,这本身就是一道过滤门槛。
调试方面,Chrome DevTools 的 Sources 面板可以直接打开 Worklet 源文件下断点,Performance 面板录制时也能看到 paint 任务耗时。开发阶段建议把绘制参数尽量简化,方便在 DevTools 里模拟不同尺寸。
结语
Paint Worklet 给我的感觉不是一项“新特效技术”,而是一把打磨 CSS 表达力的底层工具。它适合做视觉细节的最后一公里处理,但不适合包办所有绘制。判断标准其实很朴素:如果一段几十行代码的绘制函数能替代一张背景图,并且这个函数需要响应 CSS 变量的变化,那么 Paint Worklet 值得考虑;如果只是画个圆角或渐变,老老实实用 CSS 就好。未来随着浏览器支持面扩大,有理由相信它会成为设计系统组件库中一个常规选项。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/806/