The Unseen Performance Costs of Modern CSS-in-JS Libraries

栏目: IT技术 · 发布时间: 4年前

内容简介:CSS-in-JS is becoming a popular choice for any new front-end app out there, due to the fact that it offers a better API for developers to work with. Don’t get me wrong, I love CSS, but creating a proper CSS architecture is not an easy task. Unfortunately t

CSS-in-JS is becoming a popular choice for any new front-end app out there, due to the fact that it offers a better API for developers to work with. Don’t get me wrong, I love CSS, but creating a proper CSS architecture is not an easy task. Unfortunately though, besides some of the great advantages CSS-in-JS boasts over traditional CSS, it may still create performance issues in certain apps. In this article, I will attempt to demystify the high-level strategies of the most popular CSS-in-JS libraries, discuss the performance issues they may introduce on occasion and finally consider techniques that we can employ to mitigate them. So, without further ado, let’s jump straight in.

Background

In my company we figured it would be useful to build a UI library in order to be able to re-use common UI pieces across different products and I was the one to volunteer to get this endeavor started. I chose to use a CSS-in-JS solution, since I was already really happy with the styled API that most of the popular libraries expose. As I was developing it, I wanted to be smart and have re-usable logic and shared props across my components, so I started composing them. For example, an <IconButton /> would extend the <BaseButton /> that in turn implements a simple styled.button . Unfortunately, the IconButton needed to have its own styling, so it was converted to a styled component along the lines of:

const IconButton = styled(BaseButton)`
  border-radius: 3px;
`;

As more and more components were added, more and more compositions took place and it didn’t feel awkward since React was built upon the concepts of this very notion. Everything was fine until I implemented a Table . I started noticing that the rendering felt slow, especially when the number of rows got more than 50. Thus, I opened my devtools to try and investigate it.

Well needless to say, the React tree was as big as Jack’s magical beanstalk. The amount of Context.Consumer components was so high, that it could easily keep me up at nights. You see, each time you render a single styled component using styled-components or emotion , apart from the obvious React Component that gets created, an additional Context.Consumer is added in order to allow the runtime script (that most CSS-in-JS libraries depend upon) to properly manage the generated styling rules. This normally shouldn’t be too much of a problem, but don’t forget that components need to have access to your theme. This translates to an additional Context.Consumer being rendered for each styled element in order to “read” the theme from the ThemeProvider component. All in all, when you create a styled component in an app with a theme, 3 components get created: the obvious StyledXXX component and two (2) additional consumer components. Don’t be too scared, React does its work fast and this won’t be too much of an issue most of the times, but what if we compose multiple styled components in order to create a more complex component? What if this complex component is part of a big list or a table, where at least 100 of those get rendered? That’s when problems arise…

Profiling

To test CSS-in-JS solutions I created the simplest of apps, which just renders 50 “Hello World” statements. On the first experiment, I wrapped the text in a traditional div element, while on the second one, I utilized a styled.div component instead. I also added a button that would force a react re-render on those 50 div elements whenever it was clicked. The code for both can be seen on the following gists:

After rendering the <App /> component, two different React trees got rendered. The outputted trees can be seen in the screenshots below:

The Unseen Performance Costs of Modern CSS-in-JS Libraries The React tree using a normal div

The Unseen Performance Costs of Modern CSS-in-JS Libraries The React tree using a styled.div element

I then forced a re-render of the <App /> 10 times in order to gather some metrics with regards to the perf costs that these additional Context.Consumer components bring. The timings of the re-renders in development mode can be seen below:

The Unseen Performance Costs of Modern CSS-in-JS Libraries Development render timings for simple div . Average: 2.54ms

The Unseen Performance Costs of Modern CSS-in-JS Libraries Development render timings for styled.div . Average: 3.98ms

So interestingly enough, on average, the CSS-in-JS implementation is 56.6% more expensive in this example . Let’s see if things are different in production mode. The timings of the re-renders in production mode can be seen below:

The Unseen Performance Costs of Modern CSS-in-JS Libraries Production render timings for simple div . Average 1.06ms

The Unseen Performance Costs of Modern CSS-in-JS Libraries Production render timings for styled.div . Average 2.27ms

