Styling - static vs dynamic #2621
NicholasBoll
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
The documentation isn't very strong right now and I've been seeing some misunderstandings about how to style things.
What is dynamic styling?
@emotion/react
and@emotion/styled
use a dynamic style strategy. Let's go over a bit about what happens in the following scenarios.Given the following code, what happens at runtime?
Code
MyComponent
is classified as a "styled component". It returns a component within a component. You can see this in the React render tree thatMyComponent
renders a component namedstyled(MyComponent)
which then renders adiv
element. Here's steps Emotion is doing:serializedStyles
from@emotion/serialize
to convert the style object to a style string. This string does not include the CSS selector and is not valid if injected into the browser directly. It is in a state that makes it easier to extend with other styles later.stylis
is used to evaluate the style string. The style string is not complete and must be processed bystylis
using pre-determined middleware to add prefixes and convert the string into something the browser can actually understand.The output of
MyComponent
will look something like this:HTML
CSS
Note: Emotion evaluates styles at runtime during the render cycle of React. This is what can cause large inefficiencies with React's concurrent mode using a new prioritization scheduler that can run a render function multiple times without flushing to the actual DOM. Since Emotion does work during this render cycle, Emotion is adding CPU cycles that could be thrown away.
This means every render cycle, Emotion must serialize the style object to produce a string and run a hash algorithm against that string to determine the cache key. Most of the time the cache key is already in the cache. This still means Emotion has a runtime overhead even if no useful work is done.
If your example is as simple as the example above, this overhead is pretty minimal. There can even be optimizations for the
styled
API specifically that can skip the serialization and hashing steps because the style object is a static reference.But what happens when you add some dynamic styling?
Code
Both
color
andbackground
properties are now dynamic. In this example, chances are thecolor
prop won't change much and the theme probably won't change per render, but nowstyled
cannot do any optimizations because thecolor
prop could change and thetheme
could change. This is even worse if a prop is used to adjust the style and the prop is expected to change every render. Remember from the above steps that if the hash is different, Emotion will inject a new StyleSheet into the browser's StyleSheetList. This process will invalidate the browser's style cache. When the browser goes to do a style recalculation and layout recalculation, the browser throws away caches from previous recalculations and must start all over again. Style recalculation is an * m
operation wheren
is the number of selectors andm
is the number of elements on the page. A typical application can have thousands of selectors and thousands of elements. In a real world application this type of style recalc can take 80ms for a fresh style recalc vs 0.1ms cache style recalc. That's a significant difference. If this is done during animation, 60fps is not achievable since 60fps means about 16ms for each frame.But it gets worse. Many components need more specific styling. Consider this example:
Code
In this example,
MyButton
is a styled component that styles aStyledButton
. The end result is all the styles ofStyledButton
are applied toMyButton
, butpadding
is overridden to12px
. How does this happen?Each styled component returns the element provided with a
className
prop that is a result of the cache key of the hashed serialized style string. The styled component will also read theclassName
prop passed to it and run it throughgetRegisteredStyles
from@emotion/utils
which looks through each CSS class name to see if any of them match a cache key. If they do match a cache key, the styles are put into an object. Each cache hit merges mores styles to the style object. Finally, any styles given directly to the styled component are added to this style object and the final object is serialized, hashed, and, if needed, injected into theStyleSheetList
. You can find this style merging process here. The end result is a single CSS class name with all merged styles. The DOM output will look something like this:HTML
CSS
Notice that Emotion will produce a single CSS class name no matter how many times the Render tree contains wrapped styled components. Every layer simply appends styles, getting bigger. This exacerbates the processing needed for each style and makes it much more difficult to cache.
While CSS uses specificity via selectors to determine the CSS properties that override others, Emotion uses JavaScript-style object merging strategies. Since there is only 1 class name added to the element, the specificity of Emotion is always
0,1,0
. This is how Emotion controls style merging to match a developer's mental model.What is static styling?
Static styling refers to styles generated once and do not change at runtime. Canvas Kit's
createStyles
andcreateStencil
are style APIs that are designed to create styles only once, outside the React render cycle. This decreases the runtime overhead in a component, but does not eliminate a runtime.So what does the runtime look like for a Stencil?
Code
HTML
CSS
Note: The
base
styles contain the initialization of thecolor
andbackground
variables. This is because CSS custom properties (CSS Variables) cascade through each element. Adding the initialization into thebase
styles creates a cascade barrier. This keeps component styles isolated and prevents a variable prop from cascading to child components of the same type. For example, if yourCard
component uses the default background, but is sometimes rendered in anotherCard
with a custom background, yourCard
background won't change.The Stencil is returning a
className
and astyle
. TheclassName
contains all class names that match the modifiers + base. In the case of<MyButton />
, only thebase
class matches, which isa0
, so the DOM is<button class="a0" />
. The<MyButton size="large" />
has a modifiersize="large"
and so botha0
anda1
match. The DOM is<button class="a0 a1" />
. The Stencil also supports variables. It uses thestyle
attribute to set high-specificity overrides. This is how properties can be dynamic without forcing a high Style Recalculation cost. Modifiers are simply adding extra class names and letting the browser's natural style engine do its thing.Stencils make dynamic styling have very little runtime cost. But what about if you want to use stencils to style a component already styled with stencils?
Code
Our example won't exactly work because
MyButton
isn't handlingclassName
andstyle
prop merging, but let's pretend it will. In this example, the following HTML and CSS is generated:HTML
CSS
Notice that the
b0
classes were added to eachbutton
element. Extending stencils means adding more class names. This continues to keep the runtime lightweight.Static styles have a compat mode
What is compat mode? In order for Canvas Kit static styles to be compatible with all the applications that exist, we created a compat mode inside our
handleCsProp
utility function that handles theclassName
prop passed fromstyled
components and thecss
prop from@emotion/react
. It uses the same style merging strategy thatstyled
and thecss
prop does. I outlined this merging process above.Consider this example:
Code
HTML
CSS
handleCsProp
compares theclassName
passed to it to see if there's any cache hits in the Emotion cache. If there are, it goes into dynamic/compat mode to merge styles the same way@emotion/styled
and@emotion/react
do to be as compatible as possible. Emotion works by always creating a unique CSS class for each hash. This is why there's only theabc-123
class name created. Even though the styles are overridden in different ways, they all result in the same serialized style hash. Because this hash is unique and only 1 is added to the element, it doesn't matter the insertion order of the styles. Our static styles DO rely on insertion order because they rely on the browser's natural styling processes. This mix of style insertion order dependency forces us to do the least common denominator and use the same strategy as dynamic Emotion does. This gives results that developers expect, but it does mean that compat mode has the same performance profile as dynamic Emotion does.What triggers compat mode?
The above examples trigger compat mode, but the following also trigger compat mode:
mergeStyles
.mergeStyles
has the same signature ashandleCsProp
, but adds props that directly effect styling. Like<MyButton padding={14} />
handleCsProp
.handleCsProp
is very flexible in what it can accept, including an object of styles. Not all style objects trigger compat mode. If you pass only CSS variable overrides, they will be forwarded to thestyle
prop and will not trigger compat mode:handleCsProp
iterates over all the keys of any object passed to it. If the key starts with a--
, it knows it is a variable and the desire is to locally override the CSS variable. If it does not start with a--
, it assumes it is a CSS property or a CSS selector and triggers compat mode.Beta Was this translation helpful? Give feedback.
All reactions