fix: Improve performance when editing titles in large open document trees (#11858)

This commit is contained in:
Tom Moor
2026-03-23 18:53:37 -04:00
committed by GitHub
parent 33d8e41e41
commit 84aed78ee2
4 changed files with 80 additions and 48 deletions
@@ -265,27 +265,30 @@ function InnerDocumentLink(
};
});
const nodeChildren = React.useMemo(() => {
const insertDraftDocument =
activeDocument?.isDraft &&
activeDocument?.isActive &&
activeDocument?.parentDocumentId === node.id;
const insertDraftChild = !!(
activeDocument?.isDraft &&
activeDocument?.isActive &&
activeDocument?.parentDocumentId === node.id
);
return collection && insertDraftDocument
? sortNavigationNodes(
[activeDocument?.asNavigationNode, ...node.children],
collection.sort,
false
)
: node.children;
}, [
activeDocument?.isActive,
activeDocument?.isDraft,
activeDocument?.parentDocumentId,
activeDocument?.asNavigationNode,
collection,
node,
]);
// Only subscribe to asNavigationNode when this node is the parent of an
// active draft. This avoids every DocumentLink observer re-rendering on
// every title keystroke.
const draftNavNode = insertDraftChild
? activeDocument?.asNavigationNode
: undefined;
const nodeChildren = React.useMemo(
() =>
collection && draftNavNode
? sortNavigationNodes(
[draftNavNode, ...node.children],
collection.sort,
false
)
: node.children,
[draftNavNode, collection, node]
);
const doc = documents.get(node.id);
const title = doc?.title || node.title || t("Untitled");
@@ -7,38 +7,32 @@ export default function useCollectionDocuments(
collection: Collection | undefined,
activeDocument: Document | undefined
) {
const insertDraftDocument = useMemo(
() =>
activeDocument &&
activeDocument.isActive &&
activeDocument.isDraft &&
activeDocument.collectionId === collection?.id &&
!activeDocument.parentDocumentId,
[
activeDocument?.isActive,
activeDocument?.isDraft,
activeDocument?.collectionId,
activeDocument?.parentDocumentId,
collection?.id,
]
const insertDraftDocument = !!(
activeDocument &&
activeDocument.isActive &&
activeDocument.isDraft &&
activeDocument.collectionId === collection?.id &&
!activeDocument.parentDocumentId
);
// Only subscribe to asNavigationNode when we actually need to insert a draft
// into the sorted list. This avoids every CollectionLinkChildren observer
// re-rendering on every title keystroke.
const draftNavNode = insertDraftDocument
? activeDocument?.asNavigationNode
: undefined;
return useMemo(() => {
if (!collection?.sortedDocuments) {
return undefined;
}
return insertDraftDocument && activeDocument
return draftNavNode
? sortNavigationNodes(
[activeDocument.asNavigationNode, ...collection.sortedDocuments],
[draftNavNode, ...collection.sortedDocuments],
collection.sort,
false
)
: collection.sortedDocuments;
}, [
insertDraftDocument,
activeDocument?.asNavigationNode,
collection?.sortedDocuments,
collection?.sort,
]);
}, [draftNavNode, collection?.sortedDocuments, collection?.sort]);
}
+18 -6
View File
@@ -1,4 +1,5 @@
import type { NavigationNode } from "../types";
import shallowEqual from "./shallowEqual";
import naturalSort from "./naturalSort";
type Sort = {
@@ -21,12 +22,23 @@ export const sortNavigationNodes = (
direction: sort.direction,
});
return orderedDocs.map((node) => ({
...node,
children: sortChildren
? sortNavigationNodes(node.children, sort, sortChildren)
: node.children,
}));
if (!sortChildren) {
return orderedDocs;
}
return orderedDocs.map((node) => {
const sortedChildren = sortNavigationNodes(
node.children,
sort,
sortChildren
);
// Preserve the original node reference if children order didn't change.
// This allows React.memo to skip re-renders of unchanged tree nodes.
if (shallowEqual(sortedChildren, node.children)) {
return node;
}
return { ...node, children: sortedChildren };
});
};
export const colorPalette = [
+23
View File
@@ -0,0 +1,23 @@
/**
* Check if two arrays have the same elements in the same order by reference.
* Uses strict equality (===) rather than deep comparison, so object identity
* is preserved — important for React.memo optimizations.
*
* @param a first array.
* @param b second array.
* @returns true if the arrays are shallowly equal.
*/
export default function shallowEqual<T>(a: T[], b: T[]): boolean {
if (a === b) {
return true;
}
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}