When production mode is on, the implementation with the simple div seems to benefit the most by dropping its rendering time by more than 50% compared to a 43% drop on the CSS-in-JS implementation. Still, the latter takes almost twice as much time to render than the former. So what exactly is it that makes it slower?

Runtime Analysis

The obvious answer would be “Erm… you just said CSS-in-JS libraries render two Context.Consumer per component”, but if you really think about it, a context consumer is nothing more than accessing a JS variable. Sure, React has to do its work to figure out where to read the value from, but that alone doesn’t justify the timings above. The real answer comes from analyzing the reason why those contexts exist in the first place. You see, most CSS-in-JS libraries depend on a runtime that helps them dynamically update the styles of a component. These CSS-in-JS libraries don’t create CSS classes at build-time, but instead dynamically generate and update <style> tags in the document whenever a component mounts and/or has its props changed. These style tags normally contain a single CSS class, whose hashed name is mapped to a single React component. Whenever this component’s props change, the associated <style> tag must change as well. This is done by re-evaluating the CSS rules that the style tag needs to have, creating a new hashed class name to hold the aforementioned CSS rules and updating the classname prop of the associated React component in order to point to the recently-created class.

Let’s take for example the styled-components library. Whenever you create a styled.div , the library assigns an internal ID to this component and adds an empty <style> tag to the HTML <head> . This tag contains a single comment that references the internal ID of the React component that’s related to it:

<style data-styled-components>  
  /* sc-component-id: sc-bdVaJa */
</style>

When the associated React component gets rendered, styled-components:

  1. Parses the styled component’s tagged template’s CSS rules .
  2. Generates the new CSS class name (or checks whether it should retain the existing one ).
  3. Preprocesses the styles with stylis.
  4. Injects the preprocessed CSS into the associated <style> tag in the HTML <head> .

To be able to use the theme during step (1), a Context.Consumer is needed in order to read the theme’s values within the tagged template. In order to be able to be able to modify the associated <style> tag from within the React component, another Context.Consumer is needed to provide access to the stylesheet instance . That’s why we see those two (2) Consumers in most CSS-in-JS libraries.

In addition, because these computations will affect the UI, they have to be performed during the render phase of the component and cannot be performed as a React lifecycle side-effect (since they would be delayed and perceived by the user as lag). This is why the rendering takes longer in a simple styled.div than in a native one.

Now, the styled-components maintainers noticed that and added optimizations and early bailout techniques in order to bring down the time it takes for a component to re-render. Specifically, the library checks to see whether your styled component is “static”, meaning that its styling doesn’t depend on a theme or the component’s passed props. For example, the following component is static:

const StaticStyledDiv = styled.div`
  color:red
`;

while this isn’t:

const DynamicStyledDiv = styled.div`
  color: ${props => props.color}
`;

If the library detects a static component, it will skip steps 1– 4 , since it can understand that the generated class name will never have to change (since there is no dynamic element to modify its related CSS rules). In addition, it won’t render a ThemeContext.Consumer around the styled component , since a theme dependence would have prevented the component from being “static” in the first place.

If you were really observant, you would have noticed that even in production mode, the screenshot above rendered 2 Context.Consumer components for each styled.div . Interestingly though, the component that got rendered was “static” since it didn’t have any dynamic CSS rules and we would expect styled-components to skip the Consumer that had to do with the theme. The reason you see 2 Consumer s per component, is because the screenshots above were taken while utilizing emotion, another CSS-in-JS library. This library follows a similar approach, with minor differences. Again, it parses the tagged template, preprocesses it with stylis and updates the corresponding style tag. One key difference is that emotion always wraps all components with a ThemeContext.Consumer regardless of whether they are using a theme or not (which explains the screenshots above). Funnily enough, even though it renders more consumer components, it still outperforms styled-components , which denotes that the number of consumer isn’t the biggest contributor to a slow render. It should be noted that at the time of writing there is a beta version for the v5.x.x of styled-components, which will outperform emotion according to its maintainers .

