Integrating Tiptap performantly in your app

Editor

Tiptap is a very performant editor (even able to edit an entire book!), often when you run into performance issues, it's not Tiptap itself, but the way you integrate it into your app. Here are some tips to make sure your editor runs smoothly.

React Tiptap Editor Integration

When using Tiptap with React, the most common performance issue is that the editor is re-rendered too often. This can happen for several reasons:

  • When using the useEditor hook, it by default will re-render the editor on every change. So, you should isolate the editor (and things that depend on it) in a separate component to prevent unnecessary re-renders.
  • The editor should be isolated from renders that don't affect it. For example, if you have a sidebar that doesn't interact with the editor, it should be in a separate component.

Luckily, the solution for most of these issues is the same: isolate the editor in a separate component. Here is an example of how you can do this:

DO: isolate the editor in a separate component

import { EditorContent, useEditor } from '@tiptap/react'

const TiptapEditor = () => {
  const editor = useEditor({
    extensions,
    content,
  })

  return (
    <>
      <EditorContent editor={editor} />
      {/* Other components that depend on the editor instance */}
      <MenuComponent editor={editor} />
    </>
  )
}

export default TiptapEditor

DON'T: render the editor in the same component as other components

import { EditorContent, useEditor } from '@tiptap/react'

const App = () => {
  const [sidebarOpen, setSidebarOpen] = React.useState(false)
  const editor = useEditor({
    extensions,
    content,
  })

  return (
    <>
      <UnrelatedSidebar onChange={setSidebarOpen} />
      <EditorContent editor={editor} />
      <MenuComponent editor={editor} />
      <Sidenav isSidebarOpen={sidebarOpen}>
        <AnotherComponent />
      </Sidenav>
    </>
  )
}

export default App

These unrelated components will cause the editor to re-render more often than necessary, and make each render more expensive.

Gain more control over rendering

As of Tiptap v2.5.0, you can gain more control over rendering by using the immediatelyRender and shouldRerenderOnTransaction options. This can be useful if you want to prevent the editor from rendering immediately or on every transaction.

import { useEditor } from '@tiptap/react'
import deepEqual from 'deep-equal'

function Component() {
  const editor = useEditor({
    extensions,
    content,
    /**
     * This option gives us the control to enable the default behavior of rendering the editor immediately.
     */
    immediatelyRender: true,
    /**
     * This option gives us the control to disable the default behavior of re-rendering the editor on every transaction.
     */
    shouldRerenderOnTransaction: false,
  })

  const editorState = useEditorState({
    editor,
    // This function will be called every time the editor state changes
    selector: (editorInstance: Editor) => ({
      // It will only re-render if the bold or italic state changes
      isBold: editorInstance.isActive('bold'),
      isItalic: editorInstance.isActive('italic'),
    }),
    // This function will be used to compare the previous and the next state
    equalityFn: deepEqual,
  })

  return (
    <>
      <EditorContent editor={editor} />
      <button
        onClick={() => editor.chain().focus().toggleBold().run()}
        className={editorState.isBold ? 'primary' : ''}
      >
        Bold
      </button>
      <button
        onClick={() => editor.chain().focus().toggleItalic().run()}
        className={editorState.isItalic ? 'primary' : ''}
      >
        Italic
      </button>
    </>
  )
}

React NodeView Integration

While NodeViews with React are supported, if you are using them in your editor, you should be aware that they can be expensive to render.

If you want the absolute best performance, your NodeViews ideally would not be rendered by React. Instead you could use direct DOM manipulation to render them. This is because React is not optimized for rendering synchronously and NodeViews are expected to be rendered synchronously. This is especially important if you have several instances of NodeViews throughout your editor.