为什么我们要放弃CSS-in-JS
本文将深入探讨为什么我在我的项目中使用CSS-in-JS(本文使用Emotion解决方案)以及为什么现在要放弃它。什么是CSS-in-JSCSS-in-JS允许您直接使用JavaScript或TypeScript修改您的React组件的样式importstyledfrom'@emotion/styled'constErrorMessageRed=styled.div`font-weight:bold;`;functionApp(){return(
helloErrorMessageRed!!
);}exportdefaultApp;styled-components和Emotion是最流行的CSS-in-JS方案。我在本文中只提到了Emotion,但我相信大多数使用场景也适用于styled-components。本文重点介绍CSS-in-JS的运行时类型,styled-components和Emotion都属于此类。因为还有另外一种CSS-in-JS类型,文末会稍微提到编译时类型CSS-in-JS。CSS-in-JS的优点和缺点在我们深入研究CSS-in-JS模式及其对性能的影响之前,让我们先大致了解一下我们为什么使用这项技术以及为什么我们逐渐放弃优势1.Locally-scopedstyles:当我们裸写CSS时,很容易污染其他意想不到的组件。比如我们写一个列表,每一行都需要添加padding和border样式。我们可以这样写CSS代码.row{padding:0.5rem;border:1pxsolid#ddd;}几个月后,你可能忘记了这个列表的代码,然后你在另一个Component中写了className="row",那么这个新组件就有了padding和border样式,而你并没有甚至不知道为什么。您可以通过使用更长的类名或更具体的选择器来避免这种情况,但您仍然不能完全保证不会再次发生此类样式冲突。CSS-in-JS可以通过Locally-scoped样式彻底解决这个问题。如果你的列表代码是这样写的:
...rowitem... 在这种情况下,内边距和边框的样式永远不会影响其他组件。提示:CSSModules还提供Locally-scoped样式2.托管:您的React组件写在src/components目录中。当你裸写CSS时,你的.css文件可能会放在src/styles目录中。随着项目越来越大,你很难知道在哪些组件上使用了哪些CSS样式,所以你最终会得到大量冗余的样式代码。组织代码的更好方法可能是将相关代码文件放在同一个地方。这种做法被称为“托管”,可以通过本文学习。问题是,所谓的“托管”其实很难做到。如果你在项目中编写裸CSS,那么无论你的.css文件放在哪里,你的样式和样式都可能被全局应用。另一方面,如果你使用CSS-in-JS,你可以直接在React组件内部编写样式,如果组织得好,你的项目的可维护性将大大提高。提示:CSSModules还提供了“并置”的能力3.在样式中使用JavaScript变量:CSS-in-JS提供了允许您在样式中访问JavaScript变量的能力functionApp(props){constcolor="red";constErrorMessageRed=styled.div`颜色:${props.color||颜色};字体粗细:粗体;`;return(
helloErrorMessageRed!!
);}上面的例子表明我们可以在CSS-in-JS解决方案中使用JavaScriptconst变量或React组件props。当我们需要在JavaScript和CSS两边定义同一个变量时,这可以减少很多重复代码。通过这种能力,我们可以在不使用内联样式的情况下完成高度自定义的样式。(内联样式对性能不是特别友好,当我们有很多相同的样式写在不同的组件中时)中性点1.这是一个热门的新技术:很多开发者,包括我自己,会更热衷于使用JavaScript社区热点新技术。一个重要的原因是很多新的框架或库可以提升性能或体验(想象一下React相对于jQuery带来的开发效率的提升)。还有一个原因就是我们对新技术比较开放,不想错过每一个大事件。当然,我们在选择新技术的时候,也会考虑到它带来的负面影响。这大概也是我之前选择CSS-in-JS的原因。缺点CSS-in-JS运行时问题。当您的组件呈现时,CSS-in-JS库会将您的样式代码“序列化”为可在运行时插入到文档中的CSS。这无疑会消耗浏览器更多的CPU性能CSS-in-JS使您的包大小更大。这是一个明显的问题。每个访问您网站的用户都必须加载有关CSS-in-JS的JavaScript。Emotion压缩后的包大小为7.9k,而styled-components为12.7kB。虽然这些包都不是特别大,但是如果加上react&react-dom,也是一笔不小的开销。CSS-in-JS让ReactDevTools变得丑陋。对于每个使用cssprop的React元素,Emotion将渲染
和组件。如果你使用了很多cssprops,那么你会在ReactDevTools中看到类似下面的场景。频繁插入CSS样式规则会迫使浏览器做更多的工作。React团队核心成员和ReactHooks设计师Sebasian写了一篇关于CSS-in-JS库如何与React18一起工作的文章。他特别指出,在并发渲染模式下,React可以在渲染之间放弃对浏览器的控制。如果您为组件插入新的CSS规则,并且React放弃控制权,浏览器会检查新规则是否适用于现有树。所以浏览器重新计算样式规则。然后React渲染下一个组件,它找到一个新的规则,然后重新触发样式规则的计算。事实上,React渲染的每一帧,都会重新计算所有DOM元素上的CSS规则。这将非常非常缓慢,更糟糕的是,这个问题似乎无法解决(对于运行时CSS-in-JS)。运行时CSS-in-JS库会在组件渲染时插入新的样式规则,这是一个很大的性能损失。使用CSS-in-JS,会有更大的概率导致项目出错,尤其是在SSR或组件库等项目中。在Emotion的GitHub仓库中,我们可以看到很多问题如下。我在我的SSR项目中使用了Emotion,但是报错是因为……在这些海量问题中,我们可以发现一些共同的特点:同时加载多个Emotion实例。如果同时加载相同Emotion版本的多个实例,这可能会导致很多问题(例如)组件库通常不会让您完全控制插入样式的顺序(例如)Emotion的SSR对React17和18的能力支持这两个版本并不相同。我们需要做一些兼容性工作来兼容React18的流SSR(例如)相信我,以上问题只是冰山一角。基准测试在这一点上,很明显CSS-in-JS具有显着的优点和缺点。要理解我们为什么要移除这项技术,我们需要一个更真实的CSS-in-JS性能场景。这里我们将重点关注Emotion对性能的影响。情感可以以多种方式使用,每种方式都有自己的表现特点。内部序列化渲染vs.外部序列化渲染样式序列化是指Emotion将你的CSS字符串或样式对象转换成可以插入到文档中的纯CSS字符串。Emotion在序列化的过程中也会根据生成的css字符串计算出对应的hash值——这个hash值就是你看到的动态生成的类名,比如测试前的css-an61r6,我有预感这个样式序列化是不是在React组件渲染周期内完成还是在组件渲染周期外完成,都会对Emotion的性能产生比较大的影响。渲染周期完成的代码如下样式对象将被序列化一次。如果MyComponent被频繁渲染,重复序列化会有很大的性能开销。性能更好的解决方案是将样式移到组件外部,这样序列化过程只会在加载组件模块时发生,而不必每次都执行。你可以使用@emotion/react的css方法constmyCss=css({backgroundColor:'blue',width:100,height:100,});functionMyComponent(){return;}当然,这会阻止你在样式中获取组件的props,因此你会错过CSS-in-的主要卖点之一JS。测试“成员检索”功能接下来我们将使用功能在页面上实现“成员检索”,这是一个使用列表显示团队成员的简单功能。列表中几乎所有的样式都是通过Emotion实现的,尤其是使用了cssprop(为了保证信息安全,我在网上截图了一张类似的图片,功能也差不多)。测试如下:页面会显示“会员搜索”Show20usersremovereact.memowrappingthelist每秒强制渲染组件,记录前10次渲染的时间关闭ReactStrict模式(否则会触发重复渲染,时间可能是当前时间的两倍)我用ReactDevTools记录下前10的平均渲染时间是54.3毫秒。以往的经验告诉我,一个React组件的最佳渲染时间约为16毫秒(以每秒60帧计算)。组件的渲染时间约为经验值的3倍,因此是一个比较“重”的组件。如果我删除Emotion并使用Sass模块来设置页面样式,则平均渲染时间约为27.7毫秒。这几乎比使用Emotion少48%!!!这就是我们开始放弃CSS-in-JS的原因:运行时性能太差了!!!我们的新样式解决方案既然我们已经下定决心删除CSS-in-JS,剩下的问题是:我们应该怎么做。我们既要有裸CSS的性能,又要尽可能的保留CSS-in-JS的优点。这里再简单介绍一下CSS-in-JS的优点(忘记的同学可以回头看看):local-scopedstylescolocatedusesJSvariablesinCSS如果你仔细看了这篇文章,那你应该还记得如上所述,CSSModules实际上可以提供类似的功能,例如局部范围的样式和共置。并且CSSModules被编译成原生CSS文件后,没有运行时性能开销。在我看来,CSSModules的缺点在于它们仍然是原生CSS——原生CSS缺乏提升开发体验和减少冗余代码的能力。但是,如果原生CSS具有嵌套选择器的能力,情况就会改善很多。幸运的是,市场上已经有一个非常简单的解决方案来解决这个问题——SassModules(使用Sass编写CSSModules)。您不仅可以享受CSS模块的局部范围样式功能,还可以享受Sass强大的编译时功能(消除运行时性能开销)。这是我们将使用Sass模块的一个重要原因。注意:使用Sass模块,您将无法享受CSS-in-JS的第三个优势(在CSS中使用JS变量)。但是你可以使用:export块将常量从Sass代码导出到JS代码。这样用起来不是很方便,但是会让你的代码更清晰。UtilityClasses比较担心的是我们团队从Emotion切换到SassModules之后,写一些极其常用的样式会很不方便,比如display:flex。我们之前是这样写的...如果切换到SassModules,我们需要创建一个.module.scss文件,然后写一个display:flex和align-item:center.这不是世界末日,但肯定不够方便。为了改善开发体验,我们决定引入一个UtilityClasses。如果您对UtilityClasses不是很熟悉,用一句话来说就是“它们是一些只包含一个CSS属性的CSS类”。通常,您将在元素上使用多个这样的类来组合修改元素的样式。对于上面的例子,你可能需要这样写:... Bootstrap和Tailwind是目前最流行的提供UtilityClasses的解决方案。这些库在设计方案上下了很大的功夫,让我们可以放心使用,而不用自己重新构建。因为我已经使用了很多年的Bootstrap,所以我们选择了Bootstrap。我们使用Bootstrap作为我们项目的默认样式方案。几周以来,我们一直在新组件上使用Sass模块和实用程序类。我们都感觉很好。其开发体验与Emotion类似,但运行时性能更好。我们还使用typed-scss-modules为Sass模块生成TypeScript类型文件。也许这样做的最大好处是它允许我们定义一个辅助函数utils(),这样我们就可以使用类名来操作样式。关于构建时CSS-in-JS解决方案的一些内容本文重点介绍运行时CSS-in-JS解决方案,例如Emotion和styled-components。最近,我们还注意到一些构建时CSS-in-JS解决方案,其中样式转换是纯CSS。这些库(包括CompiledVanillaExtractLinaria)的目标是提供类似运行时的CSS-in-JS功能,但不会降低性能。我目前没有在实际项目中使用构建时CSS-in-JS解决方案。但我认为这些方案与SassModules相比可能存在以下不足:第一次插入样式还是会在组件挂载的时候完成,仍然会让浏览器重新计算每个DOM节点的样式,动态样式不能被提取。,因此将使用CSS变量和内联样式。过多的内联样式仍然会影响性能这些库仍然会在项目的React树中插入一些特定的组件,这仍然会导致ReactDevTools的可读性很差结论感谢您阅读本文~任何是,它都有它好的一面及其不好的一面。最终,作为开发人员,您必须评估这些利弊,并决定该技术是否适合您的项目。对于我现在的团队来说,Emotion的运行时性能消耗带来的影响大于它带来的开发体验带来的好处。我们目前使用的SassModulesplusUtilityClasses方案也一定程度上弥补了开发体验的问题。以上~也欢迎关注我的掘金号