如何优化自定义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
相关产品推荐
相关产品推荐

