React应用程序中CSS-in-JS库的隐藏价格

在现代的前端应用程序中,CSS-in-JS技术非常流行。 事实是,它为开发人员提供了一种处理样式的机制,该机制比常规CSS更为方便。 不要误会我的意思。 我真的很喜欢CSS,但是创建一个好的CSS架构并不是一件容易的事。 与传统的CSS样式相比 ,CSS-in-JS技术具有一些明显的优势 。 但是,不幸的是,在某些应用程序中使用CSS-in-JS可能会导致性能问题。 在本文中,我将尝试解析最流行的CSS-in-JS库的高级功能,讨论使用它们时有时会出现的一些问题,并提出减轻这些问题的方法。



情况概述


在我公司,决定创建一个UI库。 这将给我们带来可观的收益,使我们能够在各种项目中重用标准的接口片段。 我是承担这项任务的志愿者之一。 我已经决定使用CSS-in-JS技术,因为我已经熟悉了大多数流行的CSS-in-JS库的样式API。 在工作过程中,我努力采取合理的行动。 我设计了可重用的逻辑,并在组件中应用了共享属性。 因此,我讨论了组件的组成。 例如, <IconButton />组件扩展了<BaseButton />组件,而该组件又是一个简单的styled.button实体的实现。 不幸的是,事实证明IconButton需要自己的样式,因此我将此组件转换为样式化的组件:

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

随着库中出现越来越多的组件,我们使用了越来越多的合成操作。 这似乎并不自然。 毕竟,组合实际上是React的基础。 在创建Table组件之前,一切都很好。 我开始感到该组件渲染缓慢。 特别是在表中的行数超过50的情况下。这是不正确的。 因此,我开始借助开发人员工具来了解问题。

顺便说一句,如果您想知道为什么无法使用开发人员工具检查器来编辑CSS规则,请注意这是因为它们使用CSSStyleSheet.insertRule() 。 这是修改样式表的快速方法。 但是它的缺点之一是这样一个事实,即相应的样式表不能再由检查员编辑。

不用说,React生成的树确实很大。 Context.Consumer组件的数量如此Context.Consumer ,以至于我无法入睡。 事实是,每次使用样式化组件情感来渲染单个样式化组件时,除了创建常规的React组件外,还会Context.Consumer一个额外的Context.Consumer组件。 为了使相应的脚本(大多数CSS-in-JS库取决于在页面运行时执行的脚本)正确执行所生成的样式规则是必要的。 通常,这不会引起任何特殊问题,但是我们一定不能忘记组件必须有权访问该主题。 这意味着需要为每个样式化元素呈现其他Context.Consumer ,这使您可以从ThemeProvider组件中“读取”主题。 结果,在具有主题的应用程序中创建样式化组件时,将创建3个组件。 这是一个StyledXXX组件,另外两个是Context.Consumer组件。

没错,这里没有什么特别可怕的,因为React可以快速完成工作,这意味着在大多数情况下我们无需担心。 但是,如果组装了几个样式化的组件以创建更复杂的组件怎么办? 如果这个复杂的组件是一个长列表或大表的一部分,其中至少呈现了100个这些组件,该怎么办? 在这种情况下,我们面临着问题...

剖析


为了测试不同的CSS-in-JS解决方案,我创建了一个简单的应用程序。 它显示Hello World文本50次。 在此应用程序的第一个版本中,我将此文本包装在常规div元素中。 在第二篇文章中 ,我使用了styled.div组件。 另外,我在应用程序中添加了一个按钮,该按钮可使所有这50个元素重新呈现。

渲染<App />组件后,将显示两个不同的React树。 下图显示了React推导出的元素树。


在使用常规div元素的应用程序中显示的树


在使用styled.div的应用程序中显示的树

然后,使用按钮,我渲染了10次<App /> ,以收集有关Context.Consumer其他组件Context.Consumer的系统负载的数据。 这是有关在设计模式下使用常规div元素反复重新渲染应用程序的信息。


在设计模式下使用常规div元素重新渲染应用程序。 平均值是2.54ms。


在开发模式下使用styled.div元素重新呈现应用程序。 平均值为3.98毫秒。

有趣的是,平均而言,CSS-in-JS应用程序比平时慢56.6%。 但这是一种发展模式。 生产模式呢?


在生产模式下使用通常的div元素重新呈现应用程序。 平均值为1.06 ms。


在生产模式下使用styled.div元素重新呈现应用程序。 平均值为2.27毫秒。

