perf(editor): optimize typing performance by debouncing context state updates and using useEditorState for BubbleToolbar (US-EDITOR-PERF)

This commit is contained in:
Antigravity
2026-05-27 21:41:19 +00:00
parent 07ace46dd3
commit e3cb1307d3
4 changed files with 74 additions and 38 deletions

View File

@@ -51,7 +51,27 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
}, [session?.user?.id])
const [title, setTitle] = useState(note.title || '')
const [content, setContent] = useState(note.content)
const contentRef = useRef(note.content)
const [content, setContentState] = useState(note.content)
const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const setContentImmediate = useCallback((newVal: string) => {
contentRef.current = newVal
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current)
}
setContentState(newVal)
}, [])
const setContent = useCallback((newVal: string) => {
contentRef.current = newVal
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current)
}
debounceTimeoutRef.current = setTimeout(() => {
setContentState(newVal)
}, 1000)
}, [])
const [checkItems, setCheckItems] = useState<CheckItem[]>(note.checkItems || [])
const [labels, setLabels] = useState<string[]>(note.labels || [])
const [images, setImages] = useState<string[]>(note.images || [])
@@ -74,7 +94,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
if (note.id !== prev.id) {
setTitle(note.title || '')
setContent(note.content)
setContentImmediate(note.content)
setCheckItems(note.checkItems || [])
setLabels(note.labels || [])
setImages(note.images || [])
@@ -87,7 +107,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
} else {
if (note.title !== prev.title) setTitle(note.title || '')
// Ne pas réinitialiser le contenu quand seuls images/links changent (post-save)
if (note.content !== prev.content) setContent(note.content)
if (note.content !== prev.content) setContentImmediate(note.content)
if (JSON.stringify(note.checkItems || []) !== JSON.stringify(prev.checkItems || [])) {
setCheckItems(note.checkItems || [])
}
@@ -309,8 +329,8 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
const editor = richTextEditorRef.current?.getEditor()
if (editor) return editor.getHTML()
}
return content
}, [content, isMarkdown])
return contentRef.current
}, [isMarkdown])
const resolveImagesForSave = useCallback((contentToSave: string): string[] => {
// Images présentes dans le contenu de l'éditeur (inline dans le HTML ou Markdown)
@@ -472,7 +492,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
})
const data = await response.json()
if (!response.ok) throw new Error(data.error || t('notes.clarifyFailed'))
setContent(data.reformulatedText || data.text)
setContentImmediate(data.reformulatedText || data.text)
toast.success(t('ai.reformulationApplied'))
} catch (error) {
console.error('Clarify error:', error)
@@ -501,7 +521,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
})
const data = await response.json()
if (!response.ok) throw new Error(data.error || t('notes.shortenFailed'))
setContent(data.reformulatedText || data.text)
setContentImmediate(data.reformulatedText || data.text)
toast.success(t('ai.reformulationApplied'))
} catch (error) {
console.error('Shorten error:', error)
@@ -530,7 +550,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
})
const data = await response.json()
if (!response.ok) throw new Error(data.error || t('notes.improveFailed'))
setContent(data.reformulatedText || data.text)
setContentImmediate(data.reformulatedText || data.text)
toast.success(t('ai.reformulationApplied'))
} catch (error) {
console.error('Improve error:', error)
@@ -565,7 +585,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
const data = await response.json()
if (!response.ok) throw new Error(data.error || t('notes.transformFailed'))
setContent(data.transformedText)
setContentImmediate(data.transformedText)
setIsMarkdown(true)
setShowMarkdownPreview(false)
@@ -581,7 +601,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
const handleApplyRefactor = () => {
if (!reformulationModal) return
setContent(reformulationModal.reformulatedText)
setContentImmediate(reformulationModal.reformulatedText)
setReformulationModal(null)
toast.success(t('ai.reformulationApplied'))
}
@@ -628,7 +648,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
type: isMarkdown ? 'markdown' as const : 'richtext' as const,
size,
}, { skipRevalidation: true })
if (contentToSave !== content) setContent(contentToSave)
if (contentToSave !== content) setContentImmediate(contentToSave)
if (JSON.stringify(imagesToSave) !== JSON.stringify(images)) setImages(imagesToSave)
prevNoteRef.current = { ...prevNoteRef.current, ...result }
const deletedImages = Array.from(new Set([
@@ -752,7 +772,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
size,
}
const result = await updateNote(note.id, updatePayload, { skipRevalidation: true })
if (contentToSave !== content) setContent(contentToSave)
if (contentToSave !== content) setContentImmediate(contentToSave)
if (JSON.stringify(imagesToSave) !== JSON.stringify(images)) setImages(imagesToSave)
prevNoteRef.current = { ...prevNoteRef.current, ...result }
const deletedImages = Array.from(new Set([
@@ -853,7 +873,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
isAnalyzingSuggestions, isMarkdown, allImages, colorClasses
])
const actions: NoteEditorActions = {
const actions: NoteEditorActions = useMemo(() => ({
setTitle: (t) => { setTitle(t); setIsDirty(true); setDismissedTitleSuggestions(true) },
setDismissedTitleSuggestions,
setContent: (c) => { setContent(c); setIsDirty(true) },
@@ -904,7 +924,14 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
setIsGeneratingTitles,
setIsAnalyzingSuggestions: (_a) => { /* handled by useAutoTagging */ },
setPreviousContentForCopilot,
}
}), [
handleCheckItem, handleUpdateCheckItem, handleAddCheckItem, handleRemoveCheckItem,
handleSelectGhostTag, handleDismissGhostTag, handleRemoveLabel, handleImageUpload,
handleRemoveImage, handleAddLink, handleRemoveLink, handleReminderSave,
handleRemoveReminder, handleGenerateTitles, handleSelectTitle, handleReformulate,
handleApplyRefactor, handleClarifyDirect, handleShortenDirect, handleImproveDirect,
handleTransformMarkdown, handleSave, handleSaveInPlace, handleMakeCopy
])
const value: NoteEditorContextValue = useMemo(() => ({
note,