天天看点

2023 年的尽头是编译时 CSS-in-JS 方案么?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

2023 年的尽头是编译时 CSS-in-JS 方案么?

最近,Emotion 排名第二的维护者 Sam 所在公司弃用了 CSS-in-JS 方案,引起了不小的讨论。这也是我第一次开始重点关注 CSS-in-JS,我甚至在头条开了一个合集重点讨论 CSS-in-JS 的方案,下面是已经发表的关于 CSS-in-JS 的文章:

  • 《 2023 年的尽头是编译时 CSS-in-JS 方案么?》
  • 《 CSS vs. CSS-in-JS:2023 年你应该如何选择?》
  • 《 2023 年最受欢迎的 10 大 CSS-in-JS 库!》
  • 《 我们为何选择弃用 css-in-js ? 》

我希望通过系列文章的方式带着大家深入的了解 CSS-in-JS,包括它的优势、缺点、编译时运行时的不同等等,最终让大家对写下的每一行代码都持有足够的信心。话不多说,直接开始进入正题!

1. 运行时 CSS-in-JS 的困境

以前单独写过介绍 CSS-in-JS 的文章,重点论述过 CSS-in-JS 方案的不足,概括起来可以通过以下几个点描述。

1.1 运行时开销和异常

CSS-in-JS 增加了运行时开销。 当组件渲染时,CSS-in-JS 库必须将样式“序列化”为可以插入到文档中的纯 CSS。很明显,这会占用额外的 CPU 周期,从而对应用程序的性能产生影响。

当然,更高效的方法是将样式移到组件外部,以便序列化在模块加载时发生一次,而不是在每次渲染时发生。比如:使用来自 @emotion/react 的 css 函数来做到这一点。

import { jsx, css, Global, ClassNames } from '@emotion/react';
const myCss = css({
  backgroundColor: 'blue',
  width: 100,
  height: 100,
});
function MyComponent() {
  return <div css={myCss} />;
}           

React 核心成员和 React Hooks 的原始设计师 Sebastian Markbåge 在 React 18 工作组中就 CSS-in-JS 展开了丰富的讨论。最终指出:在 React 并发渲染中,CSS-in-JS 导致在 React 渲染时针对所有 DOM 节点的每一帧需要重新计算所有 CSS 规则,最终显著拖慢渲染速度。

2023 年的尽头是编译时 CSS-in-JS 方案么?

总之,运行时 CSS-in-JS 库通过在组件渲染时插入新的样式规则来工作,这从根本上就不利于性能。同时,使用 CSS-in-JS 还有很多可能出错的地方,尤其是在使用 SSR 和/或组件库时。

1.2 额外包体积大小

CSS-in-JS 增加了包大小,每个访问网站的用户都必须下载 CSS-in-JS 库的 JavaScript。 Emotion 压缩和 Gzip (MINIFIED + GZIPPED)后为 7.9 kB,styled-components 为 12.7 kB。

2023 年的尽头是编译时 CSS-in-JS 方案么?

虽然这两个库体积都不是很大,但当所有外部依赖加在一起结果可能发生变化。 比如:react + react-dom 的体积是 44.5 kB 。

1.3 CSS-in-JS 导致 React DevTools 混乱

对于使用 css prop 的每个元素,Emotion 将渲染 和 组件。 如果在许多元素上使用 css prop,Emotion 的内部组件会使 React DevTools 变得混乱,如下所示:

2023 年的尽头是编译时 CSS-in-JS 方案么?

因为组件层级的混乱,对开发者调试带来极大的挑战,这也是 CSS-in-JS 一个比较严重的用户体验问题。

1.4 学习曲线与缓存

如果开发者从未使用过 Web components 或基于组件的框架会有一定的学习成本,这包括:语法、组件化等全新思路。

同时 CSS-in-JS 也无法使用 CSS 缓存,因为没有维护单独的 CSS 文件。

2.编译时 CSS-in-JS 的崛起

2023 年,前端领域看到越来越多的 CSS-in-JS 库在编译时将样式转换为纯 CSS。 典型的库包括:

  • Compiled
  • Vanilla Extract
  • Linaria
  • astroturf
  • style9

这些库旨在提供与运行时 CSS-in-JS 类似的好处,但是没有多余的性能成本。接下来带着大家一起看看这些编译时 CSS-in-JS 库的用法、特点等。

2.1 Compiled

Compiled 是一个由 Atlassian Labs 创建的 React 编译时 CSS-in-JS 库,旨在提供出色的开发人员体验而无需运行时成本。 Compiled 的工作原理是在构建时静态分析代码,将其转换为编译组件,然后在运行时将样式代码移动到文档的头部。

2023 年的尽头是编译时 CSS-in-JS 方案么?

图片来源:https://blog.logrocket.com/compiled-a-css-in-js-library-without-the-runtime-cost/

Compiled 利用了在 styled-components 和 Emotion 中发现的样式处理灵感,因此如果开发者使用过其中任何一个,那么对 Compiled 都很容易上手。目前 Compiled 在 Github 上有超过 1.9k 的 star,是一个值得关注的前端开源项目。

下面是的 Compiled 简单示例:

import { styled, ClassNames } from '@compiled/react';
// Tie styles to an element
<div css={{ color: 'purple' }} />;
// Create a component that ties styles to an element
const StyledButton = styled.button`
  color: ${(props) => props.color};
`;
// Use a component where styles are not necessarily tied to an element
<ClassNames>
  {({ css }) => children({ className: css({ fontSize: 12 }) })}
</ClassNames>;           

可以在 Webpack、Babel、Parcel 中使用 Compiled,但是需要预先安装它:

npm install @compiled/webpack-loader --save-dev
// webpack
npm install @compiled/parcel-config --save-dev
// parcel
npm install @compiled/babel-plugin --save-dev
// babel           

打开提取(extraction)开关,所有在应用程序中设置样式并通过 NPM 获取的组件都将剥离其运行时并将样式提取到原子样式表中。比如下面的示例:

-import { CC, CS } from '@compiled/react/runtime';
-
-const _2 = '._syaz1q9v{color: hotpink}';
-const _ = '._1wybfyhu{font-size: 48px}';
-
export const LargeHotPinkText = () => (
-  <CC>
-   <CS>{[_, _2]}</CS>
    <span className="_syaz1q9v _1wybfyhu">Hello world</span>
-  </CC>
);           

下面是抽离的原子样式内容:

._1wybfyhu {
  font-size: 48px;
}
._syaz1q9v {
  color: hotpink;
}           

2.2 Vanilla Extract

Vanilla Extract 是 TypeScript 中的零运行时样式表。其使用局部范围的类名和 CSS 变量在 TypeScript(或 JavaScript)中编写样式,然后在构建时生成静态 CSS 文件。

2023 年的尽头是编译时 CSS-in-JS 方案么?

图片来自:https://vanilla-extract.style/

总体上看,Vanilla Extract 是“TypeScript 中的 CSS 模块”,但具有作用域的 CSS 变量(scoped CSS Variables)和堆栈。其具有以下明显特征:

  • 在构建时生成的所有样式——就像 Sass、Less 等。
  • ✨ 对标准 CSS 的最小抽象。
  • 适用于任何前端框架,或者无框架场景
  • 局部范围的类名,就像 CSS 模块一样。
  • 局部范围的 CSS 变量、@keyframes 和 @font-face 规则。
  • 支持同步主题的高级主题系统,而且没有全局变量!
  • 用于生成基于变量的计算表达式的实用程序。
  • 通过 CSSType 的类型安全样式。
  • ‍♂️ 用于开发和测试的可选运行时版本。
  • 用于动态运行时主题的可选 API。

vanilla-extract 零运行时特性很有用,它允许开发者编写样式表并在构建时将它们编译成静态 CSS 文件。 还提供了类型安全和局部作用域样式的诸多好处。

// styles.css.ts
import { createTheme, style } from '@vanilla-extract/css';
export const [themeClass, vars] = createTheme({
  color: {
    brand: 'blue',
  },
  font: {
    body: 'arial',
  },
});
export const exampleStyle = style({
  backgroundColor: vars.color.brand,
  fontFamily: vars.font.body,
  color: 'white',
  padding: 10,
});           

vanilla-extract 还有一个突出特点是它对主题的支持。 开发者可以创建一个全局主题或多个主题,所有主题都具有类型安全的令牌合约,这使得自定义项目的外观变得容易。

Vanilla-extract 的另一个优势是它与框架无关,它具有流行框架和打包器(如 webpack、esbuild、Vite 和 Next.js)的官方集成。总的来说,Vanilla-extract 是一个很优秀的库,适合任何希望利用 TypeScript 的附加优势大规模编写可维护的 CSS 的人。