启用生产模式后,与开发模式下的相同版本相比,该应用程序的div实现似乎快50%以上。 一个styled.div应用程序仅快43%。 在这里,和以前一样,很明显,CSS-in-JS解决方案的速度几乎是通常解决方案的两倍。 是什么让他慢下来?

在执行过程中分析应用程序


关于什么会导致CSS-in-JS应用程序变慢的问题,显而易见的答案是:“据说一百个CSS-in-JS库为每个组件提供了两个Context.Consumer 。” 但是,如果您Context.Consumer考虑了所有这些,那么Context.Consumer只是一种用于访问JS变量的机制。 当然,React需要做一些工作才能弄清楚从何处读取相应的值,但这本身并不能解释上述测量结果。 通过分析使用Context.Consumer的原因,可以找到该问题的真正答案。 事实是,大多数CSS-in-JS库都依赖浏览器在页面输出期间运行的脚本,这有助于库动态更新组件样式。 这些库不会在页面汇编期间创建CSS类。 相反,它们在文档中动态生成和更新<style>标签。 这是在安装组件或更改其属性时完成的。 这些标签通常包含一个CSS类,其哈希名称映射到单个React组件。 当此组件的属性更改时,相应的<style>标记也必须更改。 这是描述在此过程中正在执行的操作的方法:

  • <style>标签必须具有的CSS规则将重新生成。
  • 创建一个新的哈希类名称,该名称用于存储上述CSS规则。
  • 相应的React组件的classname属性被更新为一个新的,指示刚刚创建的类。

例如,考虑styled-components库。 创建styled.div组件时styled.div styled.div组件分配一个内部标识符(ID),并将新的<style>标记添加到<head> HTML标记。 该标签包含一个注释,该注释引用相应样式所属的React组件的内部标识符:

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

以下是样式化组件库在显示相应的React组件时执行的操作:

  1. 从样式组件模板字符串中解析CSS规则。
  2. 生成新的CSS类名称(或查找是否保留先前的名称)。
  3. 使用手写笔执行样式的预处理。
  4. 将预处理产生 CSS 嵌入到HTML <head>的相应<style>标签中。

为了能够在此过程的步骤1中使用主题, 需要Context.Consumer 。 由于有了这个组件,可以从模板字符串中的主题中读取值。 为了能够修改与此组件关联的<style>标记,需要从该组件中再添加一个Context.Consumer 。 它允许您访问样式表的实例。 这就是为什么在大多数CSS-in-JS库中,我们会遇到两个Context.Consumer实例。

除此之外,由于所有这些计算都会影响用户界面,因此应注意,它们必须在组件渲染阶段执行。 它们无法在React组件的生命周期的事件处理程序代码中执行(这样,它们的执行可能会延迟,并且对于用户而言看起来像是缓慢的页面形成)。 这就是为什么渲染style.div应用程序比渲染常规应用程序慢的原因。

所有这些已被样式组件开发人员注意到。 他们优化了库,以减少组件的重新渲染时间。 尤其是,库会找出样式化的组件是否为“静态”。 也就是说,组件的样式是取决于主题还是传递给它的属性。 例如,静态组件如下所示:

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

而且此组件不是静态的:

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

如果该库发现该组件是静态的,则跳过上述4个步骤 ,从而意识到它永远不必更改生成的类名(因为没有动态元素,可能需要更改与其关联的CSS规则)。 此外,在这种情况下,库不会在风格化组件周围显示 ThemeContext.Consumer ,因为主题依赖项不再允许该组件被视为“静态”。

如果您在分析先前显示的屏幕截图时足够谨慎,那么您可能会注意到,即使在生产模式下,每个styled.div也会Context.Consumer两个Context.Consumer组件。 有趣的是,由于没有动态CSS规则与之关联,因此呈现的组件是“静态的”。 在这种情况下,可以期望如果使用样式组件库编写了此示例,则该示例将不会Context.Consumer使用主题Context.Consumer 。 之所以在此处显示两个Context.Consumer ,是因为该实验(上面给出了数据)是使用情感进行的,这是另一个CSS-in-JS库。 该库几乎采用与样式化组件相同的方法。 它们之间的差异很小。 因此,情感库解析模板字符串,使用手写笔对样式进行预处理并更新相应的<style> 。 但是,在这里,应注意样式元素和情感之间的一个主要区别。 它包含以下事实:情感库始终将所有组件包装在ThemeContext.Consumer -不管它们是否使用主题(这都解释了上面的屏幕快照的外观)。 有趣的是,即使情感比样式组件提供更多的消费者组件,但在性能方面,情感要优于样式组件。 这表明Context.Consumer组件的数量不是减慢渲染速度的主要因素。 值得注意的是,在编写本材料时,发布了样式组件v5.xx的测试版,据该库的开发人员称, 在性能方面绕过了情感。

