Skip to content

perf(core): memoize context provider values to prevent cascading re-renders#3366

Open
orrgottlieb wants to merge 2 commits into
masterfrom
perf/memoize-context-values
Open

perf(core): memoize context provider values to prevent cascading re-renders#3366
orrgottlieb wants to merge 2 commits into
masterfrom
perf/memoize-context-values

Conversation

@orrgottlieb

@orrgottlieb orrgottlieb commented May 21, 2026

Copy link
Copy Markdown
Contributor

User description

Summary

Stabilizes value references on the following Context providers so that consumers stop re-rendering when the value object is structurally identical:

  • LayerProvider (packages/components/layer)
  • HeadingTypographyContext (packages/components/typography) — hoisted to module-scope constant
  • AlertBannerContext (packages/core)
  • GridKeyboardNavigationContext (packages/core)
  • ListContext (packages/core)
  • TableContainerProvider (packages/core)
  • RelatedComponentsContext (packages/storybook-blocks)

Why

From a performance audit (item #15): when a Provider receives a fresh object literal each render (value={{ foo, bar }}), every consumer of the context re-renders — even when nothing in the value actually changed. For widely-used providers like Layer and Table, that cascade hits every nested element on every parent update.

Test plan

  • CI: existing tests pass
  • No public API surface change — same value shape in/out, only stable references

🤖 Generated with Claude Code


PR Type

Enhancement


Description

  • Memoize context provider values to prevent cascading re-renders

    • Wraps value props in useMemo or hoists to module-scoped constants
    • Affects LayerProvider, Heading's TypographyContext, AlertBannerContext, GridKeyboardNavigationContext, ListContext, TableContainerProvider, and RelatedComponentsContext
  • Fix AlertBannerContextType preservation after memoization

    • Exports AlertBannerContextType and annotates useMemo with explicit type
    • Prevents type widening of textColor union literal

Diagram Walkthrough

flowchart LR
  A["Context Providers<br/>with fresh object literals"] -->|"Apply useMemo<br/>or hoist to constant"| B["Stable value<br/>references"]
  B -->|"Prevents unnecessary<br/>consumer re-renders"| C["Performance<br/>improvement"]
Loading

File Walkthrough

Relevant files
Enhancement
AlertBannerContext.ts
Export AlertBannerContextType for type safety                       

packages/core/src/components/AlertBanner/AlertBannerContext.ts

  • Export AlertBannerContextType to allow type annotation in AlertBanner
    component
  • Enables explicit type preservation when wrapping context value in
    useMemo
+1/-1     
AlertBanner.tsx
Memoize AlertBanner context value with type annotation     

packages/core/src/components/AlertBanner/AlertBanner.tsx

  • Import AlertBannerContextType from AlertBannerContext
  • Wrap context value in useMemo with explicit type annotation to
    preserve textColor union literal
  • Prevents type widening that would break context type assignability
+3/-2     
GridKeyboardNavigationContext.ts
Memoize GridKeyboardNavigation context return value           

packages/core/src/components/GridKeyboardNavigationContext/GridKeyboardNavigationContext.ts

  • Wrap return value in useMemo to stabilize context object reference
  • Memoizes based on onOutboundNavigation dependency
+1/-1     
List.tsx
Memoize List context value to prevent re-renders                 

packages/core/src/components/List/List.tsx

  • Create listContextValue variable with useMemo to stabilize context
    reference
  • Memoizes based on updateFocusedItem dependency
  • Pass memoized value to ListContext.Provider
+3/-1     
TableContainer.tsx
Memoize TableContainer context value                                         

packages/core/src/components/Table/TableContainer/TableContainer.tsx

  • Import useMemo hook
  • Create tableContainerContextValue with useMemo to stabilize context
    reference
  • Empty dependency array since menuContainerRef is created once per
    component instance
+3/-2     
LayerProvider.tsx
Memoize LayerProvider context value                                           

packages/components/layer/src/LayerProvider/LayerProvider.tsx

  • Import useMemo hook
  • Create value variable with useMemo to stabilize context reference
  • Memoizes based on layerRef dependency
+3/-2     
Heading.tsx
Hoist Heading TypographyContext value to constant               

packages/components/typography/src/Heading/Heading.tsx

  • Create module-scoped constant HEADING_TYPOGRAPHY_CONTEXT_VALUE for
    TypographyContext value
  • Replace inline object literal with constant reference in
    TypographyContext.Provider
  • Ensures stable reference across all Heading renders
+2/-1     
related-components.tsx
Memoize RelatedComponents context value                                   

packages/storybook-blocks/src/components/related-components/related-components.tsx

  • Create relatedComponentsContextValue with useMemo to stabilize context
    reference
  • Memoizes based on linkTarget dependency
  • Pass memoized value to RelatedComponentsContext.Provider
+3/-1     

Wraps unmemoized `value` props on `LayerProvider`, `Heading`'s
`TypographyContext`, `AlertBannerContext`, `GridKeyboardNavigationContext`,
`ListContext`, `TableContainerProvider`, and `RelatedComponentsContext`
in `useMemo` (or hoists to a module-scoped frozen constant where the
value never depends on render-time state).

When a Provider receives a fresh object literal each render, every
consumer of that context re-renders even when nothing in the value
actually changed. For widely-used providers like Layer and Table,
that cascade hits every nested element on every parent update.

Audit finding #15.
@orrgottlieb orrgottlieb requested a review from a team as a code owner May 21, 2026 23:05
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Previous review results

Review updated until commit 46800f0

Results up to commit da05ec9


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Qodo Logo

Wrapping the context value in useMemo widened textColor's inferred type
to string, breaking AlertBannerContextType assignability. Annotate the
useMemo with the explicit context type so the literal union is preserved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 46800f0

@github-actions

Copy link
Copy Markdown
Contributor

📦 Bundle Size Analysis

✅ No bundle size changes detected.

Unchanged Components
Component Base PR Diff
@vibe/button 17.3KB 17.29KB -9B 🟢
@vibe/clickable 5.95KB 5.96KB +3B 🔺
@vibe/dialog 52.14KB 52.14KB -2B 🟢
@vibe/icon-button 66.09KB 66.11KB +25B 🔺
@vibe/icon 12.92KB 12.89KB -32B 🟢
@vibe/layer 2.96KB 2.97KB +16B 🔺
@vibe/layout 9.82KB 9.83KB +11B 🔺
@vibe/loader 5.64KB 5.65KB +10B 🔺
@vibe/tooltip 61.33KB 61.25KB -85B 🟢
@vibe/typography 63.47KB 63.44KB -36B 🟢
Accordion 6.31KB 6.29KB -14B 🟢
AccordionItem 66.43KB 66.42KB -14B 🟢
AlertBanner 70.83KB 70.8KB -29B 🟢
AlertBannerButton 18.76KB 18.76KB -2B 🟢
AlertBannerLink 15.26KB 15.26KB +4B 🔺
AlertBannerText 63.95KB 63.99KB +43B 🔺
AttentionBox 74.35KB 74.29KB -65B 🟢
Avatar 66.84KB 66.77KB -74B 🟢
AvatarGroup 93.29KB 93.28KB -9B 🟢
Badge 43.19KB 43.17KB -24B 🟢
BreadcrumbItem 64.7KB 64.62KB -85B 🟢
BreadcrumbMenu 68.57KB 68.55KB -24B 🟢
BreadcrumbMenuItem 77.07KB 77.05KB -23B 🟢
BreadcrumbsBar 5.68KB 5.68KB -1B 🟢
ButtonGroup 68.32KB 68.3KB -17B 🟢
Checkbox 66.83KB 66.83KB +8B 🔺
Chips 75.05KB 75.09KB +40B 🔺
ColorPicker 74.47KB 74.45KB -26B 🟢
ColorPickerContent 73.73KB 73.76KB +29B 🔺
Combobox 84.08KB 83.97KB -111B 🟢
Counter 42.21KB 42.28KB +65B 🔺
DatePicker 112.41KB 112.44KB +26B 🔺
Divider 5.42KB 5.46KB +44B 🔺
Dropdown 95.35KB 95.3KB -54B 🟢
EditableHeading 66.63KB 66.57KB -64B 🟢
EditableText 66.46KB 66.47KB +11B 🔺
EmptyState 70.48KB 70.53KB +47B 🔺
ExpandCollapse 66.22KB 66.28KB +58B 🔺
FormattedNumber 5.86KB 5.84KB -13B 🟢
GridKeyboardNavigationContext 4.65KB 4.66KB +9B 🔺
HiddenText 5.4KB 5.39KB -15B 🟢
Info 72.06KB 72.09KB +30B 🔺
Label 68.65KB 68.68KB +25B 🔺
Link 14.91KB 14.88KB -30B 🟢
List 72.88KB 72.9KB +17B 🔺
ListItem 65.54KB 65.5KB -42B 🟢
ListItemAvatar 66.88KB 66.86KB -21B 🟢
ListItemIcon 13.97KB 13.97KB +5B 🔺
ListTitle 65.02KB 65.08KB +61B 🔺
Menu 8.65KB 8.64KB -19B 🟢
MenuDivider 5.56KB 5.57KB +7B 🔺
MenuGridItem 7.16KB 7.19KB +31B 🔺
MenuItem 76.95KB 76.92KB -33B 🟢
MenuItemButton 70.11KB 70.09KB -21B 🟢
MenuTitle 65.35KB 65.32KB -33B 🟢
MenuButton 66.08KB 66.1KB +24B 🔺
Modal 79.14KB 79.05KB -88B 🟢
ModalContent 4.72KB 4.71KB -1B 🟢
ModalHeader 65.79KB 65.85KB +63B 🔺
ModalMedia 7.51KB 7.5KB -3B 🟢
ModalFooter 67.72KB 67.67KB -48B 🟢
ModalFooterWizard 68.6KB 68.54KB -55B 🟢
ModalBasicLayout 8.96KB 8.9KB -57B 🟢
ModalMediaLayout 8.08KB 8.06KB -19B 🟢
ModalSideBySideLayout 6.3KB 6.29KB -4B 🟢
MultiStepIndicator 52.96KB 52.95KB -11B 🟢
NumberField 72.87KB 72.88KB +8B 🔺
ProgressBar 7.34KB 7.35KB +7B 🔺
RadioButton 65.9KB 65.89KB -14B 🟢
Search 70.65KB 70.68KB +28B 🔺
Skeleton 6KB 6.01KB +4B 🔺
Slider 73.86KB 73.91KB +53B 🔺
SplitButton 66.48KB 66.52KB +43B 🔺
SplitButtonMenu 8.8KB 8.76KB -34B 🟢
Steps 71.31KB 71.34KB +33B 🔺
Table 7.26KB 7.25KB -13B 🟢
TableBody 66.68KB 66.79KB +116B 🔺
TableCell 65.22KB 65.23KB +2B 🔺
TableContainer 5.31KB 5.34KB +35B 🔺
TableHeader 5.64KB 5.64KB +1B 🔺
TableHeaderCell 72.2KB 72.11KB -90B 🟢
TableRow 5.56KB 5.55KB -8B 🟢
TableRowMenu 68.87KB 68.86KB -16B 🟢
TableVirtualizedBody 71.42KB 71.43KB +10B 🔺
Tab 64KB 64KB 0B ➖
TabList 8.89KB 8.86KB -30B 🟢
TabPanel 5.3KB 5.29KB -14B 🟢
TabPanels 5.86KB 5.86KB -2B 🟢
TabsContext 5.48KB 5.51KB +29B 🔺
TextArea 66.26KB 66.31KB +54B 🔺
TextField 69.43KB 69.44KB +8B 🔺
TextWithHighlight 64.35KB 64.39KB +37B 🔺
ThemeProvider 4.36KB 4.36KB -1B 🟢
Tipseen 71.17KB 71.23KB +61B 🔺
TipseenContent 71.6KB 71.64KB +37B 🔺
TipseenMedia 71.27KB 71.3KB +29B 🔺
TipseenWizard 73.93KB 73.86KB -76B 🟢
Toast 74.1KB 74.05KB -53B 🟢
ToastButton 18.59KB 18.62KB +33B 🔺
ToastLink 15.05KB 15.08KB +31B 🔺
Toggle 66.62KB 66.6KB -12B 🟢
TransitionView 5.42KB 5.45KB +30B 🔺
VirtualizedGrid 12.54KB 12.54KB +2B 🔺
VirtualizedList 12.28KB 12.26KB -12B 🟢
List (Next) 8.17KB 8.16KB -15B 🟢
ListItem (Next) 69.88KB 69.79KB -87B 🟢
ListTitle (Next) 65.31KB 65.27KB -45B 🟢

📊 Summary:

  • Total Base Size: 4.75MB
  • Total PR Size: 4.75MB
  • Total Difference: 432B

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant