为什么我含泪告别CSS-in-JS?
时间:2023-03-14 15:01:05
科技观察
这篇文章将深入探讨最初吸引我使用CSS-in-JS以及导致我放弃它的原因。如果你对CSS-in-JS背后的逻辑感兴趣,建议耐心阅读本文。什么是CSS-in-JS?顾名思义,CSS-in-JS允许您通过直接在JavaScript或TypeScript代码中编写CSS来设置React组件的样式://@emotion/react(cssprop),withobjectstylesfunctionErrorMessage({children}){return({children}
);}//styled-components或@emotion/styled,字符串stylesconstErrorMessage=styled.div`颜色:红色;字体粗细:粗体;`;styled-components[1]和Emotion[2]是React社区中最流行的CSS-in-JS库。本文重点介绍运行时CSS-in-JS,包括样式化组件和Emotion。运行时CSS-in-JS只是意味着库在应用程序运行时解析和应用样式。编译时CSS-in-JS将在本文末尾简要讨论。CSS-in-JS的优缺点优点1.可以限制样式应用的范围。在编写纯CSS时,很容易不小心扩展样式的应用范围。例如,假设您正在创建一个列表视图并且每一行都需要有一些填充和边框。你可以这样写CSS:.row{padding:0.5rem;border:1pxsolid#ddd;}几个月后,您完全忘记了列表视图并创建了另一个带有列表行的组件。当然,您可以在这些元素上设置className="row"。现在,新组件的列表行有一个丑陋的边框,你不知道为什么!虽然可以通过使用更长的类名或更具体的选择器来解决此类问题,但作为开发人员,您需要确保不存在类名冲突。CSS-in-JS通过控制默认应用样式的位置完全解决了这个问题。如果您将列表视图行写为:
... 那么填充和边框就不可能意外地应用在不相关的元素上。注意:CSS模块还提供局部范围的样式。2.集中投放。如果你使用纯CSS,那么你可以将所有.css文件放在src/styles目录中,将所有React组件放在src/components中。但随着应用程序规模的增长,很快就会变得难以判断每个组件使用哪种样式。所以这样的CSS往往会变成死代码,因为没有简单的方法来确定这些样式是否被使用过。组织代码的更好方法是将与单个组件相关的所有内容都放在一个地方。这种做法称为汇集。问题是这在使用纯CSS时很难实现,因为CSS和JavaScript必须放在单独的文件中,并且无论.css文件位于何处,样式都会全局应用。另一方面,如果你使用CSS-in-JS,你可以直接在使用它们的React组件中编写样式!正确完成后,这可以大大提高应用程序的可维护性。注意:CSS模块还允许样式与组件一起放置,即使不在同一个文件中。3.脚本变量可以在样式中使用。CSS-in-JS使您能够在样式规则中引用JavaScript变量,例如://colors.tsexportconstcolors={primary:'#0d6efd',border:'#ddd',/*...*/};//MyComponent.tsxfunctionMyComponent({fontSize}){return(
...);}如本例所示,您可以在CSS-in-JS样式中使用JavaScript常量(例如colors)和React属性/状态(例如fontSize)。在某些情况下,在样式中使用JavaScript常量的能力减少了代码重复,因为不必将相同的常量定义为CSS变量和JavaScript常量。使用props和state的能力允许您创建具有高度可定制样式的组件,而无需使用内联样式。(当相同的样式应用于许多元素时,内联样式对性能不友好。)缺点1.CSS-in-JS增加了运行时开销。渲染组件时,CSS-in-JS库必须将样式“序列化”为可插入文档的普通CSS。显然,这需要额外的CPU开销。2.CSS-in-JS会增加Bundle的大小。这是一个明显的问题——每个访问网站的用户都必须下载一个CSS-in-JS库。Emotion是一个7.9kB的压缩包,而styled-components是12.7kB。虽然这两个库都不大,但加起来就很大(react+reactdom是44.5kB)。3.CSS-in-JS将颠覆React开发工具。Emotion将使用cssprop为每个元素呈现和组件。如果在很多元素上使用cssprop,那么Emotion的内部组件真的会把ReactDevTools搞得一团糟,如下:最可怕的地方1.频繁插入CSS会迫使浏览器做很多额外的工作。在并发渲染中,React将让位于渲染之间的浏览器。如果将新的CSS规则插入到组件中,浏览器必须首先检查这些CSS规则是否适用于现有的DOM树,因此它会重新计算样式规则。之后React渲染下一个组件,发现一个新规则,同样的事情再次发生。这会导致在React渲染时每帧为所有DOM节点重新计算所有CSS规则。这个问题最糟糕的是它不是一个可修复的问题(在运行时CSS-in-JS的上下文中)。运行时CSS-in-JS库通过在组件呈现时插入新的样式规则来工作,这对基本性能不利。2.CSS-in-JS可能会出现较多的错误,尤其是在使用SSR或组件库时。在EmotionGitHub存储库中,存在以下问题:一次加载多个Emotion实例。组件库通常不会让您完全控制样式插入的顺序。Emotion的SSR支持在React17和React18之间的工作方式不同。这是与React18的流式服务器端渲染兼容所必需的。这些缺点只是冰山一角。深入研究性能会发现,运行时CSS-in-JS既有明显的优势,也有明显的劣势。为了说明为什么我选择远离这项技术,我们需要探索CSS-in-JS的实际性能影响。呈现内部与呈现外部序列化样式序列化是Emotion获取CSS字符串或对象样式并将它们转换为可以插入到文档中的纯CSS字符串的过程。Emotion还在序列化期间计算纯CSS的哈希值——这个哈希值是您在生成的类名中看到的,例如.css-15nl2r3。Emotion文档有一个在渲染中执行序列化的示例,如下所示:渲染MyComponent时,对象样式将再次序列化。如果频繁呈现MyComponent(例如,在每次击键时),则重复序列化可能会产生很高的性能开销。提高性能的方法是将样式移到组件外部,以便在加载模块时序列化一次,而不是每次渲染时序列化一次。@emotion/react的css函数可以这样做:constmyCss=css({backgroundColor:'blue',width:100,height:100,});functionMyComponent(){return;}当然,这会阻止您访问样式中的prop,从而失去CSS-in-JS的主要优势之一。使用Emotion时对会员浏览器进行基准测试下面是会员浏览器的简单列表视图。几乎所有的会员浏览器样式都使用了Emotion,尤其是cssprops。本次测试:Member浏览器会显示20个用户,去掉列表项周围的React.memo,强制最顶层的组件每秒渲染一次,并记录前10次渲染的时间。关闭React严格模式。使用ReactDevTools分析页面,前10次渲染时间的平均值为54.3毫秒。我个人的经验法则是React组件应该在16毫秒或更短的时间内渲染,因为在每秒60帧的情况下,渲染1帧是16.67毫秒。会员浏览器目前是这个数量的3倍多,因此它是一个非常重量级的组件。测试是在M1MaxCPU上进行的,它比普通用户拥有的要快得多。在性能较差的计算机上,54.3毫秒的渲染时间甚至可以轻松达到200毫秒。分析火焰图下面是来自上述测试的单个列表项的火焰图:正如您所看到的,有大量的和组件被渲染-全部使用css属性。虽然每个组件只需要0.1-0.2毫秒的渲染时间,但是由于组件的总量很大,总的耗时会非常庞大??。在没有Emotion的情况下对Member浏览器进行基准测试为了了解这种昂贵的渲染有多少是由于Emotion造成的,我重写了Member浏览器样式以使用Sass模块而不是Emotion。(Sass模块在构建时被编译为纯CSS,因此使用时几乎没有性能损失。)重复上述相同的测试,前10次渲染的平均时间为27.7毫秒。比原来少了48%!所以,这就是我们告别CSS-in-JS的原因:运行时性能成本太高。免责声明:如果您的代码库以更高效的方式使用Emotion(例如渲染之外的样式序列化),那么您可能看不到移除CSS-in-JS带来的显着性能提升。如果你对这个测试感兴趣,这里是原始数据:在新的样式系统下定决心摆脱CSS-in-JS之后,一个问题立即摆在我们面前:那么要替换什么?理想情况下,我们希望样式系统的行为类似于纯CSS,同时尽可能多地保留CSS-in-JS的优点。也就是说,最好具备:样式的适用范围是可以控制的。样式与应用样式的组件放在一起。脚本变量可以在样式中使用。之前我说过CSS模块还提供范围控制和集中样式的能力。CSS模块编译为普通的CSS文件,因此使用它们不会产生运行时性能成本。但CSS模块的主要缺点是,归根结底,它们仍然是纯CSS——而纯CSS缺乏改进DX和减少代码重复的功能。幸运的是,这个问题有一个简单的解决方案——Sass模块,用Sass编写的CSS模块。获得CSS模块的局部范围样式和Sass的强大构建功能,基本上没有运行时成本。这就是为什么Sass模块将成为我们未来的通用样式解决方案。实用程序类我们的团队对从Emotion模块切换到Sass模块的担忧之一是应用常见样式(如display:flex)会很不方便。以前:...如果您只对Sass模块执行此操作,则必须打开.module.scss文件并创建一个应用样式显示的类:flex和align-项目:中心。为了在这方面改进DX,我们决定引入实用类系统。实用程序类是在元素上设置单个CSS属性的CSS类。通常组合多个实用程序类以获得所需的样式。上面的例子可以写成:... Bootstrap和Tailwind是最流行的提供实用类的CSS框架。我已经使用Bootstrap多年,所以我选择了Bootstrap。虽然可以将Bootstrap实用程序类作为预构建的CSS文件引入,但我们需要自定义这些类以适应现有的样式系统,因此我将Bootstrap源代码的相关部分复制到项目中。将Sass模块和实用程序类用于新组件,我已经使用了几个星期并且非常高兴。DX类似于Emotion,但运行时性能要好得多。附注:也可以使用typed-scss-modules[3]为Sass模块生成typescript定义。这样做的最大好处是它允许我们定义一个utils()辅助函数,其工作方式类似于classnames[4]。但不方便的是,它只接受有效的实用程序类名称作为参数。关于编译时CSS-in-JS的注释本文重点介绍运行时CSS-in-JS库,例如Emotion和StyledComponents。最近,我发现有越来越多的CSS-in-JS库在编译时将样式转换为纯CSS。包括:Compiled[5]VanillaExtract[6]Linaria[7]这些库旨在提供与运行时CSS-in-JS类似的优势,而无需性能成本。虽然我自己没有使用过编译时CSS-in-JS库,但我仍然认为与Sass模块相比它们有缺点。这是我在查看编译时看到的缺点:首次安装组件时,仍会插入样式,这会迫使浏览器重新计算每个DOM节点上的样式。动态样式(如示例中的color属性)无法在构建时提取,因此编译时使用样式属性(也称为内联样式)将值添加为CSS变量。众所周知,当应用于许多元素时,内联样式会导致性能不佳。该库仍会将样式化的组件插入到React树中。这将使React开发工具和运行时CSS-in-JS一样混乱。总结本文深入探讨了CSS-in-JS在运行时的优缺点。作为开发人员,我们需要评估这些利弊,然后就该技术是否适合用例做出明智的决定。对我来说,Emotion的运行时性能成本远远超过DX的好处,尤其是考虑到Sass模块+实用程序类替代方案仍然具有良好的DX,同时还提供非常优越的性能。参考资料[1]styled-components:https://styled-components.com/[2]Emotion:https://emotion.sh/[3]typed-scss-modules:https://www.npmjs.com/package/typed-scss-modules[4]classnames:https://www.npmjs.com/package/classnames[5]Compiled:https://compiledcssinjs.com/[6]VanillaExtract:https://vanilla-extract.style/[7]Linaria:https://linaria.dev/