From c29718de6204d544713cad496e9ebe141b769cca Mon Sep 17 00:00:00 2001 From: Benjamin Kindle Date: Sat, 24 Feb 2024 15:28:04 -0500 Subject: [PATCH 1/2] perf(manager): improve performance when switching stories 3 changes that add up to a moderate speed-up when switching stories, especially in projects where there are many (several hundred) stories. 1. Fuse and list within the Search component are now only calculated when needed (when the value of the search input changes), not on each render. 2. The value returned by LayoutProvider is now memoized. This has a moderate impact because this hook is used in Node within the sidebar, which may have hundreds of instances. Before this change, useLayout was causing Node to rerender every time, making its React.memo useless. 3. The list of items inside Tree is now memoized. This is helpful because this list is made by mapping over a very large list --- .../src/components/layout/LayoutProvider.tsx | 39 ++++---- .../manager/src/components/sidebar/Search.tsx | 10 +- .../manager/src/components/sidebar/Tree.tsx | 96 +++++++++++-------- 3 files changed, 85 insertions(+), 60 deletions(-) diff --git a/code/ui/manager/src/components/layout/LayoutProvider.tsx b/code/ui/manager/src/components/layout/LayoutProvider.tsx index ae088f6358d0..81c342e0546c 100644 --- a/code/ui/manager/src/components/layout/LayoutProvider.tsx +++ b/code/ui/manager/src/components/layout/LayoutProvider.tsx @@ -1,5 +1,5 @@ import type { FC, PropsWithChildren } from 'react'; -import React, { createContext, useContext, useState } from 'react'; +import React, { createContext, useContext, useMemo, useState } from 'react'; import { useMediaQuery } from '../hooks/useMedia'; import { BREAKPOINT } from '../../constants'; @@ -32,22 +32,29 @@ export const LayoutProvider: FC = ({ children }) => { const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINT}px)`); const isMobile = !isDesktop; - return ( - - {children} - + const contextValue = useMemo( + () => ({ + isMobileMenuOpen, + setMobileMenuOpen, + isMobileAboutOpen, + setMobileAboutOpen, + isMobilePanelOpen, + setMobilePanelOpen, + isDesktop, + isMobile, + }), + [ + isMobileMenuOpen, + setMobileMenuOpen, + isMobileAboutOpen, + setMobileAboutOpen, + isMobilePanelOpen, + setMobilePanelOpen, + isDesktop, + isMobile, + ] ); + return {children}; }; export const useLayout = () => useContext(LayoutContext); diff --git a/code/ui/manager/src/components/sidebar/Search.tsx b/code/ui/manager/src/components/sidebar/Search.tsx index cbbecc8264a6..3939d1ea2a37 100644 --- a/code/ui/manager/src/components/sidebar/Search.tsx +++ b/code/ui/manager/src/components/sidebar/Search.tsx @@ -176,8 +176,8 @@ export const Search = React.memo<{ [api, inputRef, showAllComponents, DEFAULT_REF_ID] ); - const list: SearchItem[] = useMemo(() => { - return dataset.entries.reduce((acc, [refId, { index, status }]) => { + const makeFuse = useCallback(() => { + const list = dataset.entries.reduce((acc, [refId, { index, status }]) => { const groupStatus = getGroupStatus(index || {}, status); if (index) { @@ -196,12 +196,12 @@ export const Search = React.memo<{ } return acc; }, []); + return new Fuse(list, options); }, [dataset]); - const fuse = useMemo(() => new Fuse(list, options), [list]); - const getResults = useCallback( (input: string) => { + const fuse = makeFuse(); if (!input) return []; let results: DownshiftItem[] = []; @@ -229,7 +229,7 @@ export const Search = React.memo<{ return results; }, - [allComponents, fuse] + [allComponents, makeFuse] ); const stateReducer = useCallback( diff --git a/code/ui/manager/src/components/sidebar/Tree.tsx b/code/ui/manager/src/components/sidebar/Tree.tsx index d1a566bddf4c..42aa27ce694a 100644 --- a/code/ui/manager/src/components/sidebar/Tree.tsx +++ b/code/ui/manager/src/components/sidebar/Tree.tsx @@ -481,55 +481,73 @@ export const Tree = React.memo<{ const groupStatus = useMemo(() => getGroupStatus(collapsedData, status), [collapsedData, status]); - return ( - 0}> - - {collapsedItems.map((itemId) => { - const item = collapsedData[itemId]; - const id = createId(itemId, refId); - - if (item.type === 'root') { - const descendants = expandableDescendants[item.id]; - const isFullyExpanded = descendants.every((d: string) => expanded[d]); - return ( - // @ts-expect-error (TODO) - - ); - } - - const isDisplayed = !item.parent || ancestry[itemId].every((a: string) => expanded[a]); - const color = groupStatus[itemId] ? statusMapping[groupStatus[itemId]][1] : null; - + const treeItems = useMemo(() => { + return collapsedItems.map((itemId) => { + const item = collapsedData[itemId]; + const id = createId(itemId, refId); + + if (item.type === 'root') { + const descendants = expandableDescendants[item.id]; + const isFullyExpanded = descendants.every((d: string) => expanded[d]); return ( - itemId === oid || itemId.startsWith(`${oid}-`))} - isDisplayed={isDisplayed} + isOrphan={false} + isDisplayed isSelected={selectedStoryId === itemId} isExpanded={!!expanded[itemId]} setExpanded={setExpanded} + isFullyExpanded={isFullyExpanded} + expandableDescendants={descendants} onSelectStoryId={onSelectStoryId} /> ); - })} + } + + const isDisplayed = !item.parent || ancestry[itemId].every((a: string) => expanded[a]); + const color = groupStatus[itemId] ? statusMapping[groupStatus[itemId]][1] : null; + + return ( + itemId === oid || itemId.startsWith(`${oid}-`))} + isDisplayed={isDisplayed} + isSelected={selectedStoryId === itemId} + isExpanded={!!expanded[itemId]} + setExpanded={setExpanded} + onSelectStoryId={onSelectStoryId} + /> + ); + }); + }, [ + ancestry, + api, + collapsedData, + collapsedItems, + docsMode, + expandableDescendants, + expanded, + groupStatus, + onSelectStoryId, + orphanIds, + refId, + selectedStoryId, + setExpanded, + status, + ]); + return ( + 0}> + + {treeItems} ); }); From aefa17aa3e2c5ee87153b58fae77202cd24a8975 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 27 Feb 2024 14:08:40 +0100 Subject: [PATCH 2/2] cleanup --- code/ui/manager/src/components/sidebar/Search.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/ui/manager/src/components/sidebar/Search.tsx b/code/ui/manager/src/components/sidebar/Search.tsx index 3939d1ea2a37..0df164a2d35b 100644 --- a/code/ui/manager/src/components/sidebar/Search.tsx +++ b/code/ui/manager/src/components/sidebar/Search.tsx @@ -5,7 +5,7 @@ import Downshift from 'downshift'; import type { FuseOptions } from 'fuse.js'; import Fuse from 'fuse.js'; import { global } from '@storybook/global'; -import React, { useMemo, useRef, useState, useCallback } from 'react'; +import React, { useRef, useState, useCallback } from 'react'; import { CloseIcon, SearchIcon } from '@storybook/icons'; import { DEFAULT_REF_ID } from './Sidebar'; import type {