Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Emotion] Create hook utility for memoizing component styles per-theme #7529

Merged
merged 9 commits into from
Feb 15, 2024

Conversation

cee-chen
Copy link
Member

@cee-chen cee-chen commented Feb 14, 2024

Summary

⚠️ As always, I strongly recommend reviewing by commit.

This PR is an initial setup PR for memoizing a component's Emotion styles per theme (instead of per component). This should (hopefully) greatly improve the performance of our Emotion styles/object generation, especially for components used repeatedly in many places, e.g. EuiCode, EuiButton, etc.

I replaced the memoization used in #7486 with this new architecture as a proof of concept; this PR should maintain the performance improvements of the linked PR while being easy to swap in and replace across all of EUI's existing components, e.g.

const euiTheme = useEuiTheme();
const styles = euiComponentStyles(euiTheme);

// becomes

const styles = useEuiMemoizedStyles(euiComponentStyles);

What this PR does not do

  • Handle class components - I have a spike for this in a separate branch, but would prefer to keep this PR small for now to focus on the overlying idea/concept

  • Memoize dark vs. light themes - I played around with trying to memoize both light and dark themes separately, to make swapping back and forth faster/more performant, but eventually gave up on the idea as the full theme reference re-instantiates every time colorMode changes (which makes sense as theme tokens are also changing). TBH I also anticipate this as not being necessary as most end-users will not be swapping back and forth frequently between light/dark modes

  • Convert all components to use this util - this is a very basic setup and proof of concept of 2 components. If it passes muster, I'll open more follow-up PRs in batches later.

QA

  • EuiBeacon CSS rendered/looks the same as before as before
  • EuiCode CSS renders the same as before
  • EuiCode Performance
    • Open your devtools console
    • euiCodeSyntaxVariables() only fires 3 times on page load
    • euiCodeSyntaxVariables() does not fire on fullscreen mode state change nor on page navigation
    • euiCodeSyntaxVariables() does fire again on dark/light screen mode change (because the theme variables have changed)

General checklist

  • Revert [REVERT ME] commit
  • Browser QA
    • Checked in both light and dark modes
    • Checked in Chrome, Safari, Edge, and Firefox
      - [ ] Checked for accessibility including keyboard-only and screenreader modes
      - [ ] Checked in mobile
  • Docs site QA - N/A, this is mostly an internal EUI utility
  • Code quality checklist
  • Release checklist
    • A changelog entry exists and is marked appropriately.
      - [ ] If applicable, added the breaking change issue label (and filled out the breaking change checklist)
  • Designer checklist - N/A

- replace previous `UseEuiCodeSyntaxVariables` memoization with this new architecture
@cee-chen cee-chen changed the title [Emotion] Create utility for memoizing component styles per-theme [Emotion] Create hook utility for memoizing component styles per-theme Feb 14, 2024
@cee-chen cee-chen force-pushed the emotion/per-theme-style-memoization branch from 4020fb1 to cd77f3a Compare February 14, 2024 17:43
@cee-chen cee-chen marked this pull request as ready for review February 14, 2024 17:52
@cee-chen cee-chen requested a review from a team as a code owner February 14, 2024 17:52
* Hook that memoizes the returned values of components style fns/generators
* per-theme
*/
export const useEuiMemoizedStyles = <
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Feedback request] I am not super in love with this name or anything and am very open to renaming it something snappier! (applies to the general context and file name as well)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just useEuiStyles imo

Copy link
Member Author

@cee-chen cee-chen Feb 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The social network gif saying 'Drop the The. It's cleaner'

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kqualters-elastic I'm going to stick with the useEuiMemoizedStyles name for now (it feels a little weird to drop Memoized as it's the entire point of the utility). But if we can always change it later down the road if it gets tedious! It's currently a mostly internal/beta utility at this point in any case 🙏

Comment on lines +65 to +69
if (!styleGenerator.name) {
throw new Error(
'Styles are memoized per function. Your style functions must be statically defined in order to not create a new map entry every rerender.'
);
}
Copy link
Member Author

