Email for Edge team

Fluent UI React uses CSS-in-JS with Atomic styles to style React components.

  • CSS-in-JS means styles are added to the DOM using sheet.insertRule API on their first use.
  • Atomic means that for every unique [CSS property;value] pair, a new class is created.
  • In order for the atomic CSS-in-JS to be deterministic, all CSS shorthands are expanded to long hands (that means 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.

Current situation in Teams Client v2

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:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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).

Root causing the issue

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.

Long term solution

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.

Discussion

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?