You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优化自定义Tiptap Annotation扩展的性能问题?

Tiptap Annotation扩展性能优化方案

核心问题定位

你的性能损耗主要来自三个点:

  • onUpdate钩子触发过于频繁:Tiptap的onUpdate会在每一次编辑器状态变更(包括光标移动、输入单个字符)时执行,导致updateAnnotations反复调用,频繁触发顶层组件的setAnnotations。
  • 同步DOM查询开销大:updateAnnotations中调用editor.view.domAtPos是同步DOM操作,每次遍历所有annotation节点都会触发多次DOM查询,阻塞主线程。
  • 不必要的React重渲染:每次onUpdate传递的data都是全新的数组和对象,即使内容没变化,也会触发顶层组件的setState和重渲染。

具体优化步骤

1. 替换onUpdate,仅在annotation节点变更时触发更新

放弃扩展自带的onUpdate钩子,改用ProseMirror插件监听transaction,只在annotation节点增删改时才执行更新逻辑:

修改addProseMirrorPlugins方法,启用插件并添加更新判断:

addProseMirrorPlugins() {
  const editor = this.editor;
  const plugin = new Plugin({
    key: new PluginKey('annotation'),
    appendTransaction(transactions, oldState, newState) {
      let tr = newState.tr;
      let hasAnnotationChange = false;

      // 检查是否有annotation节点的变更
      transactions.forEach(transaction => {
        if (transaction.docChanged) {
          // 遍历步骤,判断是否涉及annotation节点
          transaction.steps.forEach(step => {
            if (step instanceof ReplaceStep) {
              oldState.doc.nodesBetween(step.from, step.to, node => {
                if (node.type.name === 'annotation') hasAnnotationChange = true;
              });
              newState.doc.nodesBetween(step.from, step.to, node => {
                if (node.type.name === 'annotation') hasAnnotationChange = true;
              });
            }
          });
        }
      });

      // 更新annotation的label(原逻辑保留并优化)
      if (transactions.some(tr => tr.docChanged)) {
        const annotations = getAllNodesOfType(editor, 'annotation', tr);
        // 按类型分组计算label,避免重复遍历
        const annotationsByType = annotations.reduce((acc, item) => {
          const type = item.node.attrs.type;
          if (!acc[type]) acc[type] = [];
          acc[type].push(item);
          return acc;
        }, {} as Record<string, NodeWithPos[]>);

        Object.values(annotationsByType).forEach(group => {
          group.forEach((item, index) => {
            const label = item.node.attrs.type === 'annotation' 
              ? index + 1 
              : ALPHABET[index];
            tr.setNodeMarkup(item.pos, undefined, {
              ...item.node.attrs,
              label,
            });
          });
        });
      }

      // 仅当annotation有变化时,异步触发更新(避免阻塞transaction)
      if (hasAnnotationChange) {
        setTimeout(() => {
          updateAnnotations({
            editor,
            storage: this.storage,
            onUpdate: this.options.onUpdate?.bind(this),
          });
        }, 0);
      }

      return tr;
    },
  });

  return [plugin]; // 之前return []导致插件未启用,现在返回插件数组
}

删除扩展中的onUpdate钩子:

// 移除这段代码
onUpdate() {
  updateAnnotations({
    editor: this.editor as Editor,
    storage: this.storage,
    onUpdate:
      this.options.onUpdate === null || this.options.onUpdate === void 0
        ? void 0
        : this.options.onUpdate.bind(this),
  });
}

2. 优化updateAnnotations,减少DOM查询和重复计算

  • 缓存DOM元素,避免每次调用domAtPos
  • 对比新旧数据,仅在内容变化时触发onUpdate

修改updateAnnotations函数:

const updateAnnotations = (state: {
  editor: Editor;
  storage: AnnotationStorage;
  onUpdate: any;
}) => {
  const { editor, storage, onUpdate } = state;
  const nodesWithPos = getAllNodesOfType(editor, 'annotation');
  
  // 构建新的annotation数据,复用已有DOM缓存
  const newData: AnnotationData = nodesWithPos.map(nodeWithPos => {
    const nodeAttrs = nodeWithPos.node.attrs;
    return {
      label: nodeAttrs.label,
      id: nodeAttrs.id,
      type: nodeAttrs.type,
      dom: storage.elements.find(el => el.id === nodeAttrs.id) 
        || (editor.view.domAtPos(nodeWithPos.pos + 1).node as HTMLElement),
    };
  });

  // 缓存DOM元素到storage
  storage.elements = newData.map(item => item.dom);

  // 对比核心数据(忽略dom引用),仅变化时触发更新
  const isDataChanged = JSON.stringify(newData.map(({id, label, type})) 
    !== JSON.stringify(storage.content.map(({id, label, type})));
  
  if (onUpdate && isDataChanged) {
    const isCreate = storage.content.length === 0;
    onUpdate(newData, isCreate);
  }

  storage.content = newData;
};

3. 简化getAnnotationNumber,避免重复遍历

修改getAnnotationNumber,直接读取节点预计算好的label属性:

export const getAnnotationNumber = (
  editor: any,
  node: PMNode,
  nodes?: PMNode[]
) => {
  // 直接返回插件中已计算的label,无需重新遍历节点
  return node.attrs.label;
};

4. 优化React组件重渲染

在顶层组件中,对onUpdate传递的data做浅比较,避免不必要的重渲染:

// 顶层组件中使用useCallback和状态比较
const [annotations, setAnnotations] = useState<AnnotationData>([]);

const handleAnnotationUpdate = useCallback((newData: AnnotationData) => {
  // 仅对比核心字段,忽略dom引用
  const isEqual = annotations.length === newData.length 
    && annotations.every((item, index) => 
      item.id === newData[index].id 
      && item.label === newData[index].label 
      && item.type === newData[index].type
    );
  if (!isEqual) {
    setAnnotations(newData);
  }
}, [annotations]);

额外优化建议

  • 禁用annotation节点的selectable:如果不需要选中annotation节点,设置selectable: false减少编辑器状态判断开销。
  • 避免在renderHTML中做计算:渲染时直接读取node.attrs.label,无需调用函数。

内容的提问来源于stack exchange,提问作者Ugus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 17:10:55