Responsivness improvements for rendering of a large set of decorations

During the implementation of the search & replace functionality, similar to the Ctrl+F feature in many other text editors, I noticed that even a relatively small document (around 30 thousand characters) can become momentarily unresponsive when a large set of decorations needs to be rendered at once.

The specific case occurred when a user typed a single character with a high occurrence rate, and we needed to highlight all the matches. A search for, say, the letter “a” can produce hundreds of decorations, and rendering them simultaneously can significantly slow down the editor, which becomes very noticeable during typing or other interactions.

This particular problem was mitigated with input debouncing at the UI level. However, larger documents may still require numerous decoration manipulations in different contexts, and input debouncing may not always help.

This whole issue raised the question of whether deferred or batched decoration rendering could be possible. I considered breaking down decoration rendering into multiple transactions, but the implementation is heavy and feels out of place within a plugin. It seems like this should instead be handled within ProseMirror itself. Perhaps rendering optimizations should be considered more generally.

I also looked into skipping the rendering of decorations that are not currently visible, but again, it feels like the ProseMirror library should implement that rather than individual plugins.

Does anyone have experience optimizing rendering for large articles? Is there a way to implement rendering optimizations such as batching, deferring, or skipping off-screen rendering in ProseMirror?

P.S.: You can check out the search and replace plugin implementation here:

My stupid suggestion is not to do anything when the user types a single character. It’s rarely the intention and the number of hits decreases exponentially as they type more. (At 30k characters, you have more than 1000 hits for the most common letters, which is about the point where modern browsers will start to have trouble. The goes down a lot with just two characters).

I think a more general solution would involve keeping around a list of blocks that currently intersect with the viewport, and ignoring everything else for the purposes of rendering decorations, NodeViews, etc. I won’t comment on whether that should be part of the core library, but at the level of brass tacks, everything is a DOM node.

Hey, Casey! Thanks for your comment! That’s exactly what was achieved with the user input debouncing. As a result, as you said, since users usually continue to type immediately after the first character, it is very rarely that we need to highlight matches for a single character. Yet, the question stands - would it be possible and useful to optimise rendering/responsiveness on the library level for the cases when there is a lot of decoration just because the document is large and there is no way to handle it with debouncing or other similar tactics.

I don’t think that would be a good idea at all. The library does what you ask it to do, and introducing further asynchronous magic there will complicate what you can expect of it (now you no longer know whether the decorations you provided were actually rendered). A plugin is generally in a better position to determine when and whether to display decorations.

My code editor library does viewporting (lazy rendering), and while that is useful for a code editor, it also hugely complicates both the library implementation and the programming model for client code. My judgement is still that for a library like ProseMirror, this would be too much to pay in extra download size and difficulty of programming against it.

I compared the speed of ProseMirror doing a monster highlight like this to what it would cost to just do it in raw DOM, and there’s no noticeable difference. So yeah, as you already mentioned, you’ll have to take responsibility for the amount of display changes you generate on the plugin size. Maybe only highlight the area around the selection initially, and do the rest of the document after the user has been idle for a given amount of time.

I see! Thanks Marijn!