Skip to content

Conversation

@icaroharry
Copy link
Contributor

Hey Superdoc team 👋🏽

I was learning more about the project and saw this issue (#1721) related to the performance of the document when there are a lot of tracked changes.

I was able to identify 2 different problems:

  1. On the document load, the comments were added one-by-one, causing a UI update cycle for each comment
  2. Each keystroke (or click) in the document, causes a full document traversal going through all the tracked changes.

This PR has a simple fix for the 1st issue. I started investigating also the 2nd, but it seems more complex.

I went ahead and implemented this fix, mostly because I wanted to learn more how the editor works, but I'm opening the PR because it might be useful to the project. (I don't mean to go over the team's development process, so no worries if this can't be merged 😅)

Summary

This PR fixes a critical performance issue where documents with many tracked changes (e.g., 800+ changes) took over 5 seconds to load comments. The root cause was identified as 827 individual ProseMirror transactions being dispatched in a loop, each triggering a full document traversal and UI update cycle.

Before: 827 dispatches → 5+ seconds load time
After: 1 dispatch → < 1s load time

before.mp4
now.mp4

Problem Analysis

Symptoms

  • Document with ~800 tracked changes took 5+ seconds to display comments
  • Console showed 827 "Create comment for track change" debug messages
  • UI appeared frozen during initial load

Root Cause

In comments-store.js, the createCommentForTrackChanges function was iterating over grouped track changes and dispatching a separate ProseMirror transaction for each one:

// BEFORE: Inside forEach loop - 827 dispatches!
groupedChanges.forEach(({ insertedMark, deletionMark, formatMark }) => {
  // ...validation logic...

  const { tr } = editor.view.state;
  tr.setMeta(CommentsPluginKey, { type: 'force' });
  tr.setMeta(TrackChangesBasePluginKey, {
    insertedMark: insertedMark?.mark,
    deletionMark: deletionMark?.mark,
    formatMark: formatMark?.mark,
  });
  editor.view.dispatch(tr);  // ← Called 827 times!
});

Each dispatch(tr) call:

  1. Creates a new immutable editor state
  2. Triggers all plugin state recalculations
  3. Causes DOM reconciliation
  4. Fires Vue reactivity updates

Multiplied by 827 iterations, this created a severe performance bottleneck.

Solution

Approach

Batch all track changes into a single transaction and process them in one pass through the comments plugin.

Implementation

1. Collect Track Changes Before Dispatching

File: packages/superdoc/src/stores/comments-store.js

Instead of dispatching inside the loop, collect all track changes that need processing:

// AFTER: Collect first, dispatch once
const trackChangesToProcess = [];

groupedChanges.forEach(({ insertedMark, deletionMark, formatMark }) => {
  const foundComment = commentsList.value.find(
    (i) =>
      i.commentId === insertedMark?.mark.attrs.id ||
      i.commentId === deletionMark?.mark.attrs.id ||
      i.commentId === formatMark?.mark.attrs.id,
  );

  if (foundComment) return;

  if (insertedMark || deletionMark || formatMark) {
    trackChangesToProcess.push({
      insertedMark: insertedMark?.mark,
      deletionMark: deletionMark?.mark,
      formatMark: formatMark?.mark,
    });
  }
});

// Single dispatch with all track changes
const { tr } = editor.view.state;
tr.setMeta(CommentsPluginKey, { type: 'force' });
tr.setMeta(TrackChangesBasePluginKey, {
  batchedTrackChanges: trackChangesToProcess,  // ← New batched format
});
editor.view.dispatch(tr);  // ← Called once!

2. Handle Batched Track Changes in Plugin

File: packages/super-editor/src/extensions/comment/comments-plugin.js

Modified handleTrackedChangeTransaction to detect and process batched track changes:

const handleTrackedChangeTransaction = (trackedChangeMeta, trackedChanges, newEditorState, editor) => {
  // Handle batched track changes from DOCX import
  if (trackedChangeMeta.batchedTrackChanges) {
    const newTrackedChanges = { ...trackedChanges };
    const emitParamsList = [];

    trackedChangeMeta.batchedTrackChanges.forEach(({ insertedMark, deletionMark, formatMark }) => {
      const result = processSingleTrackChange(
        { insertedMark, deletionMark, formatMark },
        newTrackedChanges,
        newEditorState,
        editor,
      );
      if (result.emitParams) {
        emitParamsList.push(result.emitParams);
      }
    });

    // Emit all comments in a single batch event
    if (emitParamsList.length > 0) {
      editor.emit('commentsUpdate', {
        type: comments_module_events.BATCH_ADD,
        comments: emitParamsList,
      });
    }

    return newTrackedChanges;
  }

  // Original single track change handling preserved for real-time edits
  // ...
};

3. Extract Reusable Processing Logic

File: packages/super-editor/src/extensions/comment/comments-plugin.js

Created processSingleTrackChange helper to handle both batched and single track change scenarios without code duplication:

