perf: memoize rendering of library (#6622)

Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Arnost Pleskot 2023-05-31 15:37:13 +02:00 committed by GitHub
parent 82d8d02697
commit 253c5c7866
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 377 additions and 306 deletions

View file

@ -1,4 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { serializeLibraryAsJSON } from "../data/json";
import { t } from "../i18n";
import {
@ -14,12 +20,22 @@ import Spinner from "./Spinner";
import { duplicateElements } from "../element/newElement";
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
import { LibraryDropdownMenu } from "./LibraryMenuHeaderContent";
import LibraryMenuSection from "./LibraryMenuSection";
import {
LibraryMenuSection,
LibraryMenuSectionGrid,
} from "./LibraryMenuSection";
import { useScrollPosition } from "../hooks/useScrollPosition";
import { useLibraryCache } from "../hooks/useLibraryItemSvg";
import "./LibraryMenuItems.scss";
// using an odd number of items per batch so the rendering creates an irregular
// pattern which looks more organic
const ITEMS_RENDERED_PER_BATCH = 17;
// when render outputs cached we can render many more items per batch to
// speed it up
const CACHED_ITEMS_RENDERED_PER_BATCH = 64;
export default function LibraryMenuItems({
isLoading,
libraryItems,
@ -29,6 +45,8 @@ export default function LibraryMenuItems({
theme,
id,
libraryReturnUrl,
onSelectItems,
selectedItems,
}: {
isLoading: boolean;
libraryItems: LibraryItems;
@ -38,8 +56,9 @@ export default function LibraryMenuItems({
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
theme: UIAppState["theme"];
id: string;
selectedItems: LibraryItem["id"][];
onSelectItems: (id: LibraryItem["id"][]) => void;
}) {
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
const libraryContainerRef = useRef<HTMLDivElement>(null);
const scrollPosition = useScrollPosition<HTMLDivElement>(libraryContainerRef);
@ -49,13 +68,16 @@ export default function LibraryMenuItems({
libraryContainerRef.current?.scrollTo(0, scrollPosition);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const { svgCache } = useLibraryCache();
const unpublishedItems = libraryItems.filter(
(item) => item.status !== "published",
const { svgCache } = useLibraryCache();
const unpublishedItems = useMemo(
() => libraryItems.filter((item) => item.status !== "published"),
[libraryItems],
);
const publishedItems = libraryItems.filter(
(item) => item.status === "published",
const publishedItems = useMemo(
() => libraryItems.filter((item) => item.status === "published"),
[libraryItems],
);
const showBtn = !libraryItems.length && !pendingElements.length;
@ -69,50 +91,56 @@ export default function LibraryMenuItems({
LibraryItem["id"] | null
>(null);
const onItemSelectToggle = (
id: LibraryItem["id"],
event: React.MouseEvent,
) => {
const shouldSelect = !selectedItems.includes(id);
const onItemSelectToggle = useCallback(
(id: LibraryItem["id"], event: React.MouseEvent) => {
const shouldSelect = !selectedItems.includes(id);
const orderedItems = [...unpublishedItems, ...publishedItems];
const orderedItems = [...unpublishedItems, ...publishedItems];
if (shouldSelect) {
if (event.shiftKey && lastSelectedItem) {
const rangeStart = orderedItems.findIndex(
(item) => item.id === lastSelectedItem,
);
const rangeEnd = orderedItems.findIndex((item) => item.id === id);
if (shouldSelect) {
if (event.shiftKey && lastSelectedItem) {
const rangeStart = orderedItems.findIndex(
(item) => item.id === lastSelectedItem,
);
const rangeEnd = orderedItems.findIndex((item) => item.id === id);
if (rangeStart === -1 || rangeEnd === -1) {
setSelectedItems([...selectedItems, id]);
return;
if (rangeStart === -1 || rangeEnd === -1) {
onSelectItems([...selectedItems, id]);
return;
}
const selectedItemsMap = arrayToMap(selectedItems);
const nextSelectedIds = orderedItems.reduce(
(acc: LibraryItem["id"][], item, idx) => {
if (
(idx >= rangeStart && idx <= rangeEnd) ||
selectedItemsMap.has(item.id)
) {
acc.push(item.id);
}
return acc;
},
[],
);
onSelectItems(nextSelectedIds);
} else {
onSelectItems([...selectedItems, id]);
}
const selectedItemsMap = arrayToMap(selectedItems);
const nextSelectedIds = orderedItems.reduce(
(acc: LibraryItem["id"][], item, idx) => {
if (
(idx >= rangeStart && idx <= rangeEnd) ||
selectedItemsMap.has(item.id)
) {
acc.push(item.id);
}
return acc;
},
[],
);
setSelectedItems(nextSelectedIds);
setLastSelectedItem(id);
} else {
setSelectedItems([...selectedItems, id]);
setLastSelectedItem(null);
onSelectItems(selectedItems.filter((_id) => _id !== id));
}
setLastSelectedItem(id);
} else {
setLastSelectedItem(null);
setSelectedItems(selectedItems.filter((_id) => _id !== id));
}
};
},
[
lastSelectedItem,
onSelectItems,
publishedItems,
selectedItems,
unpublishedItems,
],
);
const getInsertedElements = useCallback(
(id: string) => {
@ -136,37 +164,45 @@ export default function LibraryMenuItems({
[libraryItems, selectedItems],
);
const onItemDrag = (id: LibraryItem["id"], event: React.DragEvent) => {
event.dataTransfer.setData(
MIME_TYPES.excalidrawlib,
serializeLibraryAsJSON(getInsertedElements(id)),
);
};
const onItemDrag = useCallback(
(id: LibraryItem["id"], event: React.DragEvent) => {
event.dataTransfer.setData(
MIME_TYPES.excalidrawlib,
serializeLibraryAsJSON(getInsertedElements(id)),
);
},
[getInsertedElements],
);
const isItemSelected = (id: LibraryItem["id"] | null) => {
if (!id) {
return false;
}
const isItemSelected = useCallback(
(id: LibraryItem["id"] | null) => {
if (!id) {
return false;
}
return selectedItems.includes(id);
};
return selectedItems.includes(id);
},
[selectedItems],
);
const onAddToLibraryClick = useCallback(() => {
onAddToLibrary(pendingElements);
}, [pendingElements, onAddToLibrary]);
const onItemClick = useCallback(
(id: LibraryItem["id"] | null) => {
if (!id) {
onAddToLibrary(pendingElements);
} else {
if (id) {
onInsertLibraryItems(getInsertedElements(id));
}
},
[
getInsertedElements,
onAddToLibrary,
onInsertLibraryItems,
pendingElements,
],
[getInsertedElements, onInsertLibraryItems],
);
const itemsRenderedPerBatch =
svgCache.size >= libraryItems.length
? CACHED_ITEMS_RENDERED_PER_BATCH
: ITEMS_RENDERED_PER_BATCH;
return (
<div
className="library-menu-items-container"
@ -181,7 +217,7 @@ export default function LibraryMenuItems({
{!isLibraryEmpty && (
<LibraryDropdownMenu
selectedItems={selectedItems}
onSelectItems={setSelectedItems}
onSelectItems={onSelectItems}
className="library-menu-dropdown-container--in-heading"
/>
)}
@ -225,20 +261,28 @@ export default function LibraryMenuItems({
</div>
</div>
) : (
<LibraryMenuSection
items={[
// append pending library item
...(pendingElements.length
? [{ id: null, elements: pendingElements }]
: []),
...unpublishedItems,
]}
onItemSelectToggle={onItemSelectToggle}
onItemDrag={onItemDrag}
onClick={onItemClick}
isItemSelected={isItemSelected}
svgCache={svgCache}
/>
<LibraryMenuSectionGrid>
{pendingElements.length > 0 && (
<LibraryMenuSection
itemsRenderedPerBatch={itemsRenderedPerBatch}
items={[{ id: null, elements: pendingElements }]}
onItemSelectToggle={onItemSelectToggle}
onItemDrag={onItemDrag}
onClick={onAddToLibraryClick}
isItemSelected={isItemSelected}
svgCache={svgCache}
/>
)}
<LibraryMenuSection
itemsRenderedPerBatch={itemsRenderedPerBatch}
items={unpublishedItems}
onItemSelectToggle={onItemSelectToggle}
onItemDrag={onItemDrag}
onClick={onItemClick}
isItemSelected={isItemSelected}
svgCache={svgCache}
/>
</LibraryMenuSectionGrid>
)}
</>
@ -251,14 +295,17 @@ export default function LibraryMenuItems({
</div>
)}
{publishedItems.length > 0 ? (
<LibraryMenuSection
items={publishedItems}
onItemSelectToggle={onItemSelectToggle}
onItemDrag={onItemDrag}
onClick={onItemClick}
isItemSelected={isItemSelected}
svgCache={svgCache}
/>
<LibraryMenuSectionGrid>
<LibraryMenuSection
itemsRenderedPerBatch={itemsRenderedPerBatch}
items={publishedItems}
onItemSelectToggle={onItemSelectToggle}
onItemDrag={onItemDrag}
onClick={onItemClick}
isItemSelected={isItemSelected}
svgCache={svgCache}
/>
</LibraryMenuSectionGrid>
) : unpublishedItems.length > 0 ? (
<div
style={{
@ -285,7 +332,7 @@ export default function LibraryMenuItems({
>
<LibraryDropdownMenu
selectedItems={selectedItems}
onSelectItems={setSelectedItems}
onSelectItems={onSelectItems}
/>
</LibraryMenuControlButtons>
)}