Why selectedCell decoration so slow in large document

I noticed this as well and have a patch for this which speeds up compute time greatly for large table nodes. It essentially debounces a particular handler for column resizing which is triggered a lot when user interacts with table (mouse over)

I will try to open a pull request soon and maybe it will help you but let me know if you are using column resizing or not.

There might be limitations to my implementation so I am curious what the other authors think.

1 Like

Again, this just targets the mousemove handler which fires too often for larger table nodes (if you used column resizing). If you want to copy this in to your node_modules you can give it a try quickly.

Edit: Opened a PR - Improve performance of column resizing mousemouse handler by debouncing by bZichett · Pull Request #154 · ProseMirror/prosemirror-tables · GitHub

Yea, I used column resizing, but it’s still slow after the column resizing is disabled.

I’d love to help you out still :+1:.

See if you can find where in the plugin most computation time is happening and we’ll see if we can work on some improvements. Let me know if you haven’t used a javascript profiler yet though as I don’t know your experience.

I started with column resizing only because it decimated performance with that one handler for large tables (but the PR is just reducing transaction on mouse events not table selections which require expensive ones that might be throttled as well in a similar manner).

There are likely a few more hot code paths that could be optimized beyond this as well.

I found the reason, when the tableEditing create the selectedCell decorations,it will enter to iterDeco function. the iterDeco function speed up compute time greatly. when it forChild, the condition result == empty is not true, even though the result is empty, but they are not the same variable, which will cause foreachall nodes of document(large document will speed up compute time greatly)

 DecorationGroup.prototype.forChild = function forChild (offset, child) {
  if (child.isLeaf) { return DecorationSet.empty }
  var found = [];
  for (var i = 0; i < this.members.length; i++) {
    var result = this.members[i].forChild(offset, child);
    if (result == empty) { continue }
    if (result instanceof DecorationGroup) { found = found.concat(result.members); }
    else { found.push(result); }
  }
  return DecorationGroup.from(found)
}

So, I copy the tableEditing code from prosemirror-tables into my project. then it’s solved. I’m not sure this is a bug of prosemirror-view?

the following is a part of tableEditing code

export function tableEditing({ allowTableNodeSelection = false } = {}) {
  return new Plugin({
    key: tableEditingKey,
    state: {
      init() {
        return null;
      },
      apply(tr, cur) {
        let set = tr.getMeta(tableEditingKey);
        if (set != null) return set == -1 ? null : set;
        if (cur == null || !tr.docChanged) return cur;
        let { deleted, pos } = tr.mapping.mapResult(cur);
        return deleted ? null : pos;
      },
    },

    props: {
      decorations: drawCellSelection,

      handleDOMEvents: {
        mousedown: handleMouseDown,
      },

      createSelectionBetween(view) {
        if (tableEditingKey.getState(view.state) != null)
          return view.state.selection;
      },

      handleTripleClick,

      handleKeyDown,

      handlePaste,
    },

    appendTransaction(_, oldState, state) {
      return normalizeSelection(
        state,
        fixTables(state, oldState),
        allowTableNodeSelection,
      );
    },
  });
}

export function drawCellSelection(state) {
  if (!(state.selection instanceof CellSelection)) return null;
  let cells = [];
  state.selection.forEachCell((node, pos) => {
    cells.push(
      Decoration.node(pos, pos + node.nodeSize, { class: 'selectedCell' }),
    );
  });
  return DecorationSet.create(state.doc, cells);
}

Ah I see. There is another handler that cannot keep up with large tables atm so for the time being, I’d like to test this approach which I just finished up now. It throttles the mousemove during cell selection changes. Again a little hit to the UX but required at the moment for a functional user experience.

Copy over the two core files to test it out (only two files that changed that you need to swap into node_modules, the rest are tests and demo updates)

Testing a public exposed npm module here: @projectample/prosemirror-tables

If you are using webpack, alias works well

  'prosemirror-tables': '@projectample/prosemirror-tables',

Its better but still not quite there. Still investigating but feel free to try that out. Seeming more like a memory leak in tableEditing, since the first interaction for dragging works well but then the next one slows down. This also happens when there are multiple tables in the same document of medium / small size (dozens)

How do I know this: I disabled all the decorations and plugins of the EditorView (except tableEditing) and the performance was OK but only for the first few interactions (anything, including drags) but then it still degraded completely to the same as before. So its likely not caused by decorations but exacerbated severely if there are a lot of them.

@jiaxiaxia Check a browser different than Chrome (or let me know what browser you are using)

Might be related to DOMObserver usage. It appears that Chromes javascript profiler is obfuscating whatever is long running.

Ok, finally reporting something interesting; the text in the table I was working with had spelling corrections pending, and it appears Google chromes implementation has to refire on every interaction.

I am aware of some threads about spellcheck but I was not aware of the performance ramifications being this deep. Is is possible your table is filled with non-English characters that your browser is attempting to correct as well?

Performance stabilized by adding the spellcheck=false attribute to just the

element.

@marijn Thoughts on this? I reached out to a prosemirror-table author and he said he wasnt working on it anymore; Been trying to improve performance there (2 open PR’s) but this spell check one might be heavily effecting non english Chrome browser users more than anything. Im not sure if its related to prosemirror-tables and its DOMObserver usage or a core prosemirror library

Edit: I see this comment now - What is the current state of spellcheck? - #11 by marijn

I am not planning to maintain prosemirror-tables either. It’d be great if a new maintainer can be found, but it’s not going to be me.

I’m not sure what you mean by DOMObserver usage and how that is related to this. Turning spell-checking off is an option sites have, but not something I want to make the library do by default.

Well, one thing I know for sure is that a large table filled with German language that triggers chromes spell check causes massive performance issues. Its exacerbated by tables due to the more complex DOM structure, presuming.

There is definitely more room for figuring out some performance issues with prosemirror-tables though. I’ll be looking into this more.

As for the spellcheck attribute. I added a conditional attribute prop for when a large doc is received by editor state (sufficiently large > 10,000 to start.) Its fine not to have built in to prosemirror.

I used the chrome 95.0.4638.69,I didn’t used webpack, I used vite

@jiaxiaxia Does your document have a lot of text that is triggering chromes auto spell checker? (Or just an empty table cells?)

If it’s empty table cells, the debounce and throttle in the pull requests I have for prosemirror-tables should help a lot.

@marijn is it a bug of prosemirror-view ,or it’s just a result of package tools

Yea,it did help a lot, but it is not the main reason for me. my main proplem is Why selectedCell decoration so slow in large document - #5 by bZichett

I don’t know—I couldn’t make much of the iterDeco comments you made.

I’m sorry,the following is the iterDeco function

Again, spellcheck has been diabled

the third parameter of the onNode function will call deco.forChild. when forChild, the result is empty$4, but the empty is empty$1 after build ,so they are different.

Could it be that you’re loading multiple versions of prosemirror-view?

Ah, I made a mistake, the version is really different, I use resolutions to override the version, but I used ^ to install the prosemirror-view in my project.Thanks very much.