Performance issues with `nodesBetween`

For calculating active states of menu buttons (like checking for h1, h2, etc.) I’m iterating over nodes within the current text selection on every keystroke.

Basically something like this:

const { from, to } = state.selection

state.doc.nodesBetween(from, to, (node, pos) => {
  // ...

This works great for “normal” sized documents. But for large documents this is pretty slow. In my test for a document with 5000 nodes this takes about 250ms.

I’m experimenting with a custom nodesBetween method which is searching within a sliced array of doc.content.content.

function fastNodesBetween(
  doc: ProseMirrorNode,
  from: number,
  to: number,
  fn: (
    node: ProseMirrorNode,
    pos: number,
    parent: ProseMirrorNode,
    index: number,
  ) => boolean | void | null | undefined,
): void {
  const $from = doc.resolve(from)
  const $to = doc.resolve(to)
  const startPos = $from.start(1)
  const fromOffset = from - startPos
  const toOffset = fromOffset + to - from
  const fromIndex = $from.index(0)
  const toIndex = $to.index(0)

  // `doc.content.content` is not public
  // @ts-ignore
  const nodes = (doc.content.content as ProseMirrorNode[]).slice(fromIndex, toIndex + 1)

    .nodesBetween(fromOffset, toOffset, (node, pos, parent, index) => {
      pos = $from.before(1) + pos
      index = parent
        ? index
        : fromIndex + index
      parent = parent || doc

      return fn(node, pos, parent, index)

In my tests this runs in about 1ms instead of 250ms which is great.

Searching within doc.slice or state.doc.content.cut didn’t help for performance. That’s why I’m searching within the private doc.content.content which isn’t optimal I think.

But maybe there is another way I missed to efficiently search nodes within the current selection?

Could you provide an example doc + nodesBetween call that produces that kind of slowdown? In a document with 5000 nodes, unless something expensive is happening the callback or something is going really wrong, nodesBetween should still take less than a millisecond.

Ok, after further investigation it seems to be related to the implementation in Vue 2 and its reactivity system.