您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > CSS

为什么我们正在放弃 CSS-in-JS

时间:2022-10-27 15:11:07  来源:今日头条  作者:程序猿最幽默

这篇文章将深入的挖掘我当时为什么会在项目中使用 css-in-JS (本文使用 Emotion 方案 ),而现在为什么正在放弃这样的方案。

什么是 CSS-in-JS

CSS-in-JS 允许你直接使用 JAVAScript 或者 TypeScript 修改你的 React 组件的样式

import styled from '@emotion/styled'

const ErrorMessageRed = styled.div`
  color: red;
  font-weight: bold;
`;

function App() {
  return (
   <div>
    <ErrorMessageRed>
      hello ErrorMessageRed !!
    </ErrorMessageRed>
   </div>
  );
}

export default App;

styled-components 和 Emotion 是 React 社区最流行的 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-scoped styles: 当我们在裸写 CSS 的时候,很容易就污染到其他我们意想不到的组件。比如我们写了一个列表,每一行的需要加一个内边距和边框的样式。我们可能会写这样的 CSS 代码

.row {
  padding: 0.5rem;
  border: 1px solid #ddd;
}

几个月之后可能你已经忘记了这个列表的代码了,然后你写了 className="row" 在另外的组件上,那么这个新的组件有了内边距合边框样式,你甚至都不知道为什么会这样。你可以使用更长的类名或者更加明确的选择器来避免这样的情况发生,但是你还是无法完全保证不会再出现这样的样式冲突。

CSS-in-JS 就可以通过 Locally-scoped styles 来完全解决这个问题。如果你的列表代码这么写的话:

<div className={css`
        padding: 0.5rem;
        border: 1px solid #ddd;
    `}>
	...row item...
 </div>

这样的话,内边距和边框的样式永远不会影响到其他组件。

提示:CSS Modules 也提供了 Locally-scoped styles

2. Colocation: 你的 React 组件是写在 src/components 目录中的,当你裸写 CSS 的时候,你的 .css 文件可能是放置在 src/styles 目录中。随着项目越来越大,你很难明确哪些 CSS 样式是用在哪些组件上,这样最后你会冗余很多样式代码。

一个更好的组织代码的方式可能是将相关的代码文件放在同个地方。这种做法成为「共置」,可以通过这篇文章了解一下。

问题在于其实很难实现所谓的「共置」。如果在项目中裸写 CSS 的话,你的样式和可能会作用于全局不管你的 .css 文件被放置在哪里。另一方面,如果你使用 CSS-in-JS,你可以直接在 React 组件内部书写样式,如果组织得好,那么你的项目的可维护性将大大提升。

提示:CSS Modules 也提供了「共置」的能力

3. 在样式中使用 JavaScript 变量: CSS-in-JS 提供了让你在样式中访问 JavaScript 变量的能力


function App(props) {
    const color = "red";
    const ErrorMessageRed = styled.div`
      color: ${props.color || color};
      font-weight: bold;
    `;
    
    return (
        <div>
            <ErrorMessageRed>
              hello ErrorMessageRed !!
            </ErrorMessageRed>
        </div>
    );
}

上面的例子展示了,我们可以在 CSS-in-JS 方案中使用 JavaScript 的 const 变量 或者是 React 组件的 props。这样可以减少很多重复代码,当我们需要同时在 JavaScript 和 CSS 两侧定义相同的变量的时候。我们通过这样的能力可以不需要使用 inline styles 这样的方式来完成高度自定义的样式。( inline styles 对性能不是特别友好,当我们有很多相同的样式写在不同的组件的时候)

中立点

1. 这是热门的新技术: 许多的开发者包括我自己,会更热衷于使用 JavaScript 社区中热门的新技术。一个重要的原因是,很多新的框架或者库,能够提升带来巨大的性能或者体验上的提升(想象一下,React 对比 jQuery 带来的开发效率提升)。另一个原因就是,我们对新技术抱有比较开放的态度,我们不愿意错过每个大事件。当然了,我们在选择新的技术的时候也会考虑到它带来的负面影响。这大概就是我之前选择 CSS-in-JS 的原因。

