fix: Render-per-model type, 4x improvement on perf (#10465)

* fix: Render-per-model type, 4x improvement on perf

* fix: Sidebar CollectionLinkChildren render when @mention changes
This commit is contained in:
Tom Moor
2025-10-24 02:23:38 +02:00
committed by GitHub
parent 77d50f8323
commit 02bdb2e464
9 changed files with 99 additions and 57 deletions
@@ -7,18 +7,28 @@ 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,
]
);
return useMemo(() => {
if (!collection?.sortedDocuments) {
return undefined;
}
const insertDraftDocument =
activeDocument?.isActive &&
activeDocument?.isDraft &&
activeDocument?.collectionId === collection.id &&
!activeDocument?.parentDocumentId;
return insertDraftDocument
return insertDraftDocument && activeDocument
? sortNavigationNodes(
[activeDocument.asNavigationNode, ...collection.sortedDocuments],
collection.sort,
@@ -26,14 +36,9 @@ export default function useCollectionDocuments(
)
: collection.sortedDocuments;
}, [
activeDocument?.isActive,
activeDocument?.isDraft,
activeDocument?.collectionId,
activeDocument?.parentDocumentId,
insertDraftDocument,
activeDocument?.asNavigationNode,
collection,
collection?.sortedDocuments,
collection?.id,
collection?.sort,
]);
}
+15 -9
View File
@@ -1,3 +1,4 @@
import { useCallback } from "react";
import useDictionary from "~/hooks/useDictionary";
import getMenuItems from "../menus/block";
import { useEditor } from "./EditorContext";
@@ -13,20 +14,25 @@ function BlockMenu(props: Props) {
const dictionary = useDictionary();
const { elementRef } = useEditor();
const renderMenuItem = useCallback(
(item, _index, options) => (
<SuggestionsMenuItem
onClick={options.onClick}
selected={options.selected}
icon={item.icon}
title={item.title}
shortcut={item.shortcut}
/>
),
[]
);
return (
<SuggestionsMenu
{...props}
filterable
trigger="/"
renderMenuItem={(item, _index, options) => (
<SuggestionsMenuItem
onClick={options.onClick}
selected={options.selected}
icon={item.icon}
title={item.title}
shortcut={item.shortcut}
/>
)}
renderMenuItem={renderMenuItem}
items={getMenuItems(dictionary, elementRef)}
/>
);
+14 -9
View File
@@ -1,5 +1,5 @@
import capitalize from "lodash/capitalize";
import { useMemo } from "react";
import { useCallback, useMemo } from "react";
import { emojiMartToGemoji, snakeCase } from "@shared/editor/lib/emoji";
import { search as emojiSearch } from "@shared/utils/emoji";
import EmojiMenuItem from "./EmojiMenuItem";
@@ -45,18 +45,23 @@ const EmojiMenu = (props: Props) => {
[search]
);
const renderMenuItem = useCallback(
(item, _index, options) => (
<EmojiMenuItem
onClick={options.onClick}
selected={options.selected}
title={item.description}
emoji={item.emoji}
/>
),
[]
);
return (
<SuggestionsMenu
{...props}
filterable={false}
renderMenuItem={(item, _index, options) => (
<EmojiMenuItem
onClick={options.onClick}
selected={options.selected}
title={item.description}
emoji={item.emoji}
/>
)}
renderMenuItem={renderMenuItem}
items={items}
/>
);
+25 -14
View File
@@ -25,6 +25,7 @@ import SuggestionsMenu, {
Props as SuggestionsMenuProps,
} from "./SuggestionsMenu";
import SuggestionsMenuItem from "./SuggestionsMenuItem";
import { runInAction } from "mobx";
interface MentionItem extends MenuItem {
attrs: {
@@ -53,12 +54,17 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
const { loading, request } = useRequest(
useCallback(async () => {
const res = await client.post("/suggestions.mention", { query: search });
const res = await client.post("/suggestions.mention", {
query: search,
limit: maxResultsInSection,
});
res.data.documents.map(documents.add);
res.data.users.map(users.add);
res.data.collections.map(collections.add);
res.data.groups.map(groups.add);
runInAction(() => {
res.data.documents.map(documents.add);
res.data.users.map(users.add);
res.data.collections.map(collections.add);
res.data.groups.map(groups.add);
});
}, [search, documents, users, collections])
);
@@ -274,6 +280,19 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
[t, users, documentId, groups]
);
const renderMenuItem = useCallback(
(item, _index, options) => (
<SuggestionsMenuItem
onClick={options.onClick}
selected={options.selected}
subtitle={item.subtitle}
title={item.title}
icon={item.icon}
/>
),
[]
);
// Prevent showing the menu until we have data otherwise it will be positioned
// incorrectly due to the height being unknown.
if (!loaded) {
@@ -287,15 +306,7 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
filterable={false}
search={search}
onSelect={handleSelect}
renderMenuItem={(item, _index, options) => (
<SuggestionsMenuItem
onClick={options.onClick}
selected={options.selected}
subtitle={item.subtitle}
title={item.title}
icon={item.icon}
/>
)}
renderMenuItem={renderMenuItem}
items={items}
/>
);
+14 -9
View File
@@ -1,6 +1,6 @@
import { observer } from "mobx-react";
import { EmailIcon, LinkIcon } from "outline-icons";
import React from "react";
import React, { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { EmbedDescriptor } from "@shared/editor/embeds";
import { MenuItem } from "@shared/editor/types";
@@ -26,6 +26,18 @@ type Props = Omit<
export const PasteMenu = observer(({ pastedText, embeds, ...props }: Props) => {
const items = useItems({ pastedText, embeds });
const renderMenuItem = useCallback(
(item, _index, options) => (
<SuggestionsMenuItem
onClick={options.onClick}
selected={options.selected}
title={item.title}
icon={item.icon}
/>
),
[]
);
if (!items) {
props.onClose();
return null;
@@ -36,14 +48,7 @@ export const PasteMenu = observer(({ pastedText, embeds, ...props }: Props) => {
{...props}
trigger=""
filterable={false}
renderMenuItem={(item, _index, options) => (
<SuggestionsMenuItem
onClick={options.onClick}
selected={options.selected}
title={item.title}
icon={item.icon}
/>
)}
renderMenuItem={renderMenuItem}
items={items}
/>
);
+5 -1
View File
@@ -641,6 +641,10 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
}
};
const handleOnClick = () => {
handleClickItem(item);
};
const currentHeading =
"section" in item ? item.section?.({ t }) : undefined;
@@ -657,7 +661,7 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
>
{props.renderMenuItem(item as any, index, {
selected: index === selectedIndex,
onClick: () => handleClickItem(item),
onClick: handleOnClick,
})}
</ListItem>
</React.Fragment>
@@ -92,4 +92,4 @@ const Shortcut = styled.span<{ $active?: boolean }>`
text-align: right;
`;
export default SuggestionsMenuItem;
export default React.memo(SuggestionsMenuItem);
+2 -1
View File
@@ -16,6 +16,7 @@ import { client } from "~/utils/ApiClient";
import User from "./User";
import Field from "./decorators/Field";
import { AfterChange } from "./decorators/Lifecycle";
import isEqual from "lodash/isEqual";
export default class Collection extends ParanoidModel {
static modelName = "Collection";
@@ -156,7 +157,7 @@ export default class Collection extends ParanoidModel {
return this.sort.field === "index";
}
@computed
@computed({ equals: isEqual })
get sortedDocuments(): NavigationNode[] | undefined {
if (!this.documents) {
return undefined;
+5
View File
@@ -6,6 +6,7 @@ import Logger from "~/utils/Logger";
import { getFieldsForModel } from "../decorators/Field";
import { LifecycleManager } from "../decorators/Lifecycle";
import { getRelationsForModelClass } from "../decorators/Relation";
import { isEqual } from "lodash";
export default abstract class Model {
static modelName: string;
@@ -147,6 +148,10 @@ export default abstract class Model {
continue;
}
// @ts-expect-error TODO
if (isEqual(this[key], data[key])) {
continue;
}
// @ts-expect-error TODO
this[key] = data[key];
} catch (error) {
Logger.warn(`Error setting ${key} on model`, error);