docs/react-v9/contributing/rfcs/react-components/convergence/theme-shape.md
@miroslavstastny, @layershifter
This RFC updates theme-tokens.md RFC. Once this on is approved, we should merge the two RFCs together.
As described in theme-tokens.md the current theme consists of global and alias tokens. All tokens are injected into DOM as CSS variables, resulting in 1200 CSS variables. With this approach, when a theme switches, there is no need to re-compute any component styles, by just setting the new token values to the CSS variables, all components change their appearance accordingly.
However, 1200 CSS variables in DOM affects browser performance. This RFC proposes ways how to improve theme performance.
The current theme consists of ~1200 tokens which are injected into DOM as CSS variables. It impacts not only Javascript performance but also browser performance in styles computation phase.
To see the performance impact we render 20 FluentProvider components side by side (each injecting a class with 1200 CSS variables) - mount and unmount 10 times and measure the performance in Chrome profiler with 6x CPU slowdown. After each mount, a CSS attribute is accessed to force reflow in order to be able to measure the rendering performance.
Current implementation in @fluentui/[email protected].
--alias-token: var(--global-token).--alias-token: #fff.It seems that the rendering performance depends on the number of CSS variables in the DOM. This is an experiment to remove even more variables to verify the hypothesis.
In inline aliases scenario, there are 1200 CSS variables with names like --alias-color-neutral-neutralStrokeAccessibleHover. The total string length of CSS variable names (including the leading --) is 39,252 characters - is that affecting the perf as well? We used 7-char hashes for variable names to check for any difference - this reduced the string length to 10,354 characters. There is no difference in the performance.
The perf example intentionally uses precomputed hashes to keep the hash computation out of the measured code path.
We also used flamegrill to compare the approaches. Inlined aliases is used as a baseline all other approaches are compared to. For each experiment we run the test 10 times for both the experiment and the baseline to get comparable numbers.
Global tokens reference values, alias tokens reference the global tokens:
/* Before */
--global-red: #f00;
--alias-error: var(--global-red);
Performance analysis shows, that by inlining the values directly into CSS variables for alias tokens we can improve Rendering perf.
Proposed change:
/* After */
--global-red: #f00; /* no change here */
--alias-error: #f00; /* inlined value from --global-red */
This change has already been implemented in #19660.
The main reason why CSS variables are used is to be able to switch themes just by changing the CSS variables values.
But, by design, global tokens never change their values when switching theme, so there is no point to have them as CSS variables.
The proposal is to remove global color tokens from the theme object and do not inject them to DOM as CSS variables.
As shown in the Performance analysis analysis, this reduces the number of CSS variables by a half and significantly improves rendering performance.
First of all, using a global color token in styles directly is almost always incorrect as the color does not change when switching themes.
For the rare cases where the global color token is needed, react-theme will export all the global tokens.
/* Before */
import { makeStyles } from '@fluentui/react-components';
const useStyles = makeStyles({
root: theme => ({
color: theme.alias.color.neutral.neutralStroke1, // color: var(--alias-color-neutral-neutralStroke1)
background: theme.global.palette.grey[50], // background: var(--global-palette-grey-50)
}),
});
/* After */
import { makeStyles, globalColorTokens } from '@fluentui/react-components';
const useStyles = makeStyles({
root: theme => ({
color: theme.alias.color.neutral.neutralStroke1, // color: var(--alias-color-neutral-neutralStroke1)
background: globalColorTokens.palette.grey[50], // background: '#ddd' 👈 value inlined/bound during babel-plugin/build
}),
});
In the current implementation the global token is injected to the DOM as CSS variable by FluentProvider - the theme passed to the FluentProvider is the source of truth for all the tokens.
In the proposed implementation, however, the global token value is inlined during the application bundling. That should not be a problem, we do not support changing global token values once the application is built and bundled.
For components developed in isolation this will work as well as the tokens are bound only when the final application is bundled (not when the component is built).
babel-make-styles consequencesThe global tokens are inlined when component styles are processed by babel-make-styles plugin.
There are 3 different places, where the plugin can be run:
Badge - this is a bug, to be reported and fixed)To be safe with the FUI library build (point 1 above) and fix the potential problem when running the babel plugin on a component (point 2), we will consider the following options (all after beta):
node_modules (to cover the FUI library and component) during the application build.Having the tokens split to global and alias tokens is definitely a valid concept for design. But does not mean anything to engineers.
Let's merge the two to a single object in the final theme.
(This change has no direct impact on perf, but is required for the next point.)
To be able to do this, we need get rid of any naming overlaps between alias and global tokens. The only potential overlap is in spacing tokens as we have both global and alias spacing tokens -> talk to design to understand which tokens engineers are supposed to use.
Current design token spec also calls out fontWeights and alignment tokens are "not actual tokens" and should not be used -> talk to design, should we remove them?
In Typescript the theme is represented by a deep object:
/* Before */
theme.alias.colors.red.neutral;
This brings the three following issues:
flattenThemeToCSSVariables()).The proposal is to merge all the tokens to a single flat object:
/* After */
theme.aliasColorsRedNeutral;
This should simplify theme merging, improve tokens discoverability. The potential drawback is we are creating a gigantic dictionary of unrelated values.
As described in Performance analysis, total string length of CSS variable names is ~40KB, by hashing the variable names to 7 char hashes, we can reduce this string length by 75%.
The performance analysis shows no perf impact (in rendering phase) by doing so. Also when using the variables in makeStyles, we need either hash the names on the fly (perf hit) or reference a dictionary (potential bundle size and memory hit).
Besides the performance, another valid reason for hashing the variable names might be encapsulation. Currently, the CSS variable names are deterministic, anybody can use them directly in both CSSInJS and SASS/CSS - this would make the variable names part of the API contract and any variable name change would be a breaking change. Hashing the variable names can prevent this misuse.
Let's create a separate RFC and have a discussion on this topic after beta.
There are currently ~1200 tokens in the theme object. ~1000 of the tokens represent global and alias shared colors which are actually rarely used in products. 80% of tokens rarely used.
When adding tokens, think about the number of CSS variables added, whether they change during runtime and need to be implemented using CSS variables.
We have learnt that CSS variables are expensive. Can we get rid of them completely? If we decided to do so, we would need to think about the following problems:
It might be an interesting exercise to explore these but sometime later.