缺点

  1. CSS-in-JS 的运行时问题。当你的组件进行渲染的时候,CSS-in-JS 库会在运行时将你的样式代码 ”序列化” 为可以插入文档的 CSS 。这无疑会消耗浏览器更多的 CPU 性能
  2. CSS-in-JS 让你的包体积更大了。 这是一个明显的问题。每个访问你的站点的用户都不得不加载关于 CSS-in-JS 的 JavaScript。Emotion 的包体积压缩之后是 7.9k ,而 styled-components 则是 12.7 kB 。虽然这些包都不算是特别大,但是如果再加上 react & react-dom 的话,那也是不小的开销。
  3. CSS-in-JS 让 React DevTools 变得难看。 每一个使用 css prop 的 react 元素, Emotion 都会渲染成 <EmotionCssPropInternal> 和 <Insertion> 组件。如果你使用很多的 css prop,那么你会在 React DevTools 看到下面这样的场景

 

  1. 频繁的插入 CSS 样式规则会迫使浏览器做更多的工作。 React 团队核心成员&React Hooks 设计者 Sebasian 写了一篇关于 CSS-in-JS 库如何与 React 18 一起工作的文章。他特别说到

在 concurrent 渲染模式下,React 可以在渲染之间让出浏览器的控制权。如果你为一个组件插入一个新的 CSS 规则,然后 React 让出控制权,浏览器会检查这个新的规则是否作用到了已有的树上。所以浏览器重新计算了样式规则。然后 React 渲染下一个组件,该组件发现一个新的规则,那么又会重新触发样式规则的计算。

实际上 React 进行渲染的每一帧,所有 DOM 元素上的 CSS 规则都会重新计算。这会非常非常的慢

更坏的是,这个问题好像是无解的(针对运行时 CSS-in-JS)。运行时 CSS-in-JS 库会在组件渲染的时候插入新的样式规则,这对性能来说是一个很大的损耗。

  1. 使用 CSS-in-JS ,会有更大的概率导致项目报错,特别是在 SSR 或者组件库这样的项目中。在 Emotion 的 Github 仓库,我们可以看到很多向如下的 issue

我在我的 SSR 项目中使用了 Emotion,但是它报错了,因为…….

在这些海量的 issue 中,我们可以找到一些共同特征:

  • 多个 Emotion 实例被同时加载。如果多个被同时加载的实例是相同的Emotion 版本,这将会引起很多问题(比如说)
  • 组件库通常无法让您完全控制插入样式的顺序(比如说)
  • Emotion 的 SSR 能力支持对于 React 17 和 18 两个版本是不相同的。我们需要做一些兼容性的工作来兼容 React 18 的 stream SSR(比如说)

相信我,上述的这些问题仅仅是冰山一角。

性能检测

在这一点上,很明显,CSS-in-JS 有着显著的优点和缺点。为了明白我们为什么正在移除这项技术,我们需要更加真实的 CSS-in-JS 性能场景。这里我们会着重关注 Emotion 对于性能的影响。Emotion 有很多种使用方式,每种方式都有其各自的性能表现特点。

内部序列化渲染 vs. 外部序列化渲染

样式序列化指的是 Emotion 将你的 CSS 字符串或者样式对象转化成可以插入文档的纯 CSS 字符串。Emotion 同时也会在序列化的过程中根据生成的存 CSS 字符串计算出相应的哈希值——这个哈希值就是你可以看到的动态生成的类名,比如 css-an61r6

在测试前,我预感到这个样式序列化是在 React 组件渲染周期里面完成还是外面完成,将对 Emotion 的性能表现起到比较大的影响。

在渲染周期内完成的代码如下

function MyComponent() {
  return (
    <div
      css={{
        backgroundColor: 'blue',
        width: 100,
        height: 100,
      }}
    />
  );
}

每次 MyComponent 渲染,样式对象都会被序列化一次。如果 MyComponent 渲染的比较频繁,重复的序列化将有很大的性能开销

一个性能更好的方案是把样式移到组件的外面,所以序列化过程只会在组件模块被载入的时候发生,而不是每次都要执行一遍。你可以使用 @emotion/react 的 css 方法

const myCss = css({
  backgroundColor: 'blue',
  width: 100,
  height: 100,
});

function MyComponent() {
  return <div css={myCss} />;
}

当然,这样使得你无法在样式种获得组件的 props,所以你会错失 CSS-in-JS 的一个主要的卖点。

测试「成员检索」功能

我们接下来将使用在一个页面上实现「成员检索」的能力,就是使用一个列表展示团队成员的一个简单的功能。列表上几乎所有的样式都是通过 Emotion 来实现,特别是使用 css prop

(为了保障信息安全,我截图了网络上一张类似的图片,功能几乎一样)

 

测试如下:

  • 「成员检索」会在页面上显示 20 个用户
  • 去除 react.memo 对列表的包裹
  • 每秒都强制渲染 组件,记录前 10 次渲染的时间
  • 关闭 React Strict 模式 (不然会触发重复渲染,时间可能是现在的 2 倍)