const processSingleTrackChange = (trackChangeData, newTrackedChanges, newEditorState, editor) => {
  const { insertedMark, deletionMark, formatMark, step, deletionNodes } = trackChangeData;

  // Determine primary mark and type
  const primaryMark = insertedMark || deletionMark || formatMark;
  if (!primaryMark) return { emitParams: null };

  const id = primaryMark.attrs.id;
  // ... existing logic for building comment data ...

  return {
    emitParams: {
      type: comments_module_events.ADD,
      comment: { /* comment data */ },
    },
  };
};

4. Add New Event Type

File: shared/common/event-types.ts

Added BATCH_ADD event type for the new batched comment creation:

export const comments_module_events = {
  RESOLVED: 'resolved',
  NEW: 'new',
  ADD: 'add',
  BATCH_ADD: 'batch-add',  // ← New event type
  UPDATE: 'update',
  DELETED: 'deleted',
  PENDING: 'pending',
  SELECTED: 'selected',
  // ...
} as const;

5. Handle Batch Event in Vue Component

File: packages/superdoc/src/SuperDoc.vue

Added handler for the new BATCH_ADD event:

const onEditorCommentsUpdate = (params = {}) => {
  let { activeCommentId, type, comment: commentPayload, comments: batchComments } = params;

  // Handle batched track change comments
  if (COMMENT_EVENTS?.BATCH_ADD && type === COMMENT_EVENTS.BATCH_ADD && batchComments?.length) {
    batchComments.forEach((trackedChangeParams) => {
      handleTrackedChangeUpdate({ superdoc: proxy.$superdoc, params: trackedChangeParams });
    });
    return;
  }

  // ... existing handlers ...
};

Files Changed

File Changes
packages/superdoc/src/stores/comments-store.js Refactored createCommentForTrackChanges to batch all track changes into single dispatch
packages/super-editor/src/extensions/comment/comments-plugin.js Added processSingleTrackChange helper, modified handleTrackedChangeTransaction to handle batched track changes
shared/common/event-types.ts Added BATCH_ADD event constant
packages/superdoc/src/SuperDoc.vue Added handler for BATCH_ADD event in onEditorCommentsUpdate
packages/super-editor/src/extensions/comment/comments-plugin.test.js Fixed 1 existing test, added 3 new tests for batched functionality

Testing

Existing Test Fix

  • Test: handleTrackedChangeTransaction returns original state when no marks provided
  • Issue: Refactored function now returns trackedChanges instead of undefined when no marks provided
  • Fix: Updated expectation from toBeUndefined() to toEqual({ existing: 'value' })

New Tests Added

  1. handleTrackedChangeTransaction processes batched track changes and emits single BATCH_ADD event

    • Verifies multiple track changes are processed in one pass
    • Confirms single BATCH_ADD event is emitted with all comments
  2. handleTrackedChangeTransaction handles empty batchedTrackChanges array

    • Verifies edge case when batch array is empty
    • Confirms no events are emitted
  3. handleTrackedChangeTransaction filters out invalid track changes in batch

    • Verifies only valid track changes (with at least one mark) are processed
    • Confirms invalid entries are skipped gracefully

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b30243a5c3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +559 to +565
if (COMMENT_EVENTS?.BATCH_ADD && type === COMMENT_EVENTS.BATCH_ADD && batchComments?.length) {
batchComments.forEach((trackedChangeParams) => {
handleTrackedChangeUpdate({ superdoc: proxy.$superdoc, params: trackedChangeParams });
});

return;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Forward batch comment updates to user callback

When a DOCX import (or any batched tracked-change load) emits BATCH_ADD, onEditorCommentsUpdate returns early and skips the config.onCommentsUpdate callback. Previously, each tracked-change add event bubbled up to user code; now integrations that rely on onCommentsUpdate (e.g., external comment sidebars, analytics, or sync hooks) will never be notified for these batch-created comments. This is a behavior regression specific to batched track-change loads and can leave external consumers out of sync.

Useful? React with 👍 / 👎.

@icaroharry icaroharry force-pushed the feature/batch-comments branch from b30243a to 5667d87 Compare January 28, 2026 17:38
Add new event constant to support batching multiple track change
comments into a single event emission, enabling performance
optimizations for bulk operations.
Optimize comments loading performance for documents with many tracked
changes by batching all track change processing into a single
ProseMirror transaction dispatch.

- Collect all track changes before dispatching instead of dispatching
  inside forEach loop
- Add batchedTrackChanges transaction metadata for bulk processing
- Extract processSingleTrackChange helper for code reuse
- Emit single BATCH_ADD event with all comments instead of individual
  ADD events

Reduces 827 dispatches to 1 for documents with ~800 tracked changes,
improving load time from 5+ seconds to near-instant.
Add handler in onEditorCommentsUpdate to process batched track change
comments from the new BATCH_ADD event type, iterating through all
comments and applying handleTrackedChangeUpdate for each.
- Fix existing test expectation for handleTrackedChangeTransaction
  return value when no marks provided
- Add test for processing batched track changes with BATCH_ADD event
- Add test for handling empty batchedTrackChanges array
- Add test for filtering invalid track changes in batch
@icaroharry icaroharry force-pushed the feature/batch-comments branch from 5667d87 to 191a434 Compare January 28, 2026 17:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant