Plugin performance - Problems when caching Decoration.widget DOM

I have a plugin that gets all the users who have written in a line and draws a circle with their initials beside the line.

image

This was working fine but the problem is that it gets slow for big documents. The main problem is that for every keystroke it builds again the DOM for the decorations widgets.

    private getDecorationWidgets(lines: Line[]){
        return lines.map(line => Decoration.widget(line.firstCharProseMirrorPosition, line.lineAuthorsDOM, {side: -1}) );
    }

so I tried to optimize it by saving the lines with the authors DOM in the plugin state and at every key stroke I only build the DOM for the new lines, reusing the DOM created for the old ones.

if(oldPluginState.decorations === DecorationSet.empty || tr.docChanged){
	const lines = this.getAllCanvasLines(authorTracking, newEditorState.doc);

	const linesWithDOM = this.getLinesWithDOM(lines, oldPluginState.lines);

	const decorations = this.getDecorationWidgets(linesWithDOM);

	newPluginState = {
	    decorations: DecorationSet.create(newEditorState.doc, decorations),
	    lines: linesWithDOM
	};
}     

.....

    private getLinesWithDOM(lines: Line[], oldLinesWithDOM: Line[]){
        return lines.map(currentLine => {
            const oldLine = oldLinesWithDOM.find(line => this.equals(currentLine, line));
            if(oldLine){
                currentLine.lineAuthorsDOM = oldLine.lineAuthorsDOM;
            }else{
                currentLine.lineAuthorsDOM = this.buildComponent(currentLine.userIds);
            }
            return currentLine;
        });
    }

This also was working fine but now for some documents I get this error:

TypeError: Cannot read property ‘nextSibling’ of null at rm (http://localhost:8080/vendor.bundle.js:59944:17) at renderDescs (http://localhost:8080/vendor.bundle.js:59839:39) at NodeViewDesc.renderChildren (http://localhost:8080/vendor.bundle.js:59650:5) at NodeViewDesc.updateChildren (http://localhost:8080/vendor.bundle.js:59646:64) at NodeViewDesc.updateInner (http://localhost:8080/vendor.bundle.js:59668:33) at NodeViewDesc.update (http://localhost:8080/vendor.bundle.js:59660:10) at ViewTreeUpdater.updateNextNode (http://localhost:8080/vendor.bundle.js:60042:14) at http://localhost:8080/vendor.bundle.js:59636:17 at iterDeco (http://localhost:8080/vendor.bundle.js:60098:7) at NodeViewDesc.updateChildren (http://localhost:8080/vendor.bundle.js:59623:5) at NodeViewDesc.updateInner (http://localhost:8080/vendor.bundle.js:59668:33) at NodeViewDesc.update (http://localhost:8080/vendor.bundle.js:59660:10) at ViewTreeUpdater.updateNextNode (http://localhost:8080/vendor.bundle.js:60042:14) at http://localhost:8080/vendor.bundle.js:59636:17 at iterDeco (http://localhost:8080/vendor.bundle.js:60098:7) at NodeViewDesc.updateChildren (http://localhost:8080/vendor.bundle.js:59623:5) at NodeViewDesc.updateInner (http://localhost:8080/vendor.bundle.js:59668:33) at NodeViewDesc.update (http://localhost:8080/vendor.bundle.js:59660:10) at ViewTreeUpdater.updateNextNode (http://localhost:8080/vendor.bundle.js:60042:14) at http://localhost:8080/vendor.bundle.js:59636:17 at iterDeco (http://localhost:8080/vendor.bundle.js:60098:7) at NodeViewDesc.updateChildren (http://localhost:8080/vendor.bundle.js:59623:5) at NodeViewDesc.updateInner (http://localhost:8080/vendor.bundle.js:59668:33) at NodeViewDesc.update (http://localhost:8080/vendor.bundle.js:59660:10) at EditorView.updateState (http://localhost:8080/vendor.bundle.js:5083:25) at CanvasComponent.handleCanvasStateChange

I assume that because I am reusing the DOM somehow ProseMirror tries to delete it more than once, it makes sense but it is funny that most part of the times it doesn’t happen.

How can I make my plugin fast without running into this problem ?

The update code will consider decorations with the same DOM element and the same position to be the same decoration, and leave it in place rather than updating it. Though in principle that should not be a problem – leaving it in place would be the proper behavior here. Can you try to reduce this to the minimal code that triggers the problem so that I can debug it?

I’ll see if I can do that. Another problem is that if the widget can only be cached for a position this optimization will not work when the user is editing the beginning of a big document because all positions after the point where the user is editing will change.

I am not sure if there is a way around this ? What I have done for now is to record the timestamp I calculate the lines and I only calculate it again if more than 2 seconds have passed.

Is there any reason you’re recomputing the whole widget set rather than mapping the old one and making only the necessary adjustments?

hmm, I am not really sure how to do the mapping so that after a doc changes I know that the character that was at position 7 is now at position 8 so the decoration {from: 7, to: 7, widget: … } should be mapped to {from: 8, to: 8, widget: … }.

That’s what DecorationSet.map will take care of for you.

Ah, that’s the method I was missing, thanks!