在 Github 上,vanilla-extract 已经有超过 8k 的 star 和 0.2k 的 fork,有超过 18.6 k 的项目使用 ,是妥妥的前端明星项目。

2.3 Linaria

Linaria 是一个典型的 CSS-in-JS 库,允许开发人员使用 JavaScript 编写 CSS,并在构建期间将样式提取到 CSS 文件中,还提供了动态样式和熟悉的 CSS 语法以及类似 Sass 的嵌套的诸多好处。

2023 年的尽头是编译时 CSS-in-JS 方案么?

除了作为一个零运行时库之外,Linaria 还通过 React 绑定支持基于属性的动态样式。 这利用了幕后的 CSS 变量,使得基于组件 props 的应用样式变得毫不费力。

Linaria 由两个主要部分组成:Babel 插件和打包器集成。 Babel 插件负责在代码中查找 css 和 style 标签,提取 CSS 并将其返回到文件的元数据中, 它还将根据文件名哈希生成唯一的类名。

import { css } from '@linaria/core';
import { modularScale, hiDPI } from 'polished';
import fonts from './fonts';
//在 `css` 标签中写下你的样式
const header = css`
  text-transform: uppercase;
  font-family: ${fonts.heading};
  font-size: ${modularScale(2)};

  ${hiDPI(1.5)} {
    font-size: ${modularScale(2.5)};
  }
`;
//然后用它作为类名

<h1 className={header}>Hello world</h1>;           

使用 styled 标签时,动态插值将替换为 CSS 自定义属性, 对作用域中常量的引用也将被内联。 如果多次使用相同的表达式,插件将为这些创建单个 CSS 自定义属性。

import { styled } from '@linaria/react';
import { families, sizes } from './fonts';
// Write your styles in `styled` tag
const Title = styled.h1`
  font-family: ${families.serif};
`;
const Container = styled.div`
  font-size: ${sizes.medium}px;
  color: ${(props) => props.color};
  border: 1px solid red;

  &:hover {
    border-color: blue;
  }
  ${Title} {
    margin-bottom: 24px;
  }
`;
// Then use the resulting component
<Container color="#333">
  <Title>Hello world</Title>
</Container>;           

在 Github 上,Linaria 已经有超过 10.4k 的 star 和 0.42k 的 fork,每周的平均下载量达到了 276k。目前有超过 1.5 k 的项目使用 Linaria、项目贡献人数 130+,是妥妥的前端明星项目。

2.4 astroturf

astroturf 允许开发者在 JavaScript 文件中编写 CSS,而无需添加任何运行时层,并使用现有的 CSS 处理管道。拆开来看,主要包括以下几个点:

  • astroturf 是零运行时 CSS-in-JS, 获得许多与 CSS-in-JS 相同的好处,但不会损失需要特定于框架的 CSS 处理的灵活性,同时保持 CSS 完全静态,无需运行时样式解析。
  • 使用现有的工具,如:Sass、PostCSS、Less 等,但仍然在 JavaScript 文件中编写样式定义
  • 单文件单组件, 在模板文字中编写 CSS,然后像在单独的文件中一样使用它

总之,利用 编译时的魔力,astroturf 让开发者可以轻松地从 JavaScript(或 TypeScript)文件中定义样式,而且框架可选。

import { stylesheet } from 'astroturf';
const height = 2;
const styles = stylesheet`
  .btn {
    appearance: none;
    height: ${height}rem;
    display: inline-block;
    padding: .5rem 1rem;
  }

  .primary {
    color: white:
    border: 1px solid white;
    background-color: taupe;

    &:hover {
      color: taupe:
      border-color: taupe;
      background-color: white;
    }
  }
`;
const Button = ({ primary }) => {
  const button = document.createElement('button');
  button.classList.add(styles.btn, primary && styles.primary);
  return button;
};           

通过 astroturf 专有的“提取过程”,每个样式表都变成了自己的 CSS 文件。 对于那些喜欢模块化方法的人来说,css 标签已经准备就绪,正在等待。 css 标签创建单个 CSS 类:

import React from 'react';
import { css } from 'astroturf';
const btn = css`
  color: black;
  border: 1px solid black;
  background-color: white;
`;
export default function Button({ children }) {
  return <button className={btn}>{children}</button>;
}           

处理后,css 块将被提取到 .css 文件中,利用配置为处理 css 的所有其他加载程序。同时,astroturf 为开发者提供了与 React.JS 的内置集成。

