tiptap: editor.isActive() very slow - freezes UI for 2-3 seconds on selectAll()
Description For ~3000 lines doc, editor.isActive() takes a long time on selectAll() isActive(‘image’‘) took: 119.39999997615814ms Editor.svelte:186 isActive(‘bold’’) took: 14.299999952316284ms Editor.svelte:186 isActive(‘italic’‘) took: 9.5ms Editor.svelte:186 isActive(‘strike’’) took: 9.300000011920929ms Editor.svelte:186 isActive(‘paragraph’‘) took: 113ms Editor.svelte:186 isActive(‘heading’’) took: 110.19999998807907ms Editor.svelte:186 isActive(‘heading’‘) took: 111.10000002384186ms Editor.svelte:186 isActive(‘heading’’) took: 112.5ms Editor.svelte:186 isActive(‘bulletList’‘) took: 113.29999995231628ms Editor.svelte:186 isActive(‘orderedList’’) took: 112.19999998807907ms Editor.svelte:186 isActive(‘code’‘) took: 7.399999976158142ms Editor.svelte:186 isActive(‘codeBlock’’) took: 109.80000001192093ms Editor.svelte:186 isActive(‘blockquote’‘) took: 110.30000001192093ms Editor.svelte:186 isActive(‘tableOfContents’’) took: 110.19999998807907ms Editor.svelte:186 isActive(‘date’‘) took: 111.59999996423721ms Editor.svelte:186 isActive(‘link’’) took: 7.899999976158142ms
Steps to reproduce the bug
- Setup a toolbar for the editor, similar to examples on tiptap.dev:
let ed = {
isActive: (name: string, attributes?: {}) => {
if (editor == null) {
return false;
}
const start = performance.now();
let result: boolean = editor.isActive(name, attributes);
const end = performance.now();
console.log(`isActive('${name}'') took: ${end - start}ms`);
return result;
},
};
...
.toolbar
button(on:click="{addImage}" class:active="{ed.isActive('image')}")
.button-fill.icon(title="image") image
button(on:click="{editor.chain().focus().toggleBold().run()}" class:active="{ed.isActive('bold')}"
)
.button-fill.icon(title="bold") bold
button(on:click="{editor.chain().focus().toggleItalic().run()}" class:active="{ed.isActive('italic') }"
)
.button-fill.icon(title="italic") italic
button(on:click="{editor.chain().focus().toggleStrike().run()}" class:active="{ed.isActive('strike') }"
)
.button-fill.icon(title="strikethrough") strikethrough
//- button(on:click="{editor.chain().focus().unsetAllMarks().run()}"
//- )
//- .button-fill.icon(title="clear formatting") clear-formatting
//- button(on:click="{editor.chain().focus().clearNodes().run()}"
//- )
//- .button-fill clear nodes
.separator
button(on:click="{editor.chain().focus().setParagraph().run()}" class:active="{ed.isActive('paragraph') }"
)
.button-fill(title="normal text") normal
button(on:click="{editor.chain().focus().toggleHeading({ level: 1 }).run()}" class:active="{ed.isActive('heading', { level: 1 }) }"
)
.button-fill.icon(title="heading 1") h1
button(on:click="{editor.chain().focus().toggleHeading({ level: 2 }).run()}" class:active="{ed.isActive('heading', { level: 2 }) }"
)
.button-fill.icon(title="heading 2") h2
button(on:click="{editor.chain().focus().toggleHeading({ level: 3 }).run()}" class:active="{ed.isActive('heading', { level: 3 }) }"
)
.button-fill.icon(title="heading 3") h3
//- button(on:click="{editor.chain().focus().toggleHeading({ level: 4 }).run()}" class:active="{ed.isActive('heading', { level: 4 }) }"
//- )
//- .button-fill.icon h4
//- button(on:click="{editor.chain().focus().toggleHeading({ level: 5 }).run()}" class:active="{ed.isActive('heading', { level: 5 }) }"
//- )
//- .button-fill.icon h5
//- button(on:click="{editor.chain().focus().toggleHeading({ level: 6 }).run()}" class:active="{ed.isActive('heading', { level: 6 }) }"
//- )
//- .button-fill.icon h6
.separator
button(on:click="{editor.chain().focus().toggleBulletList().run()}" class:active="{ed.isActive('bulletList') }"
)
.button-fill.icon(title="list") list2
button(on:click="{editor.chain().focus().toggleOrderedList().run()}" class:active="{ed.isActive('orderedList') }"
)
.button-fill.icon(title="ordered list") list-numbered
button(on:click="{editor.chain().focus().toggleCode().run()}" class:active="{ed.isActive('code') }"
)
.button-fill.icon(title="inline code") code
button(on:click="{editor.chain().focus().toggleCodeBlock().run()}" class:active="{ed.isActive('codeBlock') }"
)
.button-fill.icon(title="code block") code2
button(on:click="{editor.chain().focus().toggleBlockquote().run()}" class:active="{ed.isActive('blockquote') }"
)
.button-fill.icon(title="quote") quotes-left
.separator
button(on:click="{editor.chain().focus().setHorizontalRule().run()}"
)
.button-fill(title="horizontal line") hr
button(on:click="{editor.chain().focus().insertContent({type: 'tableOfContents'}).run()}" class:active="{ed.isActive('tableOfContents')}"
)
.button-fill(title="table of contents") toc
button(on:click="{editor.chain().focus().insertContent({type: 'date'}).run()}" class:active="{ed.isActive('date')}"
)
.button-fill.icon(title="today's date and time") date
//- button(on:click="{editor.chain().focus().setHardBreak().run()}"
//- )
//- .button-fill.icon(title="page break") pagebreak
.separator
button(on:click="{setLink}" class:active="{ed.isActive('link') }"
)
.button-fill.icon(title="add link") link
button(on:click="{editor.chain().focus().unsetLink().run()}" class:active="{ed.isActive('link') }"
)
.button-fill.icon(title="delete link") -link
button(on:click="{editor.chain().focus().undo().run()}"
)
.button-fill.icon(title="undo") undo
button(on:click="{editor.chain().focus().redo().run()}"
)
.button-fill.icon(title="redo") redo
- Add a lot of text in editor - 3000-5000 lines
- Press Ctrl+A to select all
- Editor freezes for 2+ seconds in isActive() Timings:
isActive('image'') took: 119.39999997615814ms
Editor.svelte:186 isActive('bold'') took: 14.299999952316284ms
Editor.svelte:186 isActive('italic'') took: 9.5ms
Editor.svelte:186 isActive('strike'') took: 9.300000011920929ms
Editor.svelte:186 isActive('paragraph'') took: 113ms
Editor.svelte:186 isActive('heading'') took: 110.19999998807907ms
Editor.svelte:186 isActive('heading'') took: 111.10000002384186ms
Editor.svelte:186 isActive('heading'') took: 112.5ms
Editor.svelte:186 isActive('bulletList'') took: 113.29999995231628ms
Editor.svelte:186 isActive('orderedList'') took: 112.19999998807907ms
Editor.svelte:186 isActive('code'') took: 7.399999976158142ms
Editor.svelte:186 isActive('codeBlock'') took: 109.80000001192093ms
Editor.svelte:186 isActive('blockquote'') took: 110.30000001192093ms
Editor.svelte:186 isActive('tableOfContents'') took: 110.19999998807907ms
Editor.svelte:186 isActive('date'') took: 111.59999996423721ms
Editor.svelte:186 isActive('link'') took: 7.899999976158142ms

Expected behavior I expect Ctrl+A to be instant
Environment?
- operating system: Win10
- browser: Edge Chromium
- mobile/desktop: desktop
- tiptap version: 2
Additional context Issue seems related to nodeBetween/textBetween
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Comments: 27 (14 by maintainers)
Commits related to this issue
- fix: improve performance for isActive method, see #1930 — committed to ueberdosis/tiptap by philippkuehn 3 years ago
I implemented the debouncing solution recommended by @sereneinserenade and it works now with any issues at all, there is this 1-second lag for status, but no issues at all.
It is basically similar to what I suggested initially, having an object or whatever that holds the statuses for nodes and marks.
I have encountered the same problem with the performance of isActive.
Another performance bottleneck are the methods this.$emit(‘input’, this.editor.getHTML()) and the CharacterCount extension.
The performance problems only occur with very long texts (especially when jumping to the end of the text) and with Vue 2. Vue 3 may be faster, so these bottlenecks don’t come into play.
But the general solution was the reference to the debounce() functions. Thx to @sereneinserenade
I debounced() all methods accordingly. Editing now runs absolutely smooth (Vue 2, Very long texts).
I’ve released some changes that makes isActive a lot faster in my local tests. But I think it’s still not enough for your needs. Can you please test again (with benchmarks) with the latest version of
@tiptap/core?I have written my own method isActive
isActive(value) { if (this.activeStatuses.includes(value)) { return true; } },In another updateButtons method i push the value in an array
if (this.editor.isActive('bold')) { this.activeStatuses.push('bold'); }The update method is called in the editor.on(“selectionUpdate”
const updateButtons = debounce(() => this.updateButtons(), 100); updateButtons();Works perfect. I tested this with the Book Example (>200.000 words).
@nokola I had the same issue, but with Vue 2. Since I didn’t know the solution, I added a
deboucefrom lodash(750ms) before checking the active state of every extension and now it works much better. The only catch is that the latest active state gets reflected on the menubar after ~750ms, but for us it was fine. if needed, I can provide with a codesandbox.