Any tips for caching/improving performance of Decorations?

We use decorations to highlight words and basically add “real-time” links to text (e.g. annotations, spellcheck markers, etc.). This means that the plugin runs over the full text on all transactions that change the content in some way.

I came across Decorations performance - #5 by marijn which says to store the decorations in a plugin state, but even then: how do we handle cases where a user is just typing at the end of the text (or within a single paragraph) while the rest of the document isn’t any different?

Are there ways to basically say “from 0 - 1000 we can reuse, then update just the rest”? We already have a fast lookup of things, but some users still report performance issues in edge cases (or when there’s too much to go over, like 50k word documents).

thanks in advance!

Yes, you can do this with RangeSet.update. Determine which area you want to redraw, pass that range as filterFrom/filterTo with a filter that always returns false, and pass the newly drawn decorations in add. (You’ll want to make sure you map the set before updating, so that its positions actually correspond to the post-change document.)

Okay, cool! I am guessing that I can get the “affected range” from the transaction steps? Because I technically don’t know what the user was doing, so having a way to know which parts to focus on would be good.

I accidentally linked to the CodeMirror docs there. In ProseMirror, the methods are add and remove.

You can inspect a transactions step maps under tr.mapping, and compute the changed range from those.

1 Like

I am using this

/**
 * Returns a range of the edited blocks
 * @param oldDoc
 * @param newDoc
 * @returns
 */
export function getEditedBlocksRange(
  oldDoc: PMNode,
  newDoc: PMNode,
  depth = 1
): [number, number] | undefined {
  const from = oldDoc.content.findDiffStart(newDoc.content)
  const to = oldDoc.content.findDiffEnd(newDoc.content)?.b
  if (from === null || to === undefined) return undefined
  let start = newDoc.resolve(from)
  if (start.depth === 0) {
    start = newDoc.resolve(Math.min(from + 1, newDoc.nodeSize - 2))
  }
  let end = newDoc.resolve(to)
  if (end.depth === 0) {
    end = newDoc.resolve(Math.max(to - 1, 0))
  }
  return [start.before(depth), end.after(depth)]
}

to get the range of edited block nodes to recompute everything within it.

1 Like

I wonder what’s faster?

  1. diffing the document (using the solution by @TeemuKoivisto above)
  2. re-applying just all decorations, always (could be, depending on the code)
  3. using the step maps as @marijn described above, and find out the range that way.