大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
最近,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 规则,最终显著拖慢渲染速度。
总之,运行时 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。
虽然这两个库体积都不是很大,但当所有外部依赖加在一起结果可能发生变化。 比如:react + react-dom 的体积是 44.5 kB 。
1.3 CSS-in-JS 导致 React DevTools 混乱
对于使用 css prop 的每个元素,Emotion 将渲染 和 组件。 如果在许多元素上使用 css prop,Emotion 的内部组件会使 React DevTools 变得混乱,如下所示:
因为组件层级的混乱,对开发者调试带来极大的挑战,这也是 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 的工作原理是在构建时静态分析代码,将其转换为编译组件,然后在运行时将样式代码移动到文档的头部。
图片来源: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 文件。
图片来自: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 的嵌套的诸多好处。
除了作为一个零运行时库之外,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》