So, to wrap up, the combination of multiple Context consumers (which means additional elements that React has to coordinate) and the inherent housekeeping that dynamic styling goes with, may be slowing down your app. It should also be mentioned, that the all the style tags that get added for each component never get removed at all. This is because the overhead associated with a DOM removal (e.g. browser reflows) is higher than the overhead of just keeping them there. To be honest, I’m unsure whether dangling style tags can create performance issues, since they only contain unused classes that are generated during runtime (e.g. don’t get shipped over the wire), but it’s something you should potentially consider for your app.

To be fair, those style tags are not created by all CSS-in-JS libraries, since not all of them are runtime-based. For example, linaria is a zero-runtime CSS-in-JS library that defines a set of fixed CSS classes during build time and maps all dynamic rules within a tagged template (i.e. CSS rules that depend on prop values) with CSS custom properties. Thus whenever a props changes, the CSS custom property changes and the UI updates. This makes it much faster than all runtime-based CSS-in-JS libraries, since the amount of work and housekeeping that needs to be done during a render is much less. Realistically, the only thing that it needs to do during render is to make sure to — potentially — update a CSS custom property . At the same time though, it’s not compatible with IE11, has limited support for the popular css prop and doesn’t offer theming capabilities out of the box. As with most libraries, there is no silver bullet.

Takeaways

CSS-in-JS was a revolutionary pattern which brought a better experience for many developers out there, while also solving many issues such as name collisions, vendor prefixing, etc. out of the box. The point of this article was to shed some light into the potentially unknown performance implications when using the most prominent CSS-in-JS libraries (e.g. the ones with a runtime). I want to stress that those perf considerations don’t always create problems for an app. In fact, most apps won’t even notice them unless they are relying on hundreds of concurrently rendered composed components. The benefits of CSS-in-JS typically outweigh the aforementioned perf implications, but these implications are something that developers of applications with tons of data and lots of rapidly changing UIs should definitely consider. Before you jump on any refactoring train though, please measure and judge for yourselves.

Finally, here are some techniques that you can employ in order to increase your app’s performance when using one of the popular runtime-based CSS-in-JS libraries:

  1. Don’t over-compose styled components
    Basically don’t do what I did and try to compose 3 individual styled instances, just to create a freaking button. If you want to “share” code, make use of the css prop and compose tagged templates. This will save you lots of unneeded Context consumers, which means fewer components for React to manage, which means that React’s runtime can do its work faster.
  2. Prefer “static” components
    Some CSS-in-JS libraries will optimize their execution when your CSS has no dependencies on theme or props. The more “static” your tagged templates are, the higher the chances that your CSS-in-JS runtime will execute faster.
  3. Avoid unneeded React re-renders
    Make sure to only render when you need to, so you can avoid work by both React’s and CSS-in-JS library’s runtimes. Realistically, that should only be needed in extreme scenarios where a lot of heavy components are being simultaneously rendered on the screen.
  4. Investigate whether a zero-runtime CSS-in-JS library can work for your project
    Sometimes we tend to prefer writing CSS in JS for the DX (developer experience) it offers, without a need to have access to an extended JS API. If you app doesn’t need support for theming and doesn’t make heavy and complex use of the css prop, then a zero-runtime CSS-in-JS library might be a good candidate. As a bonus you will shave ~12KB off your overall bundle size, since most CSS-in-JS libraries range between 10KB — 15KB, while zero-runtime ones (like linaria) are < 1KB.

That’s it! Thanks a lot for reading

P.S. If you’ve ever wondered why the CSS rules are not editable by the devtools inspector, it’s because they make use of CSSStyleSheet.insertRule() . This is a really performant way of modifying a stylesheet, but one of its downsides is that the associated stylesheet is no longer editable through the inspector.


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数字化商业模式

数字化商业模式

大前研一 / 王小燕 / 中信出版社 / 2006-4 / 32.00元

《数字化商业模式》为商学院课程的第三部精华集锦,来自金融界、餐饮业、公共设施等领域的领军人物亲自讲述他们的成功案例,以及他们在思考技能、人才管理、事业构想、战略技能等方面的管理理念和战略。任何成功的企业家,不是人云亦云而是能够独立思考的人,不是依赖于他人而是执著、自立的人,不只是沿袭旧思路而是具备创新力、执行力的人。一起来看看 《数字化商业模式》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具