Package: @syncfusion/ej2-documenteditor@33.1.44
Component: DocumentEditorContainer
Symptom
On a ~130-page DOCX with track changes enabled, programmatically deleting a selection that spans most of the document blocks the main thread long enough that Chrome shows a "page not responding" prompt (which the user has to dismiss multiple times before the operation completes). With track changes disabled, the exact same deletion on the exact same selection completes in a fraction of the time and does not trigger the unresponsive-page warning at all.
The operation does eventually succeed and the resulting document is correct — this is a latency problem, not a correctness bug.
Reproduction
- Load a DOCX of ~100+ pages into a
DocumentEditorContainerComponent.
- Enable track changes.
- Programmatically select a span covering most of the document (e.g.
editor.selection.select(startOffset, endOffset) where startOffset is near the top and endOffset is near the end).
- Call
editor.editor.delete() followed by editor.editor.insertText("replacement").
Observed: several seconds of blocked main thread with track changes on; multiple Chrome unresponsive-page prompts on the worst-case whole-document span. With enableTrackChanges = false the same call sequence on the same selection is much faster and does not block long enough to trigger the prompt.
Environment
@syncfusion/ej2-documenteditor 33.1.44
DocumentEditorContainerComponent from @syncfusion/ej2-react-documenteditor
- Chrome on macOS
- Test document: ~130 pages, real-world DOCX with mixed formatting (not a synthetic stress test)
Hypothesis (from tracing the installed source)
Reading through editor.js in v33.1.44, the dominant cost on the tracked-changes path seems to be a per-paragraph reLayoutParagraph call inside Editor.prototype.removeInlines:
src/document-editor/implementation/editor/editor.js:18159-18161
if (!this.documentHelper.layout.isReplacingAll) {
this.documentHelper.layout.reLayoutParagraph(paragraph, 0, 0);
}
removeInlines appears to run once per paragraph touched by the deletion. With track changes off, paragraphs get physically removed and the loop shrinks as it proceeds — which matches the observed fast behaviour. With track changes on, nothing is removed: every paragraph in the range still exists, so every one pays a full reLayoutParagraph pass on top of the per-inline addRevision bookkeeping. On a 130-page selection that's on the order of a thousand relayout passes in a tight loop.
This is a hypothesis from reading the source, not a profiler trace — happy to be corrected if the real hotspot is elsewhere.
Precedent — replaceAll already handles this pattern
Noticed that Search.prototype.replaceAll in src/document-editor/implementation/search/search.js (lines 189-234) uses two internal flags to skip the per-paragraph relayout during its own bulk operation and defer to a single final layout at the end:
this.documentHelper.layout.isReplacingAll = true; // line 190
this.viewer.owner.isLayoutEnabled = false; // line 198
// ... loop doing many edits ...
this.documentHelper.layout.isReplacingAll = false; // line 233
The guard on line 18159 above is specifically checking isReplacingAll, so this mechanism is clearly deliberate for bulk work inside the library. Editor.prototype.deleteAllComments (editor.js:1130) uses the same isLayoutEnabled = false pattern around its bulk loop.
Question
Is there a supported public API for applying the same "one bulk edit, single final layout pass" treatment to a programmatic delete() + insertText() pair — i.e. a way to tell the DocumentEditor "I'm about to make one big edit, please don't relayout per paragraph along the way"? The isReplacingAll / isLayoutEnabled flags look internal and relying on them from application code feels fragile across Syncfusion versions. Ideally this would be something like an editor.beginBulkEdit() / endBulkEdit() pair, or an option on delete() that suppresses per-paragraph relayout.
A secondary (softer) question: whether the deletion-revision bookkeeping itself (one addRevision per inline) could be represented as a single range-level revision for very large tracked deletions. That's probably a bigger architectural change and I suspect the answer is "no, it's inherent to the data model," but flagging it in case it's on the roadmap.
Happy to run any requested instrumentation against the same reproduction DOCX — let me know if a profiler trace or a minimal reproduction in stackblitz would help.
Package:
@syncfusion/ej2-documenteditor@33.1.44Component:
DocumentEditorContainerSymptom
On a ~130-page DOCX with track changes enabled, programmatically deleting a selection that spans most of the document blocks the main thread long enough that Chrome shows a "page not responding" prompt (which the user has to dismiss multiple times before the operation completes). With track changes disabled, the exact same deletion on the exact same selection completes in a fraction of the time and does not trigger the unresponsive-page warning at all.
The operation does eventually succeed and the resulting document is correct — this is a latency problem, not a correctness bug.
Reproduction
DocumentEditorContainerComponent.editor.selection.select(startOffset, endOffset)wherestartOffsetis near the top andendOffsetis near the end).editor.editor.delete()followed byeditor.editor.insertText("replacement").Observed: several seconds of blocked main thread with track changes on; multiple Chrome unresponsive-page prompts on the worst-case whole-document span. With
enableTrackChanges = falsethe same call sequence on the same selection is much faster and does not block long enough to trigger the prompt.Environment
@syncfusion/ej2-documenteditor33.1.44DocumentEditorContainerComponentfrom@syncfusion/ej2-react-documenteditorHypothesis (from tracing the installed source)
Reading through
editor.jsin v33.1.44, the dominant cost on the tracked-changes path seems to be a per-paragraphreLayoutParagraphcall insideEditor.prototype.removeInlines:src/document-editor/implementation/editor/editor.js:18159-18161removeInlinesappears to run once per paragraph touched by the deletion. With track changes off, paragraphs get physically removed and the loop shrinks as it proceeds — which matches the observed fast behaviour. With track changes on, nothing is removed: every paragraph in the range still exists, so every one pays a fullreLayoutParagraphpass on top of the per-inlineaddRevisionbookkeeping. On a 130-page selection that's on the order of a thousand relayout passes in a tight loop.This is a hypothesis from reading the source, not a profiler trace — happy to be corrected if the real hotspot is elsewhere.
Precedent —
replaceAllalready handles this patternNoticed that
Search.prototype.replaceAllinsrc/document-editor/implementation/search/search.js(lines 189-234) uses two internal flags to skip the per-paragraph relayout during its own bulk operation and defer to a single final layout at the end:The guard on line 18159 above is specifically checking
isReplacingAll, so this mechanism is clearly deliberate for bulk work inside the library.Editor.prototype.deleteAllComments(editor.js:1130) uses the sameisLayoutEnabled = falsepattern around its bulk loop.Question
Is there a supported public API for applying the same "one bulk edit, single final layout pass" treatment to a programmatic
delete()+insertText()pair — i.e. a way to tell the DocumentEditor "I'm about to make one big edit, please don't relayout per paragraph along the way"? TheisReplacingAll/isLayoutEnabledflags look internal and relying on them from application code feels fragile across Syncfusion versions. Ideally this would be something like aneditor.beginBulkEdit()/endBulkEdit()pair, or an option ondelete()that suppresses per-paragraph relayout.A secondary (softer) question: whether the deletion-revision bookkeeping itself (one
addRevisionper inline) could be represented as a single range-level revision for very large tracked deletions. That's probably a bigger architectural change and I suspect the answer is "no, it's inherent to the data model," but flagging it in case it's on the roadmap.Happy to run any requested instrumentation against the same reproduction DOCX — let me know if a profiler trace or a minimal reproduction in stackblitz would help.