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

  1. 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

  1. Add a lot of text in editor - 3000-5000 lines
  2. Press Ctrl+A to select all
  3. 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

image

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

Most upvoted comments

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?

Finally, I went that far that I completely removed isActive from the MenuBar, however, I want to migrate to vue-3 (and vuetify) in Q1 and for the time-being we can’t live without the “dancing” formats (tough we miss it a bit).

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 debouce from 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.