-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
editor.isActive() very slow - freezes UI for 2-3 seconds on selectAll() #1930
Comments
@nokola I had the same issue, but with Vue 2. Since I didn't know the solution, I added a |
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 |
I’ve updated our book example. It loads moby dick now (with over 200,000 words) which runs fine for me. |
btw, the easy workaround for me - once selection exceeds 1000 chars, have isActive return false - works for my toolbar. let ed = {
isActive: (name: string, attributes?: {}) => {
if (editor == null) {
return false;
}
const selectionRanges: SelectionRange[] =
editor.state.selection.ranges;
// if selection is big, workaround TipTap perf bug with isActive https://github.com/ueberdosis/tiptap/issues/1930
if (
selectionRanges.length > 0 &&
selectionRanges[0].$to.pos - selectionRanges[0].$from.pos > 1000
) {
return false;
}
// const start = performance.now();
const result: boolean = editor.isActive(name, attributes);
// const end = performance.now();
// console.log(`isActive('${name}'') took: ${end - start}ms`);
return result;
},
}; Then I use |
Looks great now, thanks for the fix @philippkuehn. I see a different bug now - editor doesn't start because of gapcursor. I'll investigate and open separate bug.
(edit: above issue fixed by removing I commented the gapcursor code in node_modules and here are the new timings. Crazy how much the arrays were previously thrashing GC. Much better and > 300x faster now:
|
great! the other issue seems like a bug with your dependencies. try removing node_modules und lock file and do a fresh install. |
Not sure why this was closed, I updated all tiptap dependencies and I still have the issue. Having a text around 3.000 words with around 30-40 images, some bullet lists, around 30-40 H2, H3...laggs heavy unfortunately. |
@rezaffm do you use react, vue 2, vue 3? |
@rezaffm have you tried turning spellcheck off? In my case, it was the culprit and it's been working blazing fast after turning it off |
well, without spellcheck it is a bit difficult to write 3.000 words without any errors (at least for me) :-) by the way, can you share what you did with the debounce function? I tested it with vue-2 and vue-3. I use Tables, Images, External Videos, Iframes et cetera - but even when I copy over the moby dick example it crashes completely actually. I basically set up the menu exactly like here: https://tiptap.dev/examples/collaborative-editing But even if I try it only with buttons (just as in the moby dick example), it laggs. Only solution is to take out all the isActive() calls and then it works okay. |
@rezaffm here you go, I think you'll get the idea from the code I've shared, but still if you think I left something out, let me know. use of isActive with debounce and throttleUltimately I ended up using both I have an object which has info about which button is active or not. const editorFunctionsActiveStatuses = ref<Record<string, boolean>>({
h1: false,
h2: false,
h3: false,
h4: false,
h5: false,
bold: false,
italic: false,
underline: false,
strike: false,
textAlignLeft: false,
textAlignCenter: false,
textAlignRight: false,
textAlignJustified: false,
bulletList: false,
orderedList: false,
deleteTable: false,
addColumnBefore: false,
addColumnAfter: false,
deleteColumn: false,
addRowBefore: false,
addRowAfter: false,
deleteRow: false,
mergeCells: false,
splitCell: false,
toggleHeaderColumn: false,
toggleHeaderRow: false,
toggleHeaderCell: false,
}) and i update the value with debounce import { debounce, throttle } from 'lodash'
let lastContent = editor.getHTML()
const updateEditor = () => {
if (!editor || lastContent === editor.getHTML()) return
lastContent = editor.getHTML()
// do your thing here
}
export const editorFunctionsActiveSettings = {
h1: (editor): boolean => editor.isActive('heading', { level: 1 }),
h2: (editor): boolean => editor.isActive('heading', { level: 2 }),
h3: (editor): boolean => editor.isActive('heading', { level: 3 }),
h4: (editor): boolean => editor.isActive('heading', { level: 4 }),
h5: (editor): boolean => editor.isActive('heading', { level: 5 }),
bold: (editor): boolean => editor.isActive('bold'),
italic: (editor): boolean => editor.isActive('italic'),
underline: (editor): boolean => editor.isActive('underline'),
strike: (editor): boolean => editor.isActive('strike'),
textAlignLeft: (editor): boolean => editor.isActive({ textAlign: 'left' }),
textAlignCenter: (editor): boolean => editor.isActive({ textAlign: 'center' }),
textAlignRight: (editor): boolean => editor.isActive({ textAlign: 'right' }),
textAlignJustified: (editor): boolean => editor.isActive({ textAlign: 'justify' }),
bulletList: (editor): boolean => editor.isActive('bulletList'),
orderedList: (editor): boolean => editor.isActive('orderedList'),
}
export const tableFunctionsActiveSettings = {
deleteTable: (editor): boolean => editor.can().deleteTable(),
addColumnBefore: (editor): boolean => editor.can().addColumnBefore(),
addColumnAfter: (editor): boolean => editor.can().addColumnAfter(),
deleteColumn: (editor): boolean => editor.can().deleteColumn(),
addRowBefore: (editor): boolean => editor.can().addRowBefore(),
addRowAfter: (editor): boolean => editor.can().addRowAfter(),
deleteRow: (editor): boolean => editor.can().deleteRow(),
mergeCells: (editor): boolean => editor.can().mergeCells(),
splitCell: (editor): boolean => editor.can().splitCell(),
toggleHeaderColumn: (editor): boolean => editor.can().toggleHeaderColumn(),
toggleHeaderRow: (editor): boolean => editor.can().toggleHeaderRow(),
toggleHeaderCell: (editor): boolean => editor.can().toggleHeaderCell(),
}
const calcEditorButtonsActiveStatuses = () => {
const objectToReturn: Record<string, boolean> = {}
for (const key in editorFunctionsActiveSettings) {
if (key) objectToReturn[key] = editorFunctionsActiveSettings[key](editor)
}
if (tableFunctionsActiveSettings.deleteTable(editor)) {
objectToReturn.deleteTable = true
for (const key in tableFunctionsActiveSettings) {
if (key) objectToReturn[key] = tableFunctionsActiveSettings[key](editor)
}
} else {
for (const key in tableFunctionsActiveSettings) {
if (key) objectToReturn[key] = false
}
}
editorFunctionsActiveStatuses.value = objectToReturn
}
const updateEditorThrottle = throttle(() => updateEditor(), 5000)
const updateEditorDebounce = debounce(() => updateEditor(), 1000)
const calcEditorButtonsThrottle = throttle(() => calcEditorButtonsActiveStatuses(), 1000, { leading: true, trailing: false })
const onEditorUpdated = () => {
calcEditorButtonsThrottle()
updateEditorThrottle()
updateEditorDebounce()
}
const editor = gimmeEditor({
content: props.content,
autofocus: true,
})
editor.on('update', () => onEditorUpdated())
editor.on('selectionUpdate', () => onEditorUpdated()) |
Oh and also I implemented a button for toggling spellcheck, so when users want to be in a quick mode and don't want to stop while writing and check their errors later, it's possible. vidBildschirmaufnahme.2021-12-13.um.22.31.32.mp4code: const isSpellCheckActive = ref(false)
const toggleSpellCheck = () => {
const state = JSON.parse(document.querySelector('.ProseMirror')?.getAttribute('spellcheck') || '')
const newState = `${!state}`
if (!['true', 'false'].includes(newState)) return
editor.setOptions({
editorProps: {
attributes: {
spellcheck: newState
}
}
})
isSpellCheckActive.value = !state
} |
Actually, I was just about to write that disabling spell check temporarily could be also a smart solution... that's very helpful :-) About this isActive() thing I suggested a while ago to have something like a getActiveMarkorNode() function which would just return a string, object or whatsoever... this way, you would only have to call it once and could it just compare against the string. |
Related: Tl;dr: chrome spellcheck perf got worse. |
Yes, I actually disabled spell checking of Google as I use LanguageTool. However, it is the same story with this tool in chrome. still the isActive issue remains, I like the denouncing idea but still wondering if there is not another solution? I have been playing around, with vue 2 and vue 3, once I started to use more complex html elements editor became laggy and then not usable anymore. |
@rezaffm this demo with 200.000 words runs smooth on my machines https://tiptap.dev/examples/book |
I closed it because @philippkuehn's fix fixed the issue I was reporting - from 2-3 sec my selection perf improved to <30ms. |
Okay, thatt's what I did: Machine: Ubuntu 20.04.3 LTS First Test I copied your whole example, with the menu, with 200.000 words, disabled my extensions, just used the starer kit, spellcheck disabled. Result: laggy, doesn't work at all Second Test I copied the whole example, without menu, disabled my extensions, only starer kit, spellcheck disabled. Result: works perfectly Third Test I copied the whole example, without menu, own extensions, spellcheck disabled. Result: Laggy (I have one intensive onUpdate Function that makes it slower). According spell check, yes, once activated, everything laggs and breaks. I can do the same for vue 3 if it helps. |
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. |
@rezaffm There is a known performance issue with vue 2. That’s why I asked which framework you are using. In vue 2 the whole editor instance is reactive. In vue 3 and react only the editor state is reactive. It’s a limitation of the reactivity system of vue 2 so we can’t do here anything. Vue 3 runs super smooth. Except of the spellcheck issue in chrome (which is a browser limitation). |
@philippkuehn Hi, I ran another test, when I debounced the following code on vue-3: this.$emit('update:modelValue', this.editor.getHTML()) Then indeed it runs smoothly (as in your demo). It is also correct that once spellcheck is true, the whole thing hangs and doesn't work anymore, which is something we can only wait for Chrome to fix (it's quite annoying actually). I also tested my initial text with numerous images, et cetera, and it also runs fine (even with spell checking enabled). Just wondering, wouldn't it be a good idea to give a hint in the documentation that once used together with v-model (both in vue-2 and vue-3) it is a good idea to use a debounce function? But coming back to your point, indeed on vue-3 it works fine. |
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). |
Yes, this is confirmed, tiptap has clearly a weakness for vue-2 when it comes to isActive. I can also confirm that I had to debounce the CharacterCount functionality as well, otherwise it would lagg from texts around > 1.500/2.000 words. Debouncing CharacterCount and also the input (v-model binding) event is also necessary on vue-3, otherwise it starts lagging there as well. That's why I recommended adding a "best practice" section for this matter on the documentation side. 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). The final bottleneck when it comes to performance on my side is spellcheck. We write a lot about financial topcis, so good grammar and spelling is a must, that's why we work with LanguageTool. However, LanguageTool checks texts in the browsers without debouncing, making the editor again very slugish from a count of 2.000 words. Finally, the only solution to me is to in source the spelling functionality of LanguageTool with an own plugin, which also helps to get finally rid of the unreliable way Chrome handles contenteditable fields with spellchecking (it's a mess). I really like the two guys here (and also the LanguageTool Plugin people), so definitely the extension will be open source. |
I have written my own method isActive In another updateButtons method i push the value in an array The update method is called in the editor.on("selectionUpdate" Works perfect. I tested this with the Book Example (>200.000 words). |
Hi Peter, Thanks for sharing. I like your solution. With the debounce alternative, it also started lagging for me because if you call the function on 15-20 immediately, it somehow has to use "resources", right? Maybe you had the same experience. For your updateButtons method, how many times do you call it? They way I see it, you call it for all the marks/nodes for which you want to show an active class, right? |
If you call the original editor.isActive method each time the content changes, it slows down. Therefore i update the states of the toolbar buttons in an update method which is debounce called - calls the update methods every 100ms, independent of the original calls.
Yes |
I see, I mean I debounce the input event (500m) anyways, as otherwise two way binding is laggy. I will have a try, or maybe just wait for the vue-3 integration. Thanks for sharing. |
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
Timings:
Expected behavior
I expect Ctrl+A to be instant
Environment?
Additional context
Issue seems related to nodeBetween/textBetween
The text was updated successfully, but these errors were encountered: