From ed8965132dc5ec739ea16a756b15178b6057a4a1 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 6 Oct 2026 15:44:29 +0200 Subject: [PATCH 1/2] Memoize EuiProgress, EuiToken and EuiCard styles Share generated styles across instances with useEuiMemoizedStyles and compute EuiCodeBlock line styles once per block instead of once per line. --- .../card/__snapshots__/card.test.tsx.snap | 2 +- .../eui/src/components/card/card.styles.ts | 44 ++++++++++++------- packages/eui/src/components/card/card.tsx | 13 +++++- packages/eui/src/components/code/utils.tsx | 2 +- .../components/progress/progress.styles.ts | 6 +++ .../eui/src/components/progress/progress.tsx | 17 ++++--- .../eui/src/components/token/token.styles.ts | 10 +++++ packages/eui/src/components/token/token.tsx | 15 +++---- 8 files changed, 74 insertions(+), 35 deletions(-) diff --git a/packages/eui/src/components/card/__snapshots__/card.test.tsx.snap b/packages/eui/src/components/card/__snapshots__/card.test.tsx.snap index ef1b924e8e7b..a0ff2cf97196 100644 --- a/packages/eui/src/components/card/__snapshots__/card.test.tsx.snap +++ b/packages/eui/src/components/card/__snapshots__/card.test.tsx.snap @@ -908,7 +908,7 @@ exports[`EuiCard props image 1`] = ` class="euiCard__top emotion-euiCard__top-vertical" >
{ +export const euiCardStyles = (euiThemeContext: UseEuiTheme) => { const { euiTheme } = euiThemeContext; - const paddingAmount = euiPaddingSize(euiThemeContext, paddingSize!); const spacing = euiPaddingSize(euiThemeContext, paddingKey); const halfSpacing = euiPaddingSize(euiThemeContext, halfPaddingKey); @@ -147,13 +143,6 @@ export const euiCardStyles = ( 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', @@ -180,11 +169,6 @@ export const euiCardStyles = ( 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: { @@ -199,6 +183,32 @@ export const euiCardStyles = ( }; }; +// Kept out of `euiCardStyles` so the rest can be memoized independently of `paddingSize` +export const euiCardImagePaddingStyles = ( + euiThemeContext: UseEuiTheme, + paddingSize: EuiCardProps['paddingSize'] +) => { + const paddingAmount = euiPaddingSize(euiThemeContext, paddingSize!); + + return { + image: css` + /* 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}`)} + `, + iconWithImage: css` + /* Important needed to override current Sass styles on .euiIcon */ + transform: translate( + -50%, + calc(-50% + -${paddingAmount}) + ) !important; /* stylelint-disable-line declaration-no-important */ + `, + }; +}; + export const euiCardTextStyles = (euiThemeContext: UseEuiTheme) => { const { euiTheme } = euiThemeContext; return { diff --git a/packages/eui/src/components/card/card.tsx b/packages/eui/src/components/card/card.tsx index 7bac29b6d6a7..db3f2ca974e2 100644 --- a/packages/eui/src/components/card/card.tsx +++ b/packages/eui/src/components/card/card.tsx @@ -18,6 +18,7 @@ import classNames from 'classnames'; import { getSecureRelForTarget, useEuiTheme, + useEuiMemoizedStyles, cloneElementWithCss, } from '../../services'; import { useGeneratedHtmlId } from '../../services/accessibility'; @@ -34,6 +35,7 @@ import { EuiSpacer } from '../spacer'; import { EuiCardSelect, EuiCardSelectProps } from './card_select'; import { euiCardBetaBadgeStyles, + euiCardImagePaddingStyles, euiCardStyles, euiCardTextStyles, } from './card.styles'; @@ -170,7 +172,7 @@ export const EuiCard: FunctionComponent = ({ !isDisabled && (onClick || href || (selectable && !selectable.isDisabled)); const euiThemeContext = useEuiTheme(); - const styles = euiCardStyles(euiThemeContext, paddingSize); + const styles = useEuiMemoizedStyles(euiCardStyles); const cardStyles = [ styles.card.euiCard, // Text alignment should always be left when horizontal @@ -185,7 +187,7 @@ export const EuiCard: FunctionComponent = ({ const mainStyles = [styles.main.euiCard__main, styles.main.layout[layout]]; - const textStyles = euiCardTextStyles(euiThemeContext); + const textStyles = useEuiMemoizedStyles(euiCardTextStyles); const textCSS = [ textStyles.euiCard__text, // Text alignment should always be left when horizontal @@ -224,10 +226,16 @@ export const EuiCard: FunctionComponent = ({ */ let imageNode; + let imagePaddingStyles; if (image && layout === 'vertical') { if (isValidElement(image) || typeof image === 'string') { + imagePaddingStyles = euiCardImagePaddingStyles( + euiThemeContext, + paddingSize + ); const imageStyles = [ styles.image.euiCard__image, + imagePaddingStyles.image, display === 'transparent' && styles.image.transparent, ]; imageNode = ( @@ -246,6 +254,7 @@ export const EuiCard: FunctionComponent = ({ styles.icon.euiCard__icon, styles.icon.layout[layout], imageNode && styles.icon.withImage, + imagePaddingStyles?.iconWithImage, ]; iconNode = cloneElementWithCss(icon, { className: classNames(icon.props.className, 'euiCard__icon'), diff --git a/packages/eui/src/components/code/utils.tsx b/packages/eui/src/components/code/utils.tsx index 7790f58f91eb..30d5c89779d1 100644 --- a/packages/eui/src/components/code/utils.tsx +++ b/packages/eui/src/components/code/utils.tsx @@ -227,10 +227,10 @@ function wrapLines( } }); const wrapped: RefractorNode[] = []; + const styles = euiCodeBlockLineStyles(euiTheme); grouped.forEach((node, i) => { let children: RefractorNode[] = node; - const styles = euiCodeBlockLineStyles(euiTheme); const lineStyles = cx([ styles.euiCodeBlock__line, options.showLineNumbers && styles.hasLineNumbers, diff --git a/packages/eui/src/components/progress/progress.styles.ts b/packages/eui/src/components/progress/progress.styles.ts index 2213b53e5d60..932a162628a4 100644 --- a/packages/eui/src/components/progress/progress.styles.ts +++ b/packages/eui/src/components/progress/progress.styles.ts @@ -356,6 +356,12 @@ export const euiProgressStyles = ( /** * Data styles */ +// Static per-variant generators so `useEuiMemoizedStyles` can share them across instances +export const euiProgressNativeStyles = (euiThemeContext: UseEuiTheme) => + euiProgressStyles(euiThemeContext, true); +export const euiProgressIndeterminateStyles = (euiThemeContext: UseEuiTheme) => + euiProgressStyles(euiThemeContext, false); + export const euiProgressDataStyles = (euiThemeContext: UseEuiTheme) => ({ euiProgress__data: css` display: flex; diff --git a/packages/eui/src/components/progress/progress.tsx b/packages/eui/src/components/progress/progress.tsx index a3fbfddb9d80..a54fe42153a6 100644 --- a/packages/eui/src/components/progress/progress.tsx +++ b/packages/eui/src/components/progress/progress.tsx @@ -22,10 +22,15 @@ import { EuiI18n } from '../i18n'; import { CommonProps, ExclusiveUnion } from '../common'; import { isNil } from '../../services/predicate'; -import { useEuiTheme, makeHighContrastColor } from '../../services'; +import { + useEuiTheme, + useEuiMemoizedStyles, + makeHighContrastColor, +} from '../../services'; import { EuiScreenReaderOnly } from '../accessibility'; import { - euiProgressStyles, + euiProgressNativeStyles, + euiProgressIndeterminateStyles, euiProgressDataStyles, euiProgressLabelStyles, euiProgressValueTextStyles, @@ -120,7 +125,9 @@ export const EuiProgress: FunctionComponent< ? { color: makeHighContrastColor(color)(euiTheme.euiTheme) } : {}; - const styles = euiProgressStyles(euiTheme, determinate); + const styles = useEuiMemoizedStyles( + determinate ? euiProgressNativeStyles : euiProgressIndeterminateStyles + ); const cssStyles = [ styles.euiProgress, determinate && styles.native, @@ -130,7 +137,7 @@ export const EuiProgress: FunctionComponent< isNamedColor ? styles[color as EuiProgressColor] : styles.customColor, ]; - const dataStyles = euiProgressDataStyles(euiTheme); + const dataStyles = useEuiMemoizedStyles(euiProgressDataStyles); const dataCssStyles = [ dataStyles.euiProgress__data, size === 'l' && dataStyles[size], @@ -139,7 +146,7 @@ export const EuiProgress: FunctionComponent< euiProgressLabelStyles.euiProgress__label, labelProps?.css, ]; - const valueTextStyles = euiProgressValueTextStyles(euiTheme); + const valueTextStyles = useEuiMemoizedStyles(euiProgressValueTextStyles); const valueTextCssStyles = [ valueTextStyles.euiProgress__valueText, isNamedColor diff --git a/packages/eui/src/components/token/token.styles.ts b/packages/eui/src/components/token/token.styles.ts index fcfc8b06039c..152bc5005162 100644 --- a/packages/eui/src/components/token/token.styles.ts +++ b/packages/eui/src/components/token/token.styles.ts @@ -187,3 +187,13 @@ export const euiTokenStyles = ( none: css``, }; }; + +// Static per-fill generators so `useEuiMemoizedStyles` can share them across instances +export const euiTokenFillStyles: Record< + TokenFill, + (euiThemeContext: UseEuiTheme) => ReturnType +> = { + light: (euiThemeContext) => euiTokenStyles(euiThemeContext, 'light'), + dark: (euiThemeContext) => euiTokenStyles(euiThemeContext, 'dark'), + none: (euiThemeContext) => euiTokenStyles(euiThemeContext, 'none'), +}; diff --git a/packages/eui/src/components/token/token.tsx b/packages/eui/src/components/token/token.tsx index 4b3e476393ef..ffbbe82ecabe 100644 --- a/packages/eui/src/components/token/token.tsx +++ b/packages/eui/src/components/token/token.tsx @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import React, { FunctionComponent, useMemo } from 'react'; +import React, { FunctionComponent } from 'react'; import classNames from 'classnames'; -import { useEuiTheme, isColorDark, hexToRgb } from '../../services'; +import { useEuiMemoizedStyles, isColorDark, hexToRgb } from '../../services'; import { EuiIcon, IconSize } from '../icon'; import { EuiTokenMapType, TOKEN_MAP } from './token_map'; @@ -20,7 +20,7 @@ import type { TokenShape, TokenFill, } from './token_types'; -import { euiTokenStyles } from './token.styles'; +import { euiTokenFillStyles } from './token.styles'; const isTokenColor = (color: string): color is TokenColor => COLORS.includes(color as TokenColor); @@ -53,8 +53,6 @@ export const EuiToken: FunctionComponent = ({ finalSize = 'm'; } - const euiTheme = useEuiTheme(); - // If the iconType passed is one of the prefab token types, // grab its properties const tokenDefaults = @@ -66,10 +64,9 @@ export const EuiToken: FunctionComponent = ({ const finalShape = shape || tokenDefaults.shape || 'circle'; let finalFill = fill || 'light'; - // memoize styles to reduce executing contained color calculations - const styles = useMemo(() => { - return euiTokenStyles(euiTheme, finalFill); - }, [euiTheme, finalFill]); + const styles = useEuiMemoizedStyles( + euiTokenFillStyles[finalFill as TokenFill] + ); let cssStyles = [ styles.euiToken, From c91eea41e93ce130365f78fae7660d2f501c1a1c Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 6 Oct 2026 16:14:28 +0200 Subject: [PATCH 2/2] Memoize EuiCard styles per padding size instead of splitting padding styles Keeps the card image Emotion label unchanged. --- .../card/__snapshots__/card.test.tsx.snap | 2 +- .../eui/src/components/card/card.styles.ts | 54 ++++++++++--------- packages/eui/src/components/card/card.tsx | 12 +---- 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/packages/eui/src/components/card/__snapshots__/card.test.tsx.snap b/packages/eui/src/components/card/__snapshots__/card.test.tsx.snap index a0ff2cf97196..ef1b924e8e7b 100644 --- a/packages/eui/src/components/card/__snapshots__/card.test.tsx.snap +++ b/packages/eui/src/components/card/__snapshots__/card.test.tsx.snap @@ -908,7 +908,7 @@ exports[`EuiCard props image 1`] = ` class="euiCard__top emotion-euiCard__top-vertical" >
{ +export const euiCardStyles = ( + euiThemeContext: UseEuiTheme, + paddingSize: EuiCardProps['paddingSize'] +) => { const { euiTheme } = euiThemeContext; + const paddingAmount = euiPaddingSize(euiThemeContext, paddingSize!); const spacing = euiPaddingSize(euiThemeContext, paddingKey); const halfSpacing = euiPaddingSize(euiThemeContext, halfPaddingKey); @@ -143,6 +148,13 @@ export const euiCardStyles = (euiThemeContext: UseEuiTheme) => { 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', @@ -169,6 +181,11 @@ export const euiCardStyles = (euiThemeContext: UseEuiTheme) => { 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: { @@ -183,30 +200,17 @@ export const euiCardStyles = (euiThemeContext: UseEuiTheme) => { }; }; -// Kept out of `euiCardStyles` so the rest can be memoized independently of `paddingSize` -export const euiCardImagePaddingStyles = ( - euiThemeContext: UseEuiTheme, - paddingSize: EuiCardProps['paddingSize'] -) => { - const paddingAmount = euiPaddingSize(euiThemeContext, paddingSize!); - - return { - image: css` - /* 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}`)} - `, - iconWithImage: css` - /* Important needed to override current Sass styles on .euiIcon */ - transform: translate( - -50%, - calc(-50% + -${paddingAmount}) - ) !important; /* stylelint-disable-line declaration-no-important */ - `, - }; +// Static per-size generators so `useEuiMemoizedStyles` can share them across instances +export const euiCardStylesByPadding: Record< + EuiPaddingSize, + (euiThemeContext: UseEuiTheme) => ReturnType +> = { + none: (euiThemeContext) => euiCardStyles(euiThemeContext, 'none'), + xs: (euiThemeContext) => euiCardStyles(euiThemeContext, 'xs'), + s: (euiThemeContext) => euiCardStyles(euiThemeContext, 's'), + m: (euiThemeContext) => euiCardStyles(euiThemeContext, 'm'), + l: (euiThemeContext) => euiCardStyles(euiThemeContext, 'l'), + xl: (euiThemeContext) => euiCardStyles(euiThemeContext, 'xl'), }; export const euiCardTextStyles = (euiThemeContext: UseEuiTheme) => { diff --git a/packages/eui/src/components/card/card.tsx b/packages/eui/src/components/card/card.tsx index db3f2ca974e2..4d6ffdf04fcb 100644 --- a/packages/eui/src/components/card/card.tsx +++ b/packages/eui/src/components/card/card.tsx @@ -35,8 +35,7 @@ import { EuiSpacer } from '../spacer'; import { EuiCardSelect, EuiCardSelectProps } from './card_select'; import { euiCardBetaBadgeStyles, - euiCardImagePaddingStyles, - euiCardStyles, + euiCardStylesByPadding, euiCardTextStyles, } from './card.styles'; @@ -172,7 +171,7 @@ export const EuiCard: FunctionComponent = ({ !isDisabled && (onClick || href || (selectable && !selectable.isDisabled)); const euiThemeContext = useEuiTheme(); - const styles = useEuiMemoizedStyles(euiCardStyles); + const styles = useEuiMemoizedStyles(euiCardStylesByPadding[paddingSize]); const cardStyles = [ styles.card.euiCard, // Text alignment should always be left when horizontal @@ -226,16 +225,10 @@ export const EuiCard: FunctionComponent = ({ */ let imageNode; - let imagePaddingStyles; if (image && layout === 'vertical') { if (isValidElement(image) || typeof image === 'string') { - imagePaddingStyles = euiCardImagePaddingStyles( - euiThemeContext, - paddingSize - ); const imageStyles = [ styles.image.euiCard__image, - imagePaddingStyles.image, display === 'transparent' && styles.image.transparent, ]; imageNode = ( @@ -254,7 +247,6 @@ export const EuiCard: FunctionComponent = ({ styles.icon.euiCard__icon, styles.icon.layout[layout], imageNode && styles.icon.withImage, - imagePaddingStyles?.iconWithImage, ]; iconNode = cloneElementWithCss(icon, { className: classNames(icon.props.className, 'euiCard__icon'),