import * as React from 'react';
import { css } from 'astroturf';
function Button({ children, ...props }) {
  return (
    <button
      {...props}
      css={css`
        color: blue;
        border: 1px solid blue;
        padding: 0 1rem;
      `}
    >
      {children}
    </button>
  );
}           

className 的 props 会自动与提供的 css 结合,无需额外处理。

在 Github 上,astroturf 已经有超过 2.2k 的 star,超过 0.86 k 的项目使用 astroturf,是一个值得长期关注的编译时 CSS-in-JS 方案。

2.5 style9

style9 是受到了 Facebook 的 stylex 启发的 CSS-in-JS 编译器,具有接近零的运行开销、支持原子 CSS 提取和 TypeScript 。比如下面的代码示例:

import style9 from 'style9';
const styles = style9.create({
  blue: {
    color: 'blue',
  },
  red: {
    color: 'red',
  },
});
document.body.className = styles('blue', isRed && 'red');           

编译器将生成以下输出:

/* JavaScript */
document.body.className = isRed ? 'cRCRUH ' : 'hxxstI ';
/* CSS */
.hxxstI { color: blue }
.cRCRUH { color: red }           

要充分发挥 style9 的能力可以考虑与 Webpack 打包方案集成。以下是将样式提取到 CSS 文件所需的最低限度 Webpack 设置。

const Style9Plugin = require('style9/webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  // Collect all styles in a single file - required
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          type: 'css/mini-extract',
          // For webpack@4 remove type and uncomment the line below
          // test: /\.css$/,
          chunks: 'all',
          enforce: true,
        },
      },
    },
  },
  module: {
    rules: [
      {
        test: /\.(tsx|ts|js|mjs|jsx)$/,
        use: Style9Plugin.loader,
      },
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
    ],
  },
  plugins: [new Style9Plugin(), new MiniCssExtractPlugin()],
};           

style9 还支持诸多高级特性,比如:

  • 条件样式
  • 多个声明中组合样式
  • 伪选择器
  • 媒体查询
  • Keyframes
  • REM 中的字体大小
  • 共享样式
  • 自动前缀
  • 主题配置等

比如下面示例展示了如何使用 style9 的样式组合能力,通过将 style9 作为具有生成的样式对象的属性的函数来调用。这不受与使用样式功能相同的限制,并且可以是完全动态的。

import style9 from 'style9';
const someStyles = style9.create({
  blue: {
    color: 'blue',
  },
});
const someOtherStyles = style9.create({
  tilt: {
    transform: 'rotate(45deg)',
  },
});

document.body.className = style9(someStyles.blue, someOtherStyles['ti' + 'lt']);           

在 Github 上,相对于其他上述的编译时 CSS-in-JS 库,style9 虽然只有 0.6k 的 star,但是是一个值得长期关注的编译时 CSS-in-JS 方案。

3.宇宙的尽头是编译时 CSS-in-JS 么

虽然在实际项目中,我没有使用任何编译时 CSS-in-JS 库,但与 Sass 模块相比,我仍然认为它们有缺点。

以下是在查看 Compiled 库时看到的一些缺点:

  • 当组件第一次挂载时,样式仍然被插入,迫使浏览器重新计算每个 DOM 节点样式。
  • 动态样式无法在构建时提取,因此 Compiled 使用 style 属性(也称为内联样式)将值添加为 CSS 变量。 众所周知,当应用许多元素时,内联样式会导致性能不佳。
  • Compiled 仍然将样板组件插入到 React 树中, React DevTools 依然混乱,就像运行时 CSS-in-JS 一样。

与任何技术一样,编译时 CSS-in-JS 也有其优点和缺点。 作为开发人员,需要评估这些优缺点,然后就该技术是否适合用例做出明智的决定。 后面我也会单独出文介绍编译时 CSS-in-JS 的替代方案,欢迎大家持续关注。

参考资料

https://github.com/atlassian-labs/compiled

https://github.com/vanilla-extract-css/vanilla-extract

https://github.com/callstack/linaria

https://astroturfcss.github.io/astroturf/introduction/

https://github.com/astroturfcss/astroturf

https://dev.to/obetomuniz/build-time-css-in-js-explained-35ld

https://github.com/johanholmerin/style9

https://dev.to/srmagura/why-were-breaking-up-wiht-css-in-js-4g9b

https://medium.com/@dmitrynozhenko/9-ways-to-implement-css-in-react-js-ccea4d543aa3

封面图:来自 Dmitry Nozhenko 的《9 Ways To Implement CSS in React JS》