perf: Improved pointer events related performance when the sidebar is docked with a large library open (#9086)

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
feat/remove-ga
tothatt81 3 weeks ago committed by Mark Tolmacs
parent b4920f6b20
commit 3ad47df3c8
No known key found for this signature in database

@ -2,8 +2,9 @@ import React, {
useState,
useCallback,
useMemo,
useRef,
useEffect,
memo,
useRef,
} from "react";
import type Library from "../data/library";
import {
@ -17,6 +18,7 @@ import type {
LibraryItem,
ExcalidrawProps,
UIAppState,
AppClassProperties,
} from "../types";
import LibraryMenuItems from "./LibraryMenuItems";
import { trackEvent } from "../analytics";
@ -33,9 +35,12 @@ import { useUIAppState } from "../context/ui-appState";
import "./LibraryMenu.scss";
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
import { isShallowEqual } from "../utils";
import type { NonDeletedExcalidrawElement } from "../element/types";
import type {
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "../element/types";
import { LIBRARY_DISABLED_TYPES } from "../constants";
import { isShallowEqual } from "../utils";
export const isLibraryMenuOpenAtom = atom(false);
@ -43,7 +48,8 @@ const LibraryMenuWrapper = ({ children }: { children: React.ReactNode }) => {
return <div className="layer-ui__library">{children}</div>;
};
export const LibraryMenuContent = ({
const LibraryMenuContent = memo(
({
onInsertLibraryItems,
pendingElements,
onAddToLibrary,
@ -150,63 +156,107 @@ export const LibraryMenuContent = ({
)}
</LibraryMenuWrapper>
);
};
},
);
const usePendingElementsMemo = (
appState: UIAppState,
const getPendingElements = (
elements: readonly NonDeletedExcalidrawElement[],
) => {
const create = useCallback(
(appState: UIAppState, elements: readonly NonDeletedExcalidrawElement[]) =>
getSelectedElements(elements, appState, {
selectedElementIds: UIAppState["selectedElementIds"],
) => ({
elements,
pending: getSelectedElements(
elements,
{ selectedElementIds },
{
includeBoundTextElement: true,
includeElementsInFrames: true,
}),
[],
},
),
selectedElementIds,
});
const usePendingElementsMemo = (
appState: UIAppState,
app: AppClassProperties,
) => {
const elements = useExcalidrawElements();
const [state, setState] = useState(() =>
getPendingElements(elements, appState.selectedElementIds),
);
const selectedElementVersions = useRef(
new Map<ExcalidrawElement["id"], ExcalidrawElement["version"]>(),
);
const val = useRef(create(appState, elements));
const prevAppState = useRef<UIAppState>(appState);
const prevElements = useRef(elements);
useEffect(() => {
for (const element of state.pending) {
selectedElementVersions.current.set(element.id, element.version);
}
}, [state.pending]);
const update = useCallback(() => {
useEffect(() => {
if (
!isShallowEqual(
appState.selectedElementIds,
prevAppState.current.selectedElementIds,
) ||
!isShallowEqual(elements, prevElements.current)
// Only update once pointer is released.
// Reading directly from app.state to make it clear it's not reactive
// (hence, there's potential for stale state)
app.state.cursorButton === "up" &&
app.state.activeTool.type === "selection"
) {
setState((prev) => {
// if selectedElementIds changed, we don't have to compare versions
// ---------------------------------------------------------------------
if (
!isShallowEqual(prev.selectedElementIds, appState.selectedElementIds)
) {
val.current = create(appState, elements);
prevAppState.current = appState;
prevElements.current = elements;
selectedElementVersions.current.clear();
return getPendingElements(elements, appState.selectedElementIds);
}
}, [create, appState, elements]);
return useMemo(
() => ({
update,
value: val.current,
}),
[update, val],
);
// otherwise we need to check whether selected elements changed
// ---------------------------------------------------------------------
const elementsMap = app.scene.getNonDeletedElementsMap();
for (const id of Object.keys(appState.selectedElementIds)) {
const currVersion = elementsMap.get(id)?.version;
if (
currVersion &&
currVersion !== selectedElementVersions.current.get(id)
) {
// we can't update the selectedElementVersions in here
// because of double render in StrictMode which would overwrite
// the state in the second pass with the old `prev` state.
// Thus, we update versions in a separate effect. May create
// a race condition since current effect is not fully reactive.
return getPendingElements(elements, appState.selectedElementIds);
}
}
// nothing changed
// ---------------------------------------------------------------------
return prev;
});
}
}, [
app,
app.state.cursorButton,
app.state.activeTool.type,
appState.selectedElementIds,
elements,
]);
return state.pending;
};
/**
* This component is meant to be rendered inside <Sidebar.Tab/> inside our
* <DefaultSidebar/> or host apps Sidebar components.
*/
export const LibraryMenu = () => {
const { library, id, onInsertElements } = useApp();
export const LibraryMenu = memo(() => {
const app = useApp();
const { onInsertElements } = app;
const appProps = useAppProps();
const appState = useUIAppState();
const app = useApp();
const setAppState = useExcalidrawSetAppState();
const elements = useExcalidrawElements();
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
const memoizedLibrary = useMemo(() => library, [library]);
// BUG: pendingElements are still causing some unnecessary rerenders because clicking into canvas returns some ids even when no element is selected.
const pendingElements = usePendingElementsMemo(appState, elements);
const memoizedLibrary = useMemo(() => app.library, [app.library]);
const pendingElements = usePendingElementsMemo(appState, app);
const onInsertLibraryItems = useCallback(
(libraryItems: LibraryItems) => {
@ -223,22 +273,18 @@ export const LibraryMenu = () => {
});
}, [setAppState]);
useEffect(() => {
return app.onPointerUpEmitter.on(() => pendingElements.update());
}, [app, pendingElements]);
return (
<LibraryMenuContent
pendingElements={pendingElements.value}
pendingElements={pendingElements}
onInsertLibraryItems={onInsertLibraryItems}
onAddToLibrary={deselectItems}
setAppState={setAppState}
libraryReturnUrl={appProps.libraryReturnUrl}
library={memoizedLibrary}
id={id}
id={app.id}
theme={appState.theme}
selectedItems={selectedItems}
onSelectItems={setSelectedItems}
/>
);
};
});

Loading…
Cancel
Save