跳到主要内容

为什么选择 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 库怎么选。