Fluent UI React uses CSS-in-JS with Atomic styles to style React components.
sheet.insertRule
API on their first use.border
is expanded to 12 separate CSS properties - style, color and radius for 4 sides)Fluent UI React v0 (@fluentui/react-northstar
) uses third party Fela.js as its CSS-in-JS engine.
The upcoming version, Fluent UI React v9 (@fluentui/react-components
) uses in-house CSS-in-JS engine called Griffel.
The initial render of the Teams application results in ~2000 DOM elements styled by ~2500 CSS rules. It can happen that some elements are styled using ~100 different classes:
All the CSS rules have been added using insertRule
to a single style
element.
Now, after the initial render, a user performs an action which renders a new piece of the UI (just one button) and, as a result, it causes a Recalculate Style
operation which affects ~2000 DOM elements and takes more than ~100ms (tested locally on high end Intel Macbook).
The root cause of the perf issue seems to be the insertRule()
call on the style element itself.
As a minimal repro, we have a page (plain HTML, no framework) which consists of 1000 button
elements each having a span
inside = 2000 elements in total. We style these elements using CSS rules which come from a single style
element - each button
has ~100 classnames, most of them shared among all the buttons, two of them unique for each button - this results in ~2000 CSS rules in the style
element. (This should be DOM and CSS size comparable with Teams Client v2 after the initial render). Now we click a button which adds one additional CSS rule to the style
element - this triggers Recalculate Style
which affects 2000 elements and takes ~80ms on my Macbook.
codesandbox: https://codesandbox.io/s/insertrule-2000rules-chc15k?file=/index.html
Second experiment just removes the unique classes reducing the DOM to 1000 buttons, 1000 spans and ~100 CSS rules in the style elements - the Recalculate Style
still affects 2000 elements, now it is cheaper taking ~40ms on my Macbook.
codesandbox: https://codesandbox.io/s/insertstyle-66rules-w4x38o
In both examples - start profiler, click the blue button, check console and the profile
My current understanding is that whenever insertRule
is called on the style element, all DOM elements which are styled by any rule in that style element are recomputed.
The rule which we are adding in the test does NOT affect any of the elements on the page. There is no difference when we add the rule to the beginning or the end of the style element.
As a long term solution for Fluent UI React v9 we plan to implement static CSS extraction - extract all the CSS during build time and statically have it as part of the initial HTML. But we still need a solution for Fluent UI React v0.
Is there anything wrong with how we use the insertRule
? Would there be any option/plan on the browser side to optimize?
We tried to prototype splitting CSS rules to multiple style
elements - we just count the rules inserted and for every 100 CSS rules, we dynamically add a new style
element. It improves perf (in local perf debug - ~2000 elements affected/110ms -> ~600 elements affected/50ms) but the result is still hardly acceptable.
This also raises another question - how would using multiple style
elements affect overall browser performance? We have not identified any perf hit caused by adding a new empty style
element dynamically, is that correct? Also will browser handle having hundreds of style
elements in the DOM?