diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..89d7f05 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,4 @@ +## 2024-06-25 - Excalidraw Memoization + +**Learning:** The `@excalidraw/excalidraw` package's `Excalidraw` component is exceptionally expensive to re-render. Even though `EditorCanvas` was wrapped in `React.memo`, passing an inline arrow function to `onExcalidrawAPI` in the parent `App` broke memoization, causing severe input lag when typing in completely independent UI elements like the codeblock editor sidebar due to the entire canvas re-rendering. +**Action:** When passing callbacks to heavy third-party components like Excalidraw, always wrap them in `useCallback` hook to preserve their prop stability and maintain `React.memo` benefits, preventing disastrous performance regressions on typing/input. diff --git a/src/client/App.tsx b/src/client/App.tsx index 33ddb41..2035bde 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -307,6 +307,10 @@ function PrivateApp() { [], ); + const handleExcalidrawAPI = useCallback((api: ExcalidrawImperativeAPI) => { + excalidrawApiRef.current = api; + }, []); + return (
{toastMessage && ( @@ -332,9 +336,7 @@ function PrivateApp() { onSceneChange={handleSceneChange} onSelectionStateChange={handleCodeBlockSelectionChange} onEditorActivity={scheduleThemeTokenSync} - onExcalidrawAPI={(api) => { - excalidrawApiRef.current = api; - }} + onExcalidrawAPI={handleExcalidrawAPI} renderEmbeddable={renderCodeBlockEmbeddable} /> void deleteDrawing(drawingId)} + onDelete={deleteDrawing} onTitleChange={setActiveTitle} - onTitleSubmit={() => void submitTitle()} + onTitleSubmit={submitTitle} onPublicationSlugChange={setPublicationSlug} - onPublish={() => void publishPublication()} - onDisablePublication={() => void disablePublication()} + onPublish={publishPublication} + onDisablePublication={disablePublication} />
); diff --git a/src/client/components/DrawingSidebar.tsx b/src/client/components/DrawingSidebar.tsx index 824b1eb..fdd94f2 100644 --- a/src/client/components/DrawingSidebar.tsx +++ b/src/client/components/DrawingSidebar.tsx @@ -1,3 +1,4 @@ +import { memo } from "react"; import { type DrawingMeta, type DrawingPublication } from "../../core/shared"; type DrawingSidebarProps = { @@ -9,14 +10,14 @@ type DrawingSidebarProps = { publicationSlug: string; publicationBusy: boolean; onClose: () => void; - onCreate: () => void; - onSelect: (drawingId: string) => void; - onDelete: (drawingId: string) => void; + onCreate: () => void | Promise; + onSelect: (drawingId: string) => void | Promise; + onDelete: (drawingId: string) => void | Promise; onTitleChange: (title: string) => void; - onTitleSubmit: () => void; + onTitleSubmit: () => void | Promise; onPublicationSlugChange: (slug: string) => void; - onPublish: () => void; - onDisablePublication: () => void; + onPublish: () => void | Promise; + onDisablePublication: () => void | Promise; }; function DrawerIcon() { @@ -64,7 +65,7 @@ export function DrawingsToggle({ onClick }: { onClick: () => void }) { ); } -export function DrawingSidebar({ +export const DrawingSidebar = memo(function DrawingSidebar({ open, drawings, activeId, @@ -133,7 +134,7 @@ export function DrawingSidebar({ className="title-input" value={activeTitle} onChange={(event) => onTitleChange(event.target.value)} - onBlur={onTitleSubmit} + onBlur={() => void onTitleSubmit()} onKeyDown={(event) => { if (event.key === "Enter") { event.currentTarget.blur(); @@ -143,7 +144,7 @@ export function DrawingSidebar({ @@ -214,7 +215,7 @@ export function DrawingSidebar({