乐趣区

关于css-in-js:为什么我们正在放弃-CSSinJS

这篇文章将深刻的开掘我过后为什么会在我的项目中应用 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/reactcss 办法

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 对列表的包裹
  • 每秒都强制渲染 <BrowseMembers> 组件,记录前 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: flexalign-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 计划,在肯定水平上也补救了开发体验的问题。以上~

退出移动版