Integrating Tiptap performantly in your app
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.