我使用 React DevTools 进行记录,得到前 10 次的平均渲染时间为 54.3 毫秒。

以往的经验告诉我,一个 React 组件最好的渲染时间大概是 16 毫秒(每秒 60 帧计算)。 < BrowseMembers > 组件的渲染时间是经验值的 3 倍左右,所以它是一个比较「重」的组件。

如果我去除 Emotion,而使用 Sass Modules 来实现页面的样式,平均的渲染时间大概是在 27.7 毫秒。这比原来使用 Emotion 少了将近 48% !!!

这就是为什么我们开始放弃使用 CSS-in-JS 的原因:运行时的性能消耗实在太严重了!!!

我们的新样式方案

在我们下定决心要移除 CSS-in-JS 之后,剩下的问题就是:我们应该什么方案来代替。我们既想要有裸写 CSS 这样的性能,又想要尽可能保留 CSS-in-JS 的优点。这里再次简单梳理一下 CSS-in-JS 的优点(忘记的同学可以翻回上面再看看):

  1. locally-scoped styles
  2. colocated
  3. 在 CSS 中使用 JS 变量

如果你有认真看这篇文章,那你应该还记得我在上文中提到,CSS Modules 其实也是可以提供 locally-scoped styles 和 colocated 这样类似的能力的。并且 CSS Modules 编译成原生 CSS 文件之后,没有运行时的性能开销。

在我看来,CSS Modules 的缺点在于,他们依然是原生的 CSS —— 原生 CSS 缺少提升开发体验以及减少冗余代码的能力。但是,如果当原生CSS 具备 nested selectors 的能力之后,情况将会改善很多。

幸好,市面上已经有了一个很简单的方案来解决这个问题—— Sass Modules ( 使用 Sass 来写 CSS Modules ) 。你既可以享受 CSS Modules 的 locally-scoped styles 能力,又可以享受 Sass 强大的编译时功能(去除运行时性能开销)。这就是我们会使用 Sass Modules 的一个重要原因。

注意:使用 Sass Modules ,你将无法享受到 CSS-in-JS 的第 3 个优点(在 CSS 中使用 JS 变量)。但是你可以使用 :export 块将 Sass 代码的常量导出到 JS 代码中。这个用起来不是特别方便,但是会使你的代码更加清晰。

Utility Classes

比较担心我们团队从 Emotion 切换到 Sass Modules 之后,会在写一些极度常用的样式的时候不是很方便,比如 display: flex 。之前我们是这样写的

<FlexH alignItems="center">...</FlexH>

如果改用 Sass Modules 之后,我们需要创建一个 .module.scss 文件,然后写一个 display: flex 和 align-item: center 。这不是世界末日,但肯定是不够方便的。

为了提升开发体验,我们决定引入一个 Utility Classes。如果你对 Utility Classes 还不是很熟悉,用一句话概括就是,“他们是一些只包含一个 CSS 属性的 CSS 类”。通常情况下,你会在你的元素上使用多个这样的类,通过组合的方式来修改元素的样式。对于上面的这个例子,你可能需要这样写:

<div className="d-flex align-items-center">...</div>

Bootstrap 和 TAIlwind 是目前最流行的提供 Utility Classes 的解决方案。这些库在设计方案上做了非常多的努力,这使得我们可以放心的使用他们,而不是自己重新搭建一个。因为我使用 Bootstrap 已经很多年了,所以我们选择了 Bootstrap。我们使用 Bootstrap 作为我们项目的预设样式方案。

我们已经在新组件上使用 Sass Modules 和 Utility Classes 好几个星期了。我们觉得都不错。它的开发体验跟 Emotion 差不多,但是运行时的性能更加的好。

我们也使用 typed-scss-modules 来为 Sass Modules 生成 TypeScript 的类型文件。也许这样做最大的好处就是允许我们定一个帮助函数 utils() ,这样我们可以像使用 classnames 去操作样式。

一些关于 构建时CSS-in-JS 方案

本文主要关注的是 运行时 CSS-in- JS 方案,比如 Emotion 和 styled-components 。最近,我们也关注到了一些将样式转换是纯 CSS 的构建时CSS-in-JS 方案。包括

  • Compiled
  • Vanilla Extract
  • Linaria

这些库的目标是为了提供类似于运行时 CSS-in-JS 的能力,但是没有性能损耗。

