feat: Add enableScopedSelectors prop to Stack that, when true, makes the Stack styles selectors be more scoped to not be as expensive in style recalculation#25397
Conversation
…the Stack styles selectors be more scoped to not be as expensive in style recalculation.
Asset size changes
Baseline commit: fe895d6bb4f9df0304f63f7e77acafef4b912925 (build) |
📊 Bundle size report🤖 This report was generated against fe895d6bb4f9df0304f63f7e77acafef4b912925 |
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 2423974:
|
Perf Analysis (
|
| Scenario | Render type | Master Ticks | PR Ticks | Iterations | Status |
|---|---|---|---|---|---|
| StackWithIntrinsicChildren | mount | 2761 | 2968 | 5000 | Possible regression |
| StackWithTextChildren | mount | 5695 | 5872 | 5000 | Possible regression |
All results
| Scenario | Render type | Master Ticks | PR Ticks | Iterations | Status |
|---|---|---|---|---|---|
| BaseButton | mount | 1402 | 1430 | 5000 | |
| Breadcrumb | mount | 3615 | 3488 | 1000 | |
| Checkbox | mount | 3164 | 3181 | 5000 | |
| CheckboxBase | mount | 2776 | 2908 | 5000 | |
| ChoiceGroup | mount | 5270 | 5372 | 5000 | |
| ComboBox | mount | 1467 | 1483 | 1000 | |
| CommandBar | mount | 11496 | 11475 | 1000 | |
| ContextualMenu | mount | 13075 | 13013 | 1000 | |
| DefaultButton | mount | 1639 | 1650 | 5000 | |
| DetailsRow | mount | 4316 | 4361 | 5000 | |
| DetailsRowFast | mount | 4375 | 4308 | 5000 | |
| DetailsRowNoStyles | mount | 4191 | 4243 | 5000 | |
| Dialog | mount | 3613 | 3637 | 1000 | |
| DocumentCardTitle | mount | 700 | 717 | 1000 | |
| Dropdown | mount | 4224 | 3834 | 5000 | |
| FocusTrapZone | mount | 2425 | 2473 | 5000 | |
| FocusZone | mount | 2346 | 2349 | 5000 | |
| GroupedList | mount | 62664 | 70674 | 2 | |
| GroupedList | virtual-rerender | 29610 | 29789 | 2 | |
| GroupedList | virtual-rerender-with-unmount | 108278 | 110366 | 2 | |
| GroupedListV2 | mount | 676 | 683 | 2 | |
| GroupedListV2 | virtual-rerender | 661 | 670 | 2 | |
| GroupedListV2 | virtual-rerender-with-unmount | 663 | 684 | 2 | |
| IconButton | mount | 2309 | 2303 | 5000 | |
| Label | mount | 870 | 875 | 5000 | |
| Layer | mount | 5146 | 5102 | 5000 | |
| Link | mount | 1033 | 992 | 5000 | |
| MenuButton | mount | 1990 | 2017 | 5000 | |
| MessageBar | mount | 2803 | 2773 | 5000 | |
| Nav | mount | 3966 | 3872 | 1000 | |
| OverflowSet | mount | 1664 | 1618 | 5000 | |
| Panel | mount | 3047 | 3001 | 1000 | |
| Persona | mount | 1530 | 1536 | 1000 | |
| Pivot | mount | 2006 | 1923 | 1000 | |
| PrimaryButton | mount | 1798 | 1816 | 5000 | |
| Rating | mount | 8299 | 8305 | 5000 | |
| SearchBox | mount | 1850 | 1796 | 5000 | |
| Shimmer | mount | 3358 | 3331 | 5000 | |
| Slider | mount | 2455 | 2514 | 5000 | |
| SpinButton | mount | 5639 | 5512 | 5000 | |
| Spinner | mount | 979 | 943 | 5000 | |
| SplitButton | mount | 3696 | 3716 | 5000 | |
| Stack | mount | 989 | 1011 | 5000 | |
| StackWithIntrinsicChildren | mount | 2761 | 2968 | 5000 | Possible regression |
| StackWithTextChildren | mount | 5695 | 5872 | 5000 | Possible regression |
| SwatchColorPicker | mount | 12366 | 12420 | 5000 | |
| TagPicker | mount | 3074 | 3140 | 5000 | |
| TeachingBubble | mount | 100640 | 99561 | 5000 | |
| Text | mount | 958 | 944 | 5000 | |
| TextField | mount | 1906 | 1864 | 5000 | |
| ThemeProvider | mount | 1819 | 1847 | 5000 | |
| ThemeProvider | virtual-rerender | 1280 | 1291 | 5000 | |
| ThemeProvider | virtual-rerender-with-unmount | 2566 | 2590 | 5000 | |
| Toggle | mount | 1326 | 1319 | 5000 | |
| buttonNative | mount | 644 | 658 | 5000 |
…tackScopedSelectors
|
|
||
| /** | ||
| * Defines if scoped style selectors are enabled for the Stack component, which greatly helps in style recalculation | ||
| * performance but requires children of the Stack to be able to accept a className prop (excluding Fragments). |
There was a problem hiding this comment.
nit:
| * performance but requires children of the Stack to be able to accept a className prop (excluding Fragments). | |
| * performance, but requires children of the Stack to be able to accept a className prop (excluding Fragments). |
| const disableShrinkStyles = { | ||
| // flexShrink styles are applied by the StackItem | ||
| '> *:not(.ms-StackItem)': { | ||
| [`> ${enableScopedSelectors ? '.' + GlobalClassNames.child : '*'}:not(.${StackItemGlobalClassNames.root})`]: { |
There was a problem hiding this comment.
Maybe save the scoped selector to a local const, since you use it in multiple places. It would also make this easier to read.
const childSelector = enableScopedSelectors ? `> .${GlobalClassNames.child}` : '> *';| [`> ${enableScopedSelectors ? '.' + GlobalClassNames.child : '*'}:not(.${StackItemGlobalClassNames.root})`]: { | |
| [`${childSelector}:not(.${StackItemGlobalClassNames.root})`]: { |
| maxWidth: '100vw', | ||
|
|
||
| '> *': { | ||
| [`> ${enableScopedSelectors ? '.' + GlobalClassNames.child : '*'}`]: { |
There was a problem hiding this comment.
If you use my childSelector suggestion, this would be:
| [`> ${enableScopedSelectors ? '.' + GlobalClassNames.child : '*'}`]: { | |
| [childSelector]: { |
Ditto below.
| : null; | ||
| } | ||
|
|
||
| const childAsReactElement = child as React.ReactElement; |
There was a problem hiding this comment.
(nit) is this cast necessary? I think the React.isValidElement check above should narrow the type of child to React.ReactElement.
* master: (22 commits) fix(react-menu): removes exposing of internal type FluentTriggerComponent (microsoft#25410) fix(react-popover): removes exposing of internal type FluentTriggerComponent (microsoft#25411) applying package updates fix(react-tooltip): removes exposing of internal type FluentTriggerComponent (microsoft#25409) chore: Reducing bundle size of Stack by moving selector used in multiple places to local const (microsoft#25429) docs(rfcs): Simple component implementation (microsoft#25139) Fix migration publishing (microsoft#25422) Integrate storywright for story tests - As part of exploring screener alternative (microsoft#25399) fix(react-utilities): exposes internal methods used in API surface (microsoft#25406) fix(react-dialog): removes exposing of internal type FluentTriggerComponent (microsoft#25408) fix(react-context-selector): exposes internal type ContextSelector (microsoft#25404) fix(react-aria): exposes internal leaking types (microsoft#25403) fix(react-shared-contexts): exposes internal leaks used in the API surface (microsoft#25405) fix(react-positioning): exposes new typings to avoid exposing internal methods (microsoft#25407) applying package updates fix: Allowing DatePicker to be focusable within FocusZones by default (microsoft#25428) fix: Pad in slider so the thumb does not render outside the bounds of the root element (microsoft#25378) feat: Add enableScopedSelectors prop to Stack that, when true, makes the Stack styles selectors be more scoped to not be as expensive in style recalculation (microsoft#25397) fix(react-avatar): Remove gaps between AvatarGroupItem/OveflowButton and its outline (microsoft#25382) fix: Combobox text attribute ignored when empty string is passed (microsoft#24665) ...
…the Stack styles selectors be more scoped to not be as expensive in style recalculation (microsoft#25397) * feat: Add enableScopedSelectors prop to Stack that, when true, makes the Stack styles selectors be more scoped to not be as expensive in style recalculation. * Adding change file. * Only apply stack child classname to children if enableScopeSelectors is set to true. * Addressing PR feedback. * Small improvement. Co-authored-by: KHMakoto <humberto_makoto@hotmail.com>
Previous Behavior
The
Stackcomponent was very expensive for style recalculation performance because children selectors are not scoped (they are of the type> *and the browser evaluates selectors from right to left so it always matches everything in the page first). Some improvements were made in #25381, but the performance of the component was still not up to the level we wanted it to be.Here are the results of a trace running on our v8 docsite's
Stackpage, where most of the most expensive selectors areStack-related:New Behavior
This PR adds an
enableScopedSelectorsprop that, when set to true, makes theStackchildren selectors be more scoped (they go from being of the type> *to being of the type> .ms-Stack-child). This greatly reduces the style recalculation cost.We have gated this behavior behind a prop because this approach requires all immediate children of the
Stackcomponent to be able to receive and apply aclassNameprop (excludingFragments, in which case all their immediate children are the ones looked at instead). Since this was not a requirement before, this could create some issues for people who are already using theStackcomponent in their product, which would be a breaking change. By making this behavior opt-in, we can still provide a way for people to get the performance benefits without having a breaking change in the library.Enabling this change for all
Stacksin the examples of our v8 docsite'sStackpage gives us the following numbers, which are much improved from before:Furthermore, the v8 docsite leverages the
Stackcomponent in several places that are not the examples. I experimented by enabling this change in the whole page (this change is not included in this PR and was just for experimentation) and I got much better results than even the ones above:Related Issue(s)
Fixes #24259
Follow-up of #25381