Repository navigation
[Perf] Memoize EuiProgress, EuiToken and EuiCard styles - #10183
Conversation
Share generated styles across instances with useEuiMemoizedStyles and compute EuiCodeBlock line styles once per block instead of once per line.
…styles Keeps the card image Emotion label unchanged.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Progress size getters still regenerate Emotion styles for every rendered instance.
Review effort: Balanced
Findings: 1
What changed in this PR
Memoizes component styles to reduce repeated Emotion generation and color calculations.
Changes:
- Adds shared per-variant style generators for Token, Progress, and Card.
- Memoizes theme-dependent component styles.
- Hoists CodeBlock line-style generation outside its line loop.
| File | Description |
|---|---|
token/token.tsx |
Uses memoized fill styles. |
token/token.styles.ts |
Adds static fill generators. |
progress/progress.tsx |
Uses memoized progress styles. |
progress/progress.styles.ts |
Adds determinate variant generators. |
code/utils.tsx |
Generates line styles once per block. |
card/card.tsx |
Uses memoized card and text styles. |
card/card.styles.ts |
Adds padding-specific generators. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const styles = useEuiMemoizedStyles( | ||
| determinate ? euiProgressNativeStyles : euiProgressIndeterminateStyles | ||
| ); |
There was a problem hiding this comment.
this is not important. the boost comes from expensive makeHighContrastColor and the native/indeterminate blocks
💚 Build Succeeded
History
|
💚 Build Succeeded
History
|
mgadewoll
left a comment
There was a problem hiding this comment.
🟢 LGTM. Thanks for the update!
| }; | ||
|
|
||
| // Static per-size generators so `useEuiMemoizedStyles` can share them across instances | ||
| export const euiCardStylesByPadding: Record< |
There was a problem hiding this comment.
💭 Non blocking, general thought: We're creating 6 cached style objects that are in large parts identical (only image/icon use paddingAmount). If we wanted to optimize further we could split shared styles and dynamic styles into separate memoized chunks. It reduces duplicate cached styles but adds a bit more complexity in code.
There was a problem hiding this comment.
@mgadewoll, I wanted to avoid changing the generated classes and markup to avoid jumping hurdles with the integration branch and a large EUI PR review :D Do you think it is a wrong approach to optimize for that in this case? I so, we can adjut the code
There was a problem hiding this comment.
We don't have to update anything additional in this PR. I just think we might want to ask what the end state of the style memoization should be: Are we fine with duplicated cached styles or do we opt to optimize that as well and is it worth it. That could be a separate update if we want to do it.
Just for transparency: the split wouldn't change any classes afaik, I'm thinking of something like this:
// card.tsx
const baseStyles = useEuiMemoizedStyles(euiCardStyles);
const paddingStyles = useEuiMemoizedStyles(
euiCardStylesByPadding[paddingSize]
);
const styles = { ...baseStyles, ...paddingStyles };
// card.styles.ts
export const euiCardStyles = (euiThemeContext: UseEuiTheme) => {
const { euiTheme } = euiThemeContext;
const spacing = euiPaddingSize(euiThemeContext, paddingKey);
const halfSpacing = euiPaddingSize(euiThemeContext, halfPaddingKey);
return {
card: {}
...
// no image or icon styles
}
}
export const euiCardDynamicStyles = (
euiThemeContext: UseEuiTheme,
paddingSize: EuiCardProps['paddingSize']
) => {
const { euiTheme } = euiThemeContext;
const paddingAmount = euiPaddingSize(euiThemeContext, paddingSize!);
const spacing = euiPaddingSize(euiThemeContext, paddingKey);
const halfSpacing = euiPaddingSize(euiThemeContext, halfPaddingKey);
return {
image: {
euiCard__image: css`
position: relative;
overflow: hidden;
/* Padding based sizing & negative margins */
${logicalCSS('width', `calc(100% + (${paddingAmount} * 2))`)}
${logicalCSS('left', `-${paddingAmount}`)}
${logicalCSS('top', `-${paddingAmount}`)}
/* Ensure the parent is only as tall as the image */
${logicalCSS('margin-bottom', `-${paddingAmount}`)}
/* Match border radius */
${logicalCSS(
'border-top-left-radius',
`calc(${euiTheme.border.radius.panel})`
)}
${logicalCSS(
'border-top-right-radius',
`calc(${euiTheme.border.radius.panel})`
)};
img {
${logicalCSS('width', '100%')}/* 4 */
}
`,
transparent: css`
border-radius: ${euiTheme.border.radius.panel};
`,
},
icon: {
euiCard__icon: css``,
withImage: css`
position: absolute;
${logicalCSS('top', '50%')}
${logicalCSS('left', '50%')}
/* Important needed to override current Sass styles on .euiIcon */
transform: translate(
-50%,
calc(-50% + -${paddingAmount})
) !important; /* stylelint-disable-line declaration-no-important */
`,
layout: {
vertical: css`
${logicalCSS('margin-top', halfSpacing)}
`,
horizontal: css`
${logicalCSS('margin-right', spacing)}
`,
},
},
};
};There was a problem hiding this comment.
Ah, i see! I guess these would worth it if a component has a lot of variations that are widely used sizes × colors × …