@cee-chen cee-chen Feb 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This catch exists to attempt to catch anonymous functions that re-instantiate every rerender, which end up adding potentially hundreds of map entries that we don't need/want instead of a single one. Style generator functions should ideally be static constants to correctly memoize, since I'm using the function itself as the key to its mapped output.

I'd be interested in hearing people's thoughts as to whether that's a spicy decision. In theory I could use function.name as the map key instead, but I think that leads to potentially worse end-user behavior (what if we use the same generic function name, e.g. styles() in two places, and the wrong styles get mapped/sent back?).

Would be down to hear more thoughts about which approach feels safer either from a 'things break' perspective or 'going OOM' perspective 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should definitely keep the function reference as the key, so there's no easy way for them to collide!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick note on the above: Tomasz's suggestion to change from Map() to WeakMap() (chatted about in Slack) should also help with issues where named functions get regenerated every rerender. 875dd01

Comment on lines 22 to 24
type Styles = SerializedStyles | CSSObject | string | null; // Is this too restrictive?
type StylesMaps = Record<string, Styles | Record<string, Styles>>;
type MemoizedStylesMap = Map<Function, StylesMaps>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is me trying to anticipate/type out the general shape that our component styles objects take, but I'm not sure if it's too restrictive honestly and if I should just do Map<Function, any> 🤷

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks good to me as is. We can always make it less restrictive later without breaking compatibility

@cee-chen cee-chen self-assigned this Feb 14, 2024
@tkajtoch tkajtoch self-requested a review February 15, 2024 11:19
Copy link
Member

@tkajtoch tkajtoch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested locally and confirmed the performance benefits, especially when scaled to multiples of the same components per page. The code looks clean, and the implementation should scale well. :shipit:

- to allow browser GC / avoid possible edge cases with massive RAM usage
- it's more verbose but oh well
@cee-chen cee-chen enabled auto-merge (squash) February 15, 2024 22:53
@kibanamachine
Copy link

Preview staging links for this PR:

@cee-chen cee-chen merged commit 47ab766 into elastic:main Feb 15, 2024
7 checks passed
@elasticmachine
Copy link
Collaborator

💚 Build Succeeded

History

cc @cee-chen

@cee-chen cee-chen deleted the emotion/per-theme-style-memoization branch February 15, 2024 23:16
jbudz pushed a commit to elastic/kibana that referenced this pull request Feb 23, 2024
`v93.1.1`⏩ `v93.2.0`

---

