diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..dbac76a --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-06-30 - Granular Zustand Selectors and Konva Performance +**Learning:** Destructuring the entire Zustand state object (e.g., `const { a, b } = useStore()`) inside React components causes the component to re-render whenever *any* state in the store changes, rather than just the properties being used. Furthermore, in React Konva applications, updating a single property on one canvas element can trigger O(N) re-renders for all elements on the canvas unless wrapped properly. +**Action:** Always use granular selectors (e.g., `const a = useStore(state => state.a)`) instead of destructuring the state object. Always wrap list items, especially Konva canvas elements (`ElementWrapper`), in `React.memo` to prevent O(N) re-renders when a single element updates. diff --git a/frontend/src/components/CanvasArea.tsx b/frontend/src/components/CanvasArea.tsx index ae81602..676f41b 100644 --- a/frontend/src/components/CanvasArea.tsx +++ b/frontend/src/components/CanvasArea.tsx @@ -10,7 +10,10 @@ interface ElementWrapperProps { onChange: (newAttrs: Partial) => void; } -const ElementWrapper: React.FC = ({ element, isSelected, onSelect, onChange }) => { +// ⚡ BOLT OPTIMIZATION: Wrap ElementWrapper in React.memo +// Prevents O(N) re-renders of all canvas elements when a single element is moved or selected. +// Expected measurable impact: Drastically reduces CPU time and frame drops during drag/drop or selection in documents with many elements. +const ElementWrapper: React.FC = React.memo(({ element, isSelected, onSelect, onChange }) => { const shapeRef = useRef(null); const trRef = useRef(null); @@ -71,10 +74,17 @@ const ElementWrapper: React.FC = ({ element, isSelected, on // Placeholder for tables return null; -}; +}); export const CanvasArea = () => { - const { pages, activePageId, selectedElementId, selectElement, updateElement } = useEditorStore(); + // ⚡ BOLT OPTIMIZATION: Use granular selectors for Zustand state instead of destructuring the entire state object. + // This prevents unnecessary re-renders when other un-related state properties change. + // Expected measurable impact: Eliminates unnecessary re-renders of the CanvasArea component when unrelated state changes. + const pages = useEditorStore(state => state.pages); + const activePageId = useEditorStore(state => state.activePageId); + const selectedElementId = useEditorStore(state => state.selectedElementId); + const selectElement = useEditorStore(state => state.selectElement); + const updateElement = useEditorStore(state => state.updateElement); const activePage = pages.find(p => p.id === activePageId) || pages[0]; const [scale] = useState(1); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 8ec7193..99a6347 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,7 +1,14 @@ import { useEditorStore } from '../store/useEditorStore'; export const Sidebar = () => { - const { pages, activePageId, addPage, setActivePage, selectedElementId } = useEditorStore(); + // ⚡ BOLT OPTIMIZATION: Use granular selectors for Zustand state instead of destructuring the entire state object. + // This prevents unnecessary re-renders when other un-related state properties change. + // Expected measurable impact: Eliminates unnecessary re-renders of the Sidebar component when unrelated state changes. + const pages = useEditorStore(state => state.pages); + const activePageId = useEditorStore(state => state.activePageId); + const addPage = useEditorStore(state => state.addPage); + const setActivePage = useEditorStore(state => state.setActivePage); + const selectedElementId = useEditorStore(state => state.selectedElementId); return (
diff --git a/frontend/src/components/Toolbar.tsx b/frontend/src/components/Toolbar.tsx index efd1490..7ff1ac8 100644 --- a/frontend/src/components/Toolbar.tsx +++ b/frontend/src/components/Toolbar.tsx @@ -3,7 +3,11 @@ import { useEditorStore } from '../store/useEditorStore'; import type { TextElement } from '../types'; export const Toolbar = () => { - const { addElement, activePageId } = useEditorStore(); + // ⚡ BOLT OPTIMIZATION: Use granular selectors for Zustand state instead of destructuring the entire state object. + // This prevents unnecessary re-renders when other un-related state properties change. + // Expected measurable impact: Eliminates unnecessary re-renders of the Toolbar component when unrelated state changes. + const addElement = useEditorStore(state => state.addElement); + const activePageId = useEditorStore(state => state.activePageId); const handleAddText = () => { if (!activePageId) return;