Part of #10147, part of #10152
Closes #10153
Closes #10162
Closes #10167
Summary
EuiProgress,EuiTokenandEuiCardregenerate their Emotion styles on every render of every instance. For Progress and Token that also means a lot of chroma color math (makeHighContrastColor,tint/shade), which is the expensive part. These are used widely in Kibana in lists: the chrome loading bar and field stats top values (Progress),FieldIconin the unified field list, doc viewer and data table headers (Token), Fleet integration cards (Card).This moves them to
useEuiMemoizedStyles, so the styles are generated once per theme and shared across all instances:euiProgressStylesdepends onisNative, so I added two static wrappers (euiProgressNativeStyles/euiProgressIndeterminateStyles) and pick one bydeterminate. Data and value text styles are memoized directly.useMemowith one static generator per fill (euiTokenFillStyles).euiCardStylesdepends onpaddingSize, so there's one static generator per padding size (euiCardStylesByPadding), same as the Token fills. Text styles are memoized directly.euiCardBetaBadgeStylesis left as is since it only runs for cards with a beta badge.euiCodeBlockLineStyleswas called once per line inside the line-wrapping loop; now it's called once per block. Not shared memoization, just hoisting, since it already runs inside auseMemo.No visual, API or DOM changes. Generators passed to
useEuiMemoizedStylesneed a name, so the per-variant maps are object literals rather than built withObject.fromEntries.Benchmark
React Profiler
actualDuration(ms, median), Storybook dev build, Chrome, same machine and session,mainvs this branch. Single run per side, so treat small differences as noise.EuiProgress(value,valueText,label)EuiProgress(indeterminate)EuiTokenEuiToken(mixed fills)EuiCardEuiCardwith image + iconEuiCodeBlockwith ~2,000 lines + line numbersEuiProgressEuiCardEuiTokenToken re-renders don't change because the old
useMemoalready covered re-renders of the same instance. The win there is on mount, when many tokens appear at once (field list, table headers).Methodology
<Profiler>and recordsactualDuration.flushSyncthat change one prop per item, drop the first 3 samples, take the median.iframe.htmldirectly and trigger the buttons from the console. "Before" is the same story with this PR's changes stashed. Both sides ran in the same Storybook session under the same conditions.Benchmark story
Runner (paste into the console on
iframe.html?id=bench-stylememo--mount):For
bench-stylememo--update, click eachu-*button once and take the median ofwindow.__bench[id].slice(3).API Changes
None.
Impact Assessment
No Kibana staging branch needed I think. The generated CSS and Emotion labels are identical, so class names don't change (no EUI snapshot changes either), and
useEuiMemoizedStylesfalls back to a default cache when there's noEuiProvider, same asEuiBadge/EuiAccordiontoday.