- Updated `EuiPageSidebar` and `EuiPageTemplate.Sidebar` with a new
`hasEmbellish` prop (defaults to false)
([#7521](elastic/eui#7521))
- Added `diff` glyph to `EuiIcon`
([#7520](elastic/eui#7520))
- Added `newChat` glyph to `EuiIcon`
([#7524](elastic/eui#7524))

**Bug fixes**

- Fixed `EuiSideNav` not correctly typing the `items` prop as required
([#7521](elastic/eui#7521))
- Fixed the `CSS is not defined` bug in `EuiPageTemplate` when rendering
in some SSR environments, particularly Next.js v13 and up
([#7525](elastic/eui#7525))
- Fixed `EuiDataGrid` component to clean up timer from side effect on
unmount ([#7534](elastic/eui#7534))

**Accessibility**

- Fixed `EuiSideNav` to render a fallback aria-label on mobile toggles
if no heading or mobile title exists
([#7521](elastic/eui#7521))

**CSS-in-JS conversions**

- Converted `EuiSideNav` to Emotion; Removed the following Sass
variables: ([#7521](elastic/eui#7521))
  - `$euiSideNavEmphasizedBackgroundColor`
  - `$euiSideNavRootTextcolor`
  - `$euiSideNavBranchTextcolor`
  - `$euiSideNavSelectedTextcolor`
  - `$euiSideNavDisabledTextcolor`
- Removed the `euiSideNavEmbellish` Sass mixin. Use the new
`EuiPageSidebar` `hasEmbellish` prop instead
([#7521](elastic/eui#7521))
- Added a new memoization/performance optimization utility for CSS-in-JS
styles ([#7529](elastic/eui#7529))
semd pushed a commit to semd/kibana that referenced this pull request Mar 4, 2024
`v93.1.1`⏩ `v93.2.0`

---

- Updated `EuiPageSidebar` and `EuiPageTemplate.Sidebar` with a new
`hasEmbellish` prop (defaults to false)
([elastic#7521](elastic/eui#7521))
- Added `diff` glyph to `EuiIcon`
([elastic#7520](elastic/eui#7520))
- Added `newChat` glyph to `EuiIcon`
([elastic#7524](elastic/eui#7524))

**Bug fixes**

- Fixed `EuiSideNav` not correctly typing the `items` prop as required
([elastic#7521](elastic/eui#7521))
- Fixed the `CSS is not defined` bug in `EuiPageTemplate` when rendering
in some SSR environments, particularly Next.js v13 and up
([elastic#7525](elastic/eui#7525))
- Fixed `EuiDataGrid` component to clean up timer from side effect on
unmount ([elastic#7534](elastic/eui#7534))

**Accessibility**

- Fixed `EuiSideNav` to render a fallback aria-label on mobile toggles
if no heading or mobile title exists
([elastic#7521](elastic/eui#7521))

**CSS-in-JS conversions**

- Converted `EuiSideNav` to Emotion; Removed the following Sass
variables: ([elastic#7521](elastic/eui#7521))
  - `$euiSideNavEmphasizedBackgroundColor`
  - `$euiSideNavRootTextcolor`
  - `$euiSideNavBranchTextcolor`
  - `$euiSideNavSelectedTextcolor`
  - `$euiSideNavDisabledTextcolor`
- Removed the `euiSideNavEmbellish` Sass mixin. Use the new
`EuiPageSidebar` `hasEmbellish` prop instead
([elastic#7521](elastic/eui#7521))
- Added a new memoization/performance optimization utility for CSS-in-JS
styles ([elastic#7529](elastic/eui#7529))
fkanout pushed a commit to fkanout/kibana that referenced this pull request Mar 4, 2024
`v93.1.1`⏩ `v93.2.0`

---

- Updated `EuiPageSidebar` and `EuiPageTemplate.Sidebar` with a new
`hasEmbellish` prop (defaults to false)
([elastic#7521](elastic/eui#7521))
- Added `diff` glyph to `EuiIcon`
([elastic#7520](elastic/eui#7520))
- Added `newChat` glyph to `EuiIcon`
([elastic#7524](elastic/eui#7524))

**Bug fixes**

- Fixed `EuiSideNav` not correctly typing the `items` prop as required
([elastic#7521](elastic/eui#7521))
- Fixed the `CSS is not defined` bug in `EuiPageTemplate` when rendering
in some SSR environments, particularly Next.js v13 and up
([elastic#7525](elastic/eui#7525))
- Fixed `EuiDataGrid` component to clean up timer from side effect on
unmount ([elastic#7534](elastic/eui#7534))

**Accessibility**

- Fixed `EuiSideNav` to render a fallback aria-label on mobile toggles
if no heading or mobile title exists
([elastic#7521](elastic/eui#7521))

**CSS-in-JS conversions**

- Converted `EuiSideNav` to Emotion; Removed the following Sass
variables: ([elastic#7521](elastic/eui#7521))
  - `$euiSideNavEmphasizedBackgroundColor`
  - `$euiSideNavRootTextcolor`
  - `$euiSideNavBranchTextcolor`
  - `$euiSideNavSelectedTextcolor`
  - `$euiSideNavDisabledTextcolor`
- Removed the `euiSideNavEmbellish` Sass mixin. Use the new
`EuiPageSidebar` `hasEmbellish` prop instead
([elastic#7521](elastic/eui#7521))
- Added a new memoization/performance optimization utility for CSS-in-JS
styles ([elastic#7529](elastic/eui#7529))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants