Analysis of konvajs principle



Those who have used canvas know that it has many APIs and is troublesome to use. For example, if I want to draw a circle, I have to adjust a bunch of APIs, which is not friendly to development.

const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
//Set font style
context.font = '24px SimSun, Songti SC';
Context. Filltext ('24px in Song typeface ', 20, 50);
//Draw a complete circle
context.fillStyle = 'RGB(255, 0, 0)';
context.arc(150, 75, 50, 0, Math.PI * 2);

In order to solve this pain point, canvas libraries such as Pixi, zrender and fabric were born. Konva today is also an excellent canvas framework. The API encapsulation is simple and easy to understand. It is implemented based on typescript and has react and Vue versions.

      const stage = new Konva.Stage({
        container: 'root',
        width: 1000,
        height: 1000,
      const layer = new Konva.Layer();
      const group = new Konva.Group();
      const text = new Konva.Text({
        text: 'Hello, this is some good text',
        fontSize: 30,

      const circle = new Konva.Circle({
        x: stage.width() / 2,
        y: stage.height() / 2,
        radius: 70,
        fill: 'red',
        stroke: 'black',
        strokeWidth: 4

architecture design

Konva Tree

From the code given in the preface, we can see that konva has a certain nested structure, some of which are similar to DOM structure. You can add and remove child nodes through add and remove.

Analysis of konvajs principle

Konva tree mainly includes the following parts:

  1. Stage root node: This is the root node of the application. A div node will be created as the event receiving layer and distributed according to the coordinates when the event is triggered. A stage node can contain multiple layer layers.
  2. Layer: a canvas node will be created in layer, which is mainly used to draw elements in canvas. A layer can contain multiple groups and shapes.
  3. Group: a group contains multiple shapes. If you transform and filter them, all shapes in it will take effect.
  4. Shape: refers to text, rect, circle and other graphics. These are classes encapsulated by konva.

Analysis of konvajs principle

build dom

When creating a stage, two canvas nodes and content container nodes will be created. These two canvas nodes are used for perfectdrawenabled, which will be described later.

It should be noted here that this content node is the container of the entire konva canvas, and subsequent layers will be appended.

  _buildDOM() {
    this.bufferCanvas = new SceneCanvas({
      width: this.width(),
      height: this.height(),
    this.bufferHitCanvas = new HitCanvas({
      pixelRatio: 1,
      width: this.width(),
      height: this.height(),

    if (!Konva.isBrowser) {
    var container = this.container();
    if (!container) {
      throw 'Stage has no container. A container is required.';
    // clear content inside container
    container.innerHTML = '';

    // content
    this.content = document.createElement('div'); = 'relative'; = 'none';
    this.content.className = 'konvajs-content';

    this.content.setAttribute('role', 'presentation');



When calling stage.add, it will not only call the drawing method of layer, but also append the canvas node of layer.

  add(layer: Layer, {
    if (arguments.length > 1) {
      for (var i = 0; i < arguments.length; i++) {
      return this;

    var length = this.children.length;
    if (length > MAX_LAYERS_NUMBER) {
        'The stage has ' +
          length +
          ' layers. Recommended maximum number of layers is 3-5. Adding more layers into the stage may drop the performance. Rethink your tree structure, you can use Konva.Group.'
    layer.setSize({ width: this.width(), height: this.height() });

    // draw layer and append canvas to container

    if (Konva.isBrowser) {

    // chainable
    return this;


Batch Render

As can be seen from the previous code, the drawing method is not called manually, but it will still be drawn, indicating that it will be rendered at a certain time.
This opportunity is in the add method. No matter which group, layer or stage is added first, the rendering will eventually be triggered.

All three of them inherit the container class. There is an add method in the container class. Let’s explore it.

add(...children: ChildType[]) {
    if (arguments.length > 1) {
      for (var i = 0; i < arguments.length; i++) {
      return this;
    var child = children[0];
    //If the child node to be added already has a parent node, first remove it from the parent node and then insert it into the current node
    if (child.getParent()) {
      return this;
    //Set the index and parent of the child node
    child.index = this.getChildren().length;
    child.parent = this;
    this._fire('add', {
      child: child,
    //Request drawing
    return this;

In addition to some conventional processing, the key to rendering is_requestDrawInside the method. Here, the above layer is calledbatchDrawBatch redrawing.

  _requestDraw() {
    if (Konva.autoDrawEnabled) {
      const drawNode = this.getLayer() || this.getStage();

The principle of batch redrawing is to use the requestanimationframe method to put the content to be drawn to the next frame for drawing. In this way, you do not need to draw repeatedly to modify multiple graphics and multiple attributes at the same time.

batchDraw() {
    // _ Waitingfordraw guarantees that requestanimframe will be executed only once
    if (!this._waitingForDraw) {
      this._waitingForDraw = true;
      //If you call multiple methods to modify the shape attribute, batch painting will be performed here
      //Avoid the overhead caused by multiple drawing
      Util.requestAnimFrame(() => {
        this._waitingForDraw = false;
    return this;

Shape painting

All places involving drawing are implemented by calling shape class_sceneFuncMethod, take circle as an example:

  _sceneFunc(context) {
    context.arc(0, 0, this.attrs.radius || 0, 0, Math.PI * 2, false);

The two base classes of shape and node are only responsible for calling, and the specific implementation is placed on the specific shape implementation. This brings two benefits. One is to realize custom graphics, and the other is that it will be convenient to support SVG and webgl in the future.

offscreen rendering

What is off screen rendering? It is to pre render a canvas outside the screen, and then draw it on the canvas to be displayed on the screen in the form of DrawImage, which improves the rendering performance of objects with similar or repeated shapes.

Suppose we have a list page, and it will cost a lot to redraw it every time we scroll. But if we implement a canvas pool and save the drawn list items. The next time you scroll here, you can directly take the DrawImage from the canvas pool to the page.

There is a cache method on the node class, which can realize fine-grained off screen rendering.

Three canvass will be created inside the cache method, namely:

  1. Cachedscenecanvas: off screen rendering of the canvas used to draw graphics.
  2. Cachedfiltercanvas: used to process filter effects.
  3. Cachedhitcanvas: used to handle off screen rendering of hitcanvas.
_drawCachedSceneCanvas(context: Context) {;
    //Get off screen canvas
    const canvasCache = this._getCanvasCache();
    context.translate(canvasCache.x, canvasCache.y);

    var cacheCanvas = this._getCachedSceneCanvas();
    var ratio = cacheCanvas.pixelRatio;
    //Draw the off screen canvas onto the canvas to be displayed
      cacheCanvas.width / ratio,
      cacheCanvas.height / ratio


When canvas draws stroke and fill, if it encounters transparency, stroke will coincide with part of fill, which is not in line with our expectations.

For example, the following code:

      const canvas = document.getElementById("canvas");
      const bufferCanvas = document.createElement("canvas");
      const bufferCtx = bufferCanvas.getContext("2d");
      const ctx = canvas.getContext("2d");

      ctx.globalAlpha = 0.5;
      ctx.fillStyle="RGB(255, 0, 0)";

The actual display effect is that stroke and fill in the middle overlap.

Analysis of konvajs principle

In this case, konvajs implements a perfectdrawenabled function, which will do the following:

  1. Draw shape on buffercanvas
  2. Draw fill and stroke
  3. Apply transparency on layer
  4. Draw the buffercanvas onto the scenecanvas

You can see the obvious difference between enabling perfectdrawenabled and disabling perfectdrawenabled:

Analysis of konvajs principle

It will create a buffercanvas and a bufferhitcanvas in the stage. The former is for scenecanvas and the latter is for hitcanvas.

In the drawscene method of shape, whether to use buffercanvas will be judged:

// if buffer canvas is needed
    if (this._useBufferCanvas() && !skipBuffer) {
      stage = this.getStage();
      bufferCanvas = stage.bufferCanvas;
      bufferContext = bufferCanvas.getContext();
      // layer might be undefined if we are using cache before adding to layer
      var o = this.getAbsoluteTransform(top).getMatrix();
      bufferContext.transform(o[0], o[1], o[2], o[3], o[4], o[5]);
      //Draw fill and stroke in buffercanvas, bufferContext, this);

      var ratio = bufferCanvas.pixelRatio;

      if (hasShadow) {
      //Applying transparency in scenecanvas
      //Draw buffercanvas to scenecanvas
        bufferCanvas.width / ratio,
        bufferCanvas.height / ratio


The events in konva create a div node in the outer layer of canvas, receive DOM events on this node, judge which shape is currently clicked according to the coordinate points, and then distribute the events.

So the key is how to judge which shape is currently clicked? Compared with the complex calculation in zrender, konva uses a quite clever way.

Event distribution

Konva currently supports the following events. Events isEvent name - event handling methodMapping of.

    [MOUSEENTER, '_pointerenter'],
    [MOUSEDOWN, '_pointerdown'],
    [MOUSEMOVE, '_pointermove'],
    [MOUSEUP, '_pointerup'],
    [MOUSELEAVE, '_pointerleave'],
    [TOUCHSTART, '_pointerdown'],
    [TOUCHMOVE, '_pointermove'],
    [TOUCHEND, '_pointerup'],
    [TOUCHCANCEL, '_pointercancel'],
    [MOUSEOVER, '_pointerover'],
    [WHEEL, '_wheel'],
    [CONTEXTMENU, '_contextmenu'],
    [POINTERDOWN, '_pointerdown'],
    [POINTERMOVE, '_pointermove'],
    [POINTERUP, '_pointerup'],
    [POINTERCANCEL, '_pointercancel'],
    [LOSTPOINTERCAPTURE, '_lostpointercapture'],
  //Binding event
  _bindContentEvents() {
    if (!Konva.isBrowser) {
    EVENTS.forEach(([event, methodName]) => {
      //Events are bound to the DOM node content
      this.content.addEventListener(event, (evt) => {

We take MouseDown as an example to analyze its processing method_pointerdownInside.
_pointerdownLet’s do it firstsetPointersPositions, calculate the coordinates of the current mouse click, subtract the coordinates of content relative to the page, and get the coordinates of the current click relative to content. And deposited it in_changedPointerPositionsInside.

Analysis of konvajs principle

Then traverse_changedPointerPositions, passgetIntersectionGet the shape shape you clicked. thisgetIntersectionTraversal calls the of each layergetIntersectionMethod to obtain the corresponding shape through layer.

Because there can be multiple layers and each layer can draw multiple shapes at the same position, multiple shapes can be obtained theoretically. Konva only takes the first shape in the order of layer – > shape.

Analysis of konvajs principle

Then the stage will call the on the shape_fireAndBubbleMethod, this method calls_fireSend konva’s own event, and the event callback bound by on will be triggered, a bit like jQuery.

Then konva will continue to find the parent node and continue to call the parent node_fireAndBubbleMethod until the parent node can no longer be found, which implements event bubbling.

For shapes that do not want to be clicked, you can setisListeningProperty is false so that the event will not be triggered.

Match shape

So how does layer get the corresponding shape according to the click coordinates? If it is a regular figure (rectangle and circle), it is easier to calculate. What about the following irregular figures?

Analysis of konvajs principle

As we all know, there is one in canvasgetImageDataMethod, which will return an imagedata information according to the incoming coordinates, which has the color value corresponding to the current coordinates. So can we get the corresponding shape according to this color value?

Analysis of konvajs principle

Therefore, konva will create two canvases when creating the layer, one for scenecanvas to draw the shape, and the other hitcanvas is in memory to judge whether it is hit.

canvas = new SceneCanvas();
hitCanvas = new HitCanvas({
  pixelRatio: 1,

Analysis of konvajs principle

When the shape is initialized, a random color will be generated and stored in the shapes array as a key.

constructor(config?: Config) {
    // set colorKey
    let key: string;

    while (true) {
      //Generate random color values
      key = Util.getRandomColor();
      if (key && !(key in shapes)) {
    this.colorKey = key;
    //Stored in shapes array
    shapes[key] = this;

Each time you draw on the scenecanvas, it will also be drawn in the hitcanvas in memory, and the randomly generated color values above will be filled as the colors of fill and stroke.

When you click scenecanvas, you can obtain the clicked coordinate points by calling hitcanvasgetImageDataYou can get the colorkey, and then you can find the corresponding shape through the colorkey. It’s a very clever implementation.

Analysis of konvajs principle

However, this method also has defects, because the generated random hex color has an upper limit of 256 at most 256256 = 16777216, if more than this, it will lead to inaccurate matching.

However, consider that if there are 16777216 DOM nodes, the browser will explode. Replacing so many canvas graphics will also lead to a performance explosion.

Custom hitfunc

If you want to customize the event response area, konva also provides the hitfunc method for you to implement. When drawing hitcanvas, the original drawing scenefunction is invalid and hitfunc is drawn instead.

drawHit(can?: HitCanvas, top?: Node, skipDragCheck = false) {
    if (!this.shouldDrawHit(top, skipDragCheck)) {
      return this;

    var layer = this.getLayer(),
      canvas = can || layer.hitCanvas,
      context = canvas && canvas.getContext(),
      //If hitfunc is available, scenefunc is not used
      drawFunc = this.hitFunc() || this.sceneFunc(),
      cachedCanvas = this._getCanvasCache(),
      cachedHitCanvas = cachedCanvas && cachedCanvas.hit;

    if (!this.colorKey) {
        'Looks like your canvas has a destroyed shape in it. Do not reuse shape after you destroyed it. If you want to reuse shape you should call remove() instead of destroy()'
    // ..., context, this);
    // ...

Drag event

Konva’s drag event does not use the native method, but calculates the moving distance based on MouseMove and touchmove, and then manually sets the position of the shape. The implementation logic is relatively simple, which will not be described in detail here.


Konva supports a variety of filters. You need to cache the shape before using the filterfilter()Method to add a filter.
In the cache, in addition to creating canvas for off screen rendering, filter canvas is also created. Filter treatment in_getCachedSceneCanvasInside.

Analysis of konvajs principle

First, draw the scenecanvas onto filtercanvas through DrawImage, then filtercanvas obtains all the imagedata, traverses all the set filter methods, and passes the imagedata to the filter method for processing.

After processing the imagedata, draw it onto the filtercanvas through putimagedata.

    if (filters) {
      if (!this._filterUpToDate) {
        var ratio = sceneCanvas.pixelRatio;
          sceneCanvas.width / sceneCanvas.pixelRatio,
          sceneCanvas.height / sceneCanvas.pixelRatio
        try {
          len = filters.length;

          // copy cached canvas onto filter context
            sceneCanvas.getWidth() / ratio,
            sceneCanvas.getHeight() / ratio
          imageData = filterContext.getImageData(

          // apply filters to filter context
          for (n = 0; n < len; n++) {
            filter = filters[n];
            if (typeof filter !== 'function') {
                'Filter should be type of function, but got ' +
                  typeof filter +
                  ' instead. Please check correct filters'
  , imageData);
            filterContext.putImageData(imageData, 0, 0);
        } catch (e) {
            'Unable to apply filter. ' +
              e.message +
              ' This post my help you'

        this._filterUpToDate = true;

      return filterCanvas;

How do you draw the filter effect? Special processing is carried out in konva. If filtercanvas exists, cachecanvas will not be used, that is, the off screen canvas originally used for caching will be replaced by filtercanvas.

Finally, filtercanvas will be drawn onto the scene canvas by means of DrawImage.


Konva implements a selector to facilitate us to quickly find a shape. At present, there are three kinds of selectors: ID selector, name selector and type selector.

The first two need to pass in an ID or name attribute when instantiating, and the latter is found according to the class name (rect, line, etc.).

When the selector searches, it needs to call the find method, which is mounted on the container class. It calls_ Descendants traverses the child nodes, and calls the ismatch method to determine whether the node nodes traversed match.

_generalFind<ChildNode extends Node = Node>(
    selector: string | Function,
    findOne: boolean
  ) {
    var retArr: Array<ChildNode> = [];
    //Call_ Descendants gets all child nodes
    this._descendants((node: ChildNode) => {
      const valid = node._isMatch(selector);
      if (valid) {
      //If it is findone, the following will not continue
      if (valid && findOne) {
        return true;
      return false;

    return retArr;
  private _descendants(fn: (n: Node) => boolean) {
    let shouldStop = false;
    const children = this.getChildren();
    for (const child of children) {
      shouldStop = fn(child);
      if (shouldStop) {
        return true;
      if (!child.hasChildren()) {
      //If the child node also has child nodes, it is traversed recursively
      shouldStop = (child as any)._descendants(fn);
      //If you should stop searching (generally, you don't need to find the following when findone)
      if (shouldStop) {
        return true;
    return false;

In ismatch, you can see that the matching is performed according to the type of selector.

      // id selector
      if (sel.charAt(0) === '#') {
        if ( === sel.slice(1)) {
          return true;
      } else if (sel.charAt(0) === '.') {
        // name selector
        if (this.hasName(sel.slice(1))) {
          return true;
      } else if (this.className === sel || this.nodeType === sel) {
        return true;


Konva also supports the serialization and deserialization of stage. In short, it is to export the stage data into a JSON data and import the JSON data to facilitate the server-side rendering on the nodejs side.

Serialization is mainly in the toobject method. It will filter functions and DOM nodes, and only retain a description information, such as layer information, shape information, etc., which is somewhat similar to the virtual DOM in react.

toObject() {
    var obj = {} as any,
      attrs = this.getAttrs(),

    obj.attrs = {};

    for (key in attrs) {
      val = attrs[key];
      nonPlainObject =
        Util.isObject(val) && !Util._isPlainObject(val) && !Util._isArray(val);
      if (nonPlainObject) {
      getter = typeof this[key] === 'function' && this[key];
      delete attrs[key];
      //A special processing function that mounts the result to the current key after its execution
      defaultValue = getter ? : null;
      // restore attr value
      attrs[key] = val;
      if (defaultValue !== val) {
        obj.attrs[key] = val;

    obj.className = this.getClassName();
    return Util._prepareToStringify(obj);

Deserialization is to parse the incoming JSON information, create different objects according to the classname, recurse the deep structure, and then add it to the parent node.

static _createNode(obj, container?) {
    var className =,
      children = obj.children,

    // if container was passed in, add it to attrs
    if (container) {
      obj.attrs.container = container;

    if (!Konva[className]) {
        'Can not find a node with class name "' +
          className +
          '". Fallback to "Shape".'
      className = 'Shape';
    //Instantiate based on the passed in classname
    const Class = Konva[className];

    no = new Class(obj.attrs);
    if (children) {
      len = children.length;
      for (n = 0; n < len; n++) {
        //If there are child nodes, create them recursively

    return no;


Instead of repackaging components, konva and react bind adopt the same form as react Dom and react native, and implement a set of hostconfig based on react reconciler to customize their own host component.


After the birth of the react fiber architecture, they separated the original react core code. It mainly includes three parts: react, react reconciler and platform implementation (react DOM, react native, etc.).

The famous diff algorithm, time slicing, scheduling and so on are implemented in react reconciler. It also exposes us a hostconfig file, which allows us to realize our own rendering in various hook functions.

In react, there are two component types: host component and composition component.

In DOM, the former is H1, div, span and other elements. In react native, the former is view, text, Scrollview and other elements. The latter is our customized components based on the host component, such as app, header, etc.

In react reconciler, it allows us to customize the rendering of the host component (add, delete, check and modify), which also means cross platform capability. We only need to write a hostconfig file to realize our own rendering.

Analysis of konvajs principle

Referring to the above architecture diagram, you will find that it can be rendered to native, canvas, or even small programs. There are already solutions in the industry based on this. You can refer to REMAX of ant financial: [REMAX – build applets using real react


The main implementation of react konva is in reactkonvahostconfig.js. It uses konva’s original API to realize the mapping of virtual Dom and responds to the addition, deletion and modification of virtual dom.

Part of the source code is extracted here:

//Create an instance
export function createInstance(type, props, internalInstanceHandle) {
  let NodeClass = Konva[type];

  const propsWithoutEvents = {};
  const propsWithOnlyEvents = {};

  for (var key in props) {
    var isEvent = key.slice(0, 2) === 'on';
    if (isEvent) {
      propsWithOnlyEvents[key] = props[key];
    } else {
      propsWithoutEvents[key] = props[key];
  //Create an instance based on the passed in type, which is equivalent to new layer, new rect, etc
  const instance = new NodeClass(propsWithoutEvents);
  //Set the incoming props to the instance
  //If it is an ordinary prop, update it directly through instance.setattr
  //If it is an event such as onclick, it is bound through instance. On
  applyNodeProps(instance, propsWithOnlyEvents);

  return instance;
//Insert the child node and directly call konva's add method
export function appendChild(parentInstance, child) {
  if (child.parent === parentInstance) {
  } else {


//Remove the child node and call the destroy method directly
export function removeChild(parentInstance, child) {

//InsertBefore is implemented by setting zindex
export function insertBefore(parentInstance, child, beforeChild) {
  // child._remove() will not stop dragging
  // but child.remove() will stop it, but we don't need it
  // removing will reset zIndexes


On Vue, konva registers a plug-in through vue.use, which registers each component separately.

const components = [
    name: 'Stage',
    component: Stage
  }, => ({
    component: KonvaNode(name)
const VueKonva = {
  install: (Vue, options) => {
    let prefixToUse = componentPrefix;
    if(options && options.prefix){
      prefixToUse = options.prefix;
    components.forEach(k => {
      Vue.component(`${prefixToUse}${}`, k.component);

export default VueKonva;

if (typeof window !== 'undefined' && window.Vue) {

Let’s look at the implementation of konvanode. In konvanode, the addition, deletion, query and modification of nodes are implemented in the life cycle of Vue.
In the created life cycle of Vue, call initkonva to create a new nodeclass, which is almost the same as the react method above.

      initKonva() {
        const NodeClass = window.Konva[nameNode];

        if (!NodeClass) {
          console.error('vue-konva error: Can not find node ' + nameNode);

        this._konvaNode = new NodeClass();
        this._konvaNode.VueComponent = this;


When updating, update props and destroy nodes in destroyed, which makes the implementation more concise.

    updated() {
      checkOrder(this.$vnode, this._konvaNode);
    destroyed() {


Dirty rectangle

In terms of performance, konva still doesn’t see enough libraries compared with Pixi and zrender. If we have a lot of shapes on the layer, if we want to update a shape, it will still be painted in full according to konva’s implementation.

Although konva supports single shape redrawing, it is implemented without brain covering the original position, which also means that if your shape is under other node shapes, there will be problems.

Therefore, there is a lack of very important local update capability, that is, what we often call dirty rectangles.

Dirty rectangle means that when we update a shape, we use collision detection to calculate all shapes intersecting it, merge them, and calculate a dirty area. Then we use clip to restrict canvas to only draw in this dirty area, so as to realize local update.

Analysis of konvajs principle

Unfortunately, konva’s bounding box is very simple to implement, which is not suitable for collision detection, and it does not provide the ability of dirty rectangles.