Making React ProseMirror really, really fast

Hey folks! I finally wrote up a post about the performance improvements we made to React ProseMirror last year. In both writing this post and getting feedback from folks trying out the demo editors, I’ve learned some more interesting things about browser contenteditable performance:

  • I didn’t really account for how much faster React’s production build is than the development build. I basically can’t type fast enough on my phone (Android or iPhone) to even notice the lag on the unmemoized editor
  • Firefox on macOS is just crazy slow for large documents, basically no matter what. Just putting Moby Dick in a plain contenteditable is slow, putting it in the unmemoized editor is slow, and putting it in the memoized editor is best but still pretty slow
  • Firefox on Linux is also pretty slow (not as bad as macOS) with plain contenteditables, but using beforeinput makes it much faster. The demo editor at React-ProseMirror Demo is much faster than than the demo editor at https://prosemirror.net for very large documents, but that’s also true of just putting Moby Dick in a plain contenteditable in Firefox.
  • Safari on macOS is crazy fast haha. I couldn’t discern any lag at all on the unmemoized editor when I was testing on my m4 Mac Mini
1 Like

Firefox on macOS is just crazy slow for large documents

Strange; I noticed it’s much faster than Chrome for large documents

I just compared with the ECMA standard and confirmed https://262.ecma-international.org/16.0/index.html.

I probably shouldn’t have stated that so confidently: it seems to vary very widely across OSes, OS versions, and browsers. Maybe even CPU architectures? But I put Moby Dick in a plain contenteditable on an otherwise empty page on Firefox on macOS 26 on a Mac Mini m4, and it was extremely slow to type in!

Very nice article. I’m curious: have you experimented at all with the react-compiler project? I always thought that the marriage of ProseMirror’s persistent data structures and memoized React components was a good match.

I wonder if you see any advantages to performing the equivalent of useMemo within the components that people write against react-prosemirror implementations, or if shipping a pre-compiled version of the library has any advantages over your (carefully) hand-optimized implementation.

Thanks! Yes, I’ve used the React compiler a bit — we use it for Storyteller’s web and mobile apps.

Normally, the React compiler seems like it does better than a developer can/will with just useMemo. In our case, there were two optimizations that the React compiler wouldn’t have made, which make a very big difference in performance for text editing:

  1. The getPos example that I elaborate on in the post, which technically violates a rule of React (it updates a ref during render). We decided that this was acceptable because we couldn’t come up with any way that it could actually cause an issue, but if we do run into any issues with it we’ll have to rethink it.
  2. We don’t actually just do a simple memo for node view components. We have our own equality function that we use to decide whether to re-render a child node view or not. This is the same equality check that prosemirror-view uses. It technically doesn’t re-render the component in some situations that React would normally want to (e.g. if the decorations’ positions have changed, but nothing else), but, again, we’re matching ProseMirror behavior here, rather than default React behavior.

There might still be value in running the react compiler (or manually memoizing) consumer-provided React node view components. React ProseMirror will make sure that a node view only re-renders if its content has actually changed, but if your node view is really expensive to re-render, that might still be too often! So you may need to, for example, separately memoize any inline interactive elements, like menus, that live within your node view components.

1 Like