总结一下我们在说什么。 事实证明,许多Context.Consumer元素(这意味着React必须协调其他元素的工作)和内部动态样式机制的组合会减慢应用程序的速度。 我必须说,每个组件添加到<head>所有<style>标记都不会删除。 这是由于元素删除操作在DOM上造成了很大的负担(例如,因此浏览器必须重新排列页面)。 此负载高于页面上由于不必要的<style>元素的存在而导致的系统额外负载。 老实说,我不能自信地说不必要的<style>标签会导致性能问题。 它们只是存储在页面操作期间生成的未使用的类(即,该数据未通过网络传输)。 但是您应该了解使用CSS-in-JS技术的此功能。

我必须说<style>标记不会创建所有CSS-in-JS库,因为并不是所有的CSS-in-JS库都基于页面在浏览器中工作时的运行机制。 例如,当页面在浏览器中运行时, linaria库什么也不做。

它在项目的构建过程中定义了一组固定的CSS类,并调整模板字符串中所有动态规则(即,取决于组件属性的CSS规则)与自定义CSS属性的对应关系。 结果,当组件属性更改时,CSS属性也会更改,并且界面外观也会更改。 因此,linaria比依赖页面运行时运行的机制的库快得多。 问题是,使用此库时,系统在组件渲染期间必须执行少得多的计算。 在渲染过程中使用linaria时,您唯一需要做的就是记住更新自定义CSS属性。 但是,与此同时,此方法与IE11不兼容,它对流行的CSS属性的支持有限,并且没有其他配置,就不允许您使用主题。 与其他Web开发领域一样,在CSS-in-JS-library中没有一个适合所有场合的理想库。

总结


一次CSS-in-JS技术看起来像是样式领域的一场革命。 它使许多开发人员的工作变得更加轻松,并且无需进行额外的工作就可以解决很多问题,例如名称冲突和使用浏览器制造商前缀。 本文旨在阐明一些流行的CSS-in-JS库(在页面运行时控制样式的库)如何影响Web项目的性能的问题。 我要特别注意以下事实:这些库对性能的影响并不总是会导致出现明显的问题。 实际上,在大多数应用中,这种效果是完全不可见的。 在页面包含数百个复杂组件的应用程序中可能会出现问题。

CSS-in-JS的好处通常超过了使用该技术的潜在负面影响。 但是,那些应用程序呈现大量数据的开发人员,其项目包含许多不断变化的界面元素的开发人员应牢记CSS-in-JS的缺点。 如果您怀疑自己的应用受到CSS-in-JS的负面影响,那么在进行重构之前,值得对所有内容进行适当的评估和衡量。

以下是一些使用流行的CSS-in-JS库提高应用程序性能的技巧,这些库可在浏览器中运行页面时发挥作用:

  1. 不要对程式化组件的组成太过迷恋。 为了创建一个不幸的按钮,请不要重复我一开始所谈论的错误,而不要尝试构建三个样式化组件的组合。 如果要“重用”代码,请使用CSS属性并编写模板字符串。 这将使您不需要Context.Consumer的许多不必要的组件。 结果,React将不得不管理更少的组件,这将提高项目的生产率。
  2. 力争使用“静态”组件。 如果组件的样式不取决于主题或属性,则某些CSS-in-JS库会优化生成的代码。 模板字符串中的“静态”越多,CSS-in-JS库中的脚本运行得越快的可能性就越大。
  3. 尝试避免对React应用程序进行不必要的重新渲染操作。 力争仅在确实需要时才渲染。 因此,React操作和CSS-in-JS库操作都不会加载。 重新渲染是仅在特殊情况下才应执行的操作。 例如-同时撤回大量“重”组件。
  4. 找出CSS-in-JS库是否适合您的项目,该库不使用在浏览器中运行页面时执行的脚本。 有时,我们选择CSS-in-JS技术是因为开发人员更方便地使用它,而不是各种JavaScript-API。 但是,如果您的应用不需要大量使用CSS属性就不需要支持,那么很可能像linaria这样的CSS-in-JS库不会使用在页面运行时运行的脚本。 此外,这种方法还将使应用程序包的大小减少约12 Kb。 事实是,大多数CSS-in-JS库的代码大小都在12-15 Kb之内,而相同的linaria的代码小于1 Kb。

亲爱的读者们! 您是否使用CSS-in-JS库?


Source: https://habr.com/ru/post/zh-CN480358/


All Articles