为什么选择 Konva
要判断该不该用 Konva,先得看清它到底替你做了什么。 答案不是「画图更方便」——原生 Canvas 画个矩形也就一行——而是它改变了画布的编程模型。
即时模式与保留模式
原生 Canvas 是即时模式(immediate mode)。调用 ctx.fillRect(10, 10, 100, 50)
之后,画布上留下的只有一片像素。没有「那个矩形」这个东西存在,
你没法问它现在在哪、也没法让它监听点击。
// 原生 Canvas:画完即忘
ctx.fillStyle = 'tomato';
ctx.fillRect(10, 10, 100, 50);
// 现在想让这个矩形能被拖动 —— 你得自己:
// 1. 记住它的 x/y/w/h
// 2. 监听 canvas 的 mousedown,判断坐标是否落在它范围内
// 3. mousemove 时更新坐标
// 4. 清空整个画布,重画所有图形
Konva 是保留模式(retained mode)。你创建的是一棵对象树, 每个节点自己持有属性、自己能绑事件、自己能被变换,重绘由框架负责:
const rect = new Konva.Rect({
x: 10, y: 10, width: 100, height: 50,
fill: 'tomato',
draggable: true, // 拖拽就这一行
});
rect.on('click', () => rect.fill('skyblue'));
layer.add(rect);
上面那四步全部消失了。这就是 Konva 的全部价值所在—— 它维护一棵场景图(scene graph),并在后台维护一张命中图(hit graph): 一张与可见画布等大、但每个图形用唯一颜色绘制的离屏画布。 判断鼠标点中了谁,只需要读取命中图上那一个像素的颜色,是 O(1) 的, 而不是遍历所有图形逐个做几何相交测试。
Konva 解决的问题
具体到日常开发,Konva 替你做掉的是这几件事:
命中检测。上面说的命中图。自己实现的话,圆形、路径、旋转过的矩形
各有各的相交算法,而 Konva 对任意 sceneFunc 画出来的自定义图形
都能正确命中——因为它是按像素判定的,不是按几何。
变换的累乘。父容器旋转 30°、子节点再缩放 2 倍,
最终的变换矩阵由框架算。你只管设 rotation 和 scaleX,
需要绝对坐标时调 getAbsolutePosition()。
重绘调度。改任何属性都会标记图层为脏,下一帧统一重绘, 不会因为你连改十个属性就重绘十次。
分层。Konva 的 Layer 对应一个真实的 <canvas> 元素。
把静态背景和频繁变动的前景分到两层,背景就不必每帧重画。
这是 Canvas 性能优化里性价比最高的一招,而它依赖框架管理多个画布元素。
什么时候不该用 Konva
Konva 不是免费的。它有几百 KB 的体积、一棵需要维护的对象树、 一张与画布等大的离屏命中画布(内存翻倍)。以下情况它是纯粹的负担:
只画静态图形。一张流程图、一个进度环、一段数据可视化,画完就不动、 也不需要点击——原生 API 更轻,几十行就够。
图表。ECharts、Chart.js 这类库已经把渲染封装好了, 它们内部也用 Canvas,你不需要再套一层。
需要 CSS 控制样式或可访问性。Canvas 上的内容对屏幕阅读器完全不可见, 也没法用 CSS 选择器命中。这两个需求指向 SVG 而不是任何 Canvas 库。
极致的包体积要求。比如嵌进第三方页面的挂件,整体预算只有几十 KB。
常见问题
Konva 和原生 Canvas 的性能差多少?
这个问题没有单一答案,取决于图形数量和要不要交互。
纯绘制、无交互时,Konva 一定更慢——它多了一层对象遍历和变换计算。 但差距通常在同一数量级,几百个图形时感知不到。
有交互时结论常常反过来。Konva 的命中图是 O(1) 的像素查表,
而多数人手写的命中检测是 O(n) 的遍历 + 几何相交。
图形上千之后,手写方案在 mousemove 里每帧遍历会先卡住。
真正需要小心的是内存:命中图是一张与画布等大的额外 canvas,
在 4K 画布上这不是小数目。不需要交互的节点设 listening(false)
可以让它们不进入命中图,这是 性能优化 里的第一条。
需要引入整个 Konva 吗?
Konva 10.0.0 起发布 ESM,支持 tree-shaking,理论上只打包你用到的图形。
但有两个现实的例外。一是 Konva.Node.create() 反序列化时按类名动态查找类,
打包器静态分析不出来,会把全部图形类保留。二是滤镜挂在 Konva.Filters
命名空间上,访问方式同样是动态的。
所以如果你用了序列化功能,实际体积接近完整包。不用的话能省下一部分, 但省不到「只有 Rect 和 Circle」那么极端。
学习成本高吗?
核心概念只有四层:Stage(舞台)→ Layer(图层)→ Group(分组)→ Shape(图形)。 搞清楚这四层的关系,一天之内就能写出可交互的画面,开始一页足够。
真正的成本在后面:什么时候该 cache()、图层怎么拆、
为什么加了滤镜就卡、导出的图为什么糊。这些都不是 API 问题,
而是 Canvas 本身的问题——换任何一个 Canvas 库都要重新踩一遍。
与其他方案的取舍
原生 Canvas API。分界线是「要不要交互」。画完不动的东西用原生, 需要点击、拖拽、独立变换的用 Konva。中间地带(只有一两个元素需要交互) 可以原生 + 手写命中,但超过三五个就该换了。
SVG。分界线是「图形数量」。SVG 的每个图形是一个 DOM 节点, 好处是能用 CSS、能被屏幕阅读器读到、缩放永远清晰; 代价是浏览器要为每个节点维护布局与样式,元素上千之后 DOM 开销会让页面明显变慢。Canvas 无论画多少都只是「多画几笔」。 经验拐点大约在一两千个元素,但这强烈依赖元素的复杂度与动画频率。
Fabric.js。定位与 Konva 最接近,同样是带交互的 Canvas 场景图。 区别在完成度的方向:Fabric 内置了自由绘制、SVG 导入导出、图形编辑控件, 拿来直接做设计工具很快;Konva 更轻、API 更一致, 适合把画布嵌进自己的产品里、按自己的交互逻辑定义一切。 选 Fabric 是接受它的产品形态,选 Konva 是自己定义产品形态。
PixiJS。基于 WebGL,渲染吞吐远高于 Canvas 2D, 适合游戏、粒子系统、需要着色器的视觉效果。代价有三个: 文本渲染不如 Canvas 2D 自然(Pixi 要把文字先画成纹理)、 与 DOM 的集成更麻烦、以及 WebGL 上下文丢失需要自己处理—— 移动端切后台回来画面变黑是真实会发生的,Canvas 2D 没有这个问题。
一句话:静态图形和图表用原生或专门的图表库,需要可访问性用 SVG, 做可交互的画布应用用 Konva,做完整设计工具考虑 Fabric,做游戏和高吞吐视觉用 Pixi。 更细的对比维度见 Canvas 库怎么选。