目前我还没有在真实项目中使用构建时 CSS-in-JS 方案。但我想这些方案对比 Sass Modules 大概会有以下的缺点:

  • 依然会在组件 mount 的时候完成样式的第一次插入,这还是会使得浏览器重新计算每个 DOM 节点的样式
  • 动态样式无法被抽取出来,所以会使用 CSS 变量加上行内样式的方法来替代。过多的行内样式依然会影响性能
  • 这些库依然会插入一些特定的组件到项目的 React 树中,依然会导致 React DevTools 的可读性变得比较差

结论

感谢你阅读到这里~任何事情都是,有它好的一面也有它不好的一面。最终,作为开发人员,你必须评估这些优缺点,然后就该技术是否适合你的项目,然后做出决定。而对于目前我所在的团队来说,Emotion 带来的运行时性能消耗的影响已经大于它带来的开发体验的好处。而我们目前所使用的 Sass Modules 加上 Utility Classes 方案,在一定程度上也弥补了开发体验的问题。



Tags:CSS   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
12 个超级实用的 CSS 技巧
user-selectuser-select 属性可以用来控制用户是否能够选择文本。<div> <p>You can&#39;t select this text.</p></div><p>You can select this text.</p>CSS:div { width...【详细内容】
2023-12-19  Search: CSS  点击:(129)  评论:(0)  加入收藏
Vue里使用Tailwind CSS,这不是耍流氓吗
前言随着前端的发展,对前端页面的要求越来越高,而css的功能也越来越强大,但对于写css样式来说却是非常头疼的的事。因为前端页面的动画要求以及页面布局的精细管控,需要写大量的...【详细内容】
2023-12-11  Search: CSS  点击:(163)  评论:(0)  加入收藏
原生CSS嵌套使用,你学明白了吗?
如果你是一个前端开发人员,那么你应该使用过CSS预处理器以及预处理器中的嵌套特性。它一直是一个受欢迎的功能,我一直都在使用CSS预处理器。今年所有的主流浏览器都支持原生CS...【详细内容】
2023-12-06  Search: CSS  点击:(183)  评论:(0)  加入收藏
CSS_Flex 那些鲜为人知的内幕
前言Flex想必大家都很熟悉,也是大家平时在进行页面布局的首选方案。(反正我是!)。不知道大家平时在遇到Flex布局属性问题时,是如何查阅并解决的。反正,我每次记不住哪些属性或...【详细内容】
2023-12-06  Search: CSS  点击:(140)  评论:(0)  加入收藏
CSS:这几个伪类,你用了吗
## :root 伪类:root 伪类是匹配文档的根元素,很多时候,根元素也就是 html 元素,用 root 伪类来匹配根元素,目的就是解决根元素不是 html 的场景,比如根元素是 svg 的时候。 root...【详细内容】
2023-11-30  Search: CSS  点击:(169)  评论:(0)  加入收藏
新 CSS Math方法:Rem() 和 Mod()
CSS 添加了许多新的数学函数来补充旧有的函数(如 calc() 和最近的 clamp() )。这些函数最终都表示一个数值,但其工作原理的细微差别并不总是一开始就很清楚。本文介绍每个函数...【详细内容】
2023-11-23  Search: CSS  点击:(254)  评论:(0)  加入收藏
CSS 新功能:让编码更高效
CSS 是一种不断发展的语言。每一次迭代,它都会变得越来越好。因此,了解最新的 CSS 功能非常重要,这样你才能在项目中使用它们,减少对第三方库的依赖。本文将介绍一些即将推出的...【详细内容】
2023-11-16  Search: CSS  点击:(165)  评论:(0)  加入收藏
使用 CSS Grid 的响应式网页设计:消除媒体查询过载
前言你是否厌倦了在实现响应式网站时需要管理多个媒体查询?说再见复杂的代码,拥抱更简单的解决方案吧:CSS Grid。在这篇文章中,我们将踏上一场激动人心的 CSS Grid 之旅,发现它如...【详细内容】
2023-11-10  Search: CSS  点击:(277)  评论:(0)  加入收藏
CSS这几个函数很实用,也很简单
calc()CSS 的 calc 函数非常实用,很多情况下,我们都会用到这个函数。calc 函数支持加减乘除四种运算,但是,它也有限制: 运算符前后带有单位或者百分比的数值,只能进行加减,不能进行...【详细内容】
2023-11-08  Search: CSS  点击:(219)  评论:(0)  加入收藏
这三个CSS生成器千万别错过啦!
前言 大家好我是小卢,「新拟态风格」和「磨砂玻璃」风格都是让人眼前一亮的一种设计风格,已经成为了最具代表性的设计趋势之一。 但是我们在真正写 CSS 的时候不容易调整这个...【详细内容】
2023-11-03  Search: CSS  点击:(159)  评论:(0)  加入收藏
▌简易百科推荐
12 个超级实用的 CSS 技巧
user-selectuser-select 属性可以用来控制用户是否能够选择文本。<div> <p>You can&#39;t select this text.</p></div><p>You can select this text.</p>CSS:div { width...【详细内容】
2023-12-19  前端充电宝  微信公众号  Tags:CSS   点击:(129)  评论:(0)  加入收藏
原生CSS嵌套使用,你学明白了吗?
如果你是一个前端开发人员,那么你应该使用过CSS预处理器以及预处理器中的嵌套特性。它一直是一个受欢迎的功能,我一直都在使用CSS预处理器。今年所有的主流浏览器都支持原生CS...【详细内容】
2023-12-06  南城大前端  微信公众号  Tags:CSS   点击:(183)  评论:(0)  加入收藏
CSS_Flex 那些鲜为人知的内幕
前言Flex想必大家都很熟悉,也是大家平时在进行页面布局的首选方案。(反正我是!)。不知道大家平时在遇到Flex布局属性问题时,是如何查阅并解决的。反正,我每次记不住哪些属性或...【详细内容】
2023-12-06  前端柒八九  微信公众号  Tags:CSS   点击:(140)  评论:(0)  加入收藏
CSS:这几个伪类,你用了吗
## :root 伪类:root 伪类是匹配文档的根元素,很多时候,根元素也就是 html 元素,用 root 伪类来匹配根元素,目的就是解决根元素不是 html 的场景,比如根元素是 svg 的时候。 root...【详细内容】
2023-11-30  读心悦  微信公众号  Tags:CSS   点击:(169)  评论:(0)  加入收藏
让你开发更舒适的 Tailwind 技巧
免费体验 Gpt4 plus 与 AI作图神器,我们出的钱 体验地址:体验使用 Tailwind CSS,我避免了在 React 项目中复制大量 CSS 文件的麻烦,使网页开发变得更加迅速高效。虽然 Tailwind...【详细内容】
2023-11-28  大迁世界  微信公众号  Tags:Tailwind   点击:(177)  评论:(0)  加入收藏
Display和Visibility的区别,你了解了吗?
采用CSS实现元素隐藏的方法有很多种,比如定位到屏幕之外、透明度变换等。而常见的两种方式是将元素设置为display:none或者visibility:hidden。元素样式设置为display:none当...【详细内容】
2023-11-27  读心悦  微信公众号  Tags:Display   点击:(172)  评论:(0)  加入收藏
新 CSS Math方法:Rem() 和 Mod()
CSS 添加了许多新的数学函数来补充旧有的函数(如 calc() 和最近的 clamp() )。这些函数最终都表示一个数值,但其工作原理的细微差别并不总是一开始就很清楚。本文介绍每个函数...【详细内容】
2023-11-23  大迁世界  微信公众号  Tags:CSS   点击:(254)  评论:(0)  加入收藏
CSS 新功能:让编码更高效
CSS 是一种不断发展的语言。每一次迭代,它都会变得越来越好。因此,了解最新的 CSS 功能非常重要,这样你才能在项目中使用它们,减少对第三方库的依赖。本文将介绍一些即将推出的...【详细内容】
2023-11-16  大迁世界  微信公众号  Tags:CSS   点击:(165)  评论:(0)  加入收藏
使用 CSS Grid 的响应式网页设计:消除媒体查询过载
前言你是否厌倦了在实现响应式网站时需要管理多个媒体查询?说再见复杂的代码,拥抱更简单的解决方案吧:CSS Grid。在这篇文章中,我们将踏上一场激动人心的 CSS Grid 之旅,发现它如...【详细内容】
2023-11-10  前端YUE  微信公众号  Tags:CSS   点击:(277)  评论:(0)  加入收藏
2024年了,别只使用React,需要学习一下Vue,不然没出路了
最近,我的朋友因为不熟悉 Vue.js 而未能通过面试。她平时工作中大部分时间都在使用React,所以也懒得去了解其他前端框架。世界上所有的前端框架我们都应该熟悉吗?不,这是极其不...【详细内容】
2023-11-08  web前端开发  微信公众号  Tags:Vue   点击:(295)  评论:(0)  加入收藏
站内最新
站内热门
站内头条