emotion 排名第二的维护者 Sam 所在公司弃用了 css-in-js 方案,引起了不小的讨论:Why We're Breaking Up with CSS-in-JS
概述 & 精读
原文很有有条理,先从 css-in-js 优点说起,再转而谈到缺点,说明了 css-in-js 这个新事物拥有明显的优点与缺点;然后从性能问题作为切入点,说明自己所在的公司为什么不得不抛弃 css-in-js;最后告诉读者目前自己的解决方案是 css-modules。
之后还有一点儿延展性思考,即目前还诞生了一批编译时 css-in-js 方案,但面对性能问题时依然徒劳。
让我们花点儿时间了解下作者的具体思路吧。
css-in-js 的优缺点
css-in-js 作为一个理念较新的开发思路,拥有如下几个明显的优缺点。
优点:
- 无全局样式冲突。就像 js 文件天然支持模块化的好处一样,原生 css 因为没有模块化能力,天然容易导致全局样式污染,如果不是特意用 BEM 方式命名,想要避免冲突就只能借助 css-in-js 了。(css-modules 也一样能做到)
- 与 js 代码合在一起。天然融合进 js 代码方便模块化管理,使 css 可以与某个局部模块绑定。(css-modules 也一样能做到,只是必须单独拆一个样式文件)
- 能将 js 变量应用到样式上。虽然 css 变量也能解决这个问题,但不如 css-in-js 那么直观,inline-style 也能解决这个问题,但会产生大量重复的局部样式,且这个优势 css-modules 做不到。
缺点:
- css-in-js 运行时解析的实现版本增加了运行时性能压力,尤其在 React18 调度机制模式下,存在无法解决的性能问题(运行时插入样式会导致 React 渲染暂停,浏览器解析一遍样式,渲染再继续,然后浏览器又解析一遍样式)。
- 增加了包体积。相比原生或者 css-modules 方案来说,增加了运行时框架代码 8kb 左右。
- 让 ReactDevTools 结构变得复杂,因为 css-in-js 会包裹额外的 React 组件层用来实现样式插入。
除了上述缺点外,css-in-js 还有三点深度使用后才能察觉的坑:
- 多个不同(甚至是相同)版本的 css-in-js 库同时加载时可能导致错误。笔者用 styled-components 就遇到了类似问题,甚至语法会产生不兼容的情况,虽然这些问题都可以被解决,但花费的额外时间需要计算一样,相比 css-in-js 得到的收益是否值得。
- 样式插入优先级无法自定义,这就导致产生样式覆盖时,业务对样式覆盖的优先级无法产生稳定的预期。class 优先级由 header 定义顺序决定,而非 className 的字符顺序决定,而 header 定义顺序又由资源加载与 css-in-js 插入执行时机决定,导致业务几乎不可能有稳定的样式覆盖顺序。这里产生的问题就是业务代码不断增多的
!impprtant
定义。 - 不同 React 版本的 SSR,css-in-js 需要适配不同的实现,这对框架作者不太友好。
除了性能问题以外,其他问题都可以忍,但偏偏在性能问题上,css-in-js 遇到了无解的场景。
无解的性能问题
第一条缺点提到的运行时解析,是 css-in-js 方案永远跨不过去的困境,即便对于编译时 css-in-js 方案来说,也免不了在渲染时做额外的逻辑执行拖慢渲染速度:
代码语言:javascript复制function App() {
return <div css={{ color: "red" }} />; // 就是这种代码导致了性能问题
}
原因是当 React 重渲染组件时,需要重新解析样式定义,并序列化 className,当渲染非常频繁时会导致明显的性能瓶颈,而解决方法是把样式定义抽出来,但这样就损失了第三个优点,即无法读取 js 变量了:
代码语言:javascript复制const myCss = css({
backgroundColor: "blue",
width: 100,
height: 100,
});
不得不的说 React 的渲染机制实在是太有问题了,如果换成 SolidJS 这个问题就好办了,因为运行时的样式代码仅会运行一次,组件重渲染也不会导致这段解析代码被重复执行,此时 css-in-js 在样式变化时再做一次精确样式更新,性能问题就可以被解决了。
换成 css modules
css-modules 同时支持优点一和二,而优点三可以通过一些特定语法糖绕过:通过 :import
:export
伪类做 css 变量的导入导出,用 webpack-loader 实现 js 中引用 css 变量,用 css variable 实现 css 引用 js 变量。
所以当性能问题是绕不过去的话题,而 css-modules 在性能最优的情况下,有一些曲线方案可以同时支持 css-in-js 的优点,也就能理解为什么作者要弃用 css-in-js 了。
包体积真的变大了吗
原文谈到的 css-in-js 增加了 8~16kb 其实是在强行堆缺点了,除非你的项目只有一行 css 定义。如果我们只考虑传输时的包体积与 HTML 中样式定义数量,而忽略运行时产生的性能负担,那么 css-in-js 在大型项目无疑是最优的。
原因就是 css-in-js 样式是按需插入的,没有渲染的组件就不会插入样式。甚至渲染了的组件也不一定会插入样式,因为 css-in-js 可以对包含相同样式定义的场景做 className 合并,类似于 webpack 打包时,可以把不同模块公共代码抽到一个 chunk 里。
编译时 css-in-js 方案是出路吗
理论上是出路,但限制了 css-in-js 的灵活性。从 vanilla-extract 等编译时 css-in-js 框架来看,确实解决了运行时 css-in-js 性能问题,但带来了更多语法限制,比如必须预先定义样式再使用:
代码语言:javascript复制import { style } from '@vanilla-extract/css'
const myStyle = style({
display: 'flex',
paddingTop: '3px'
})
const App = () => <div className={myStyle}/>
编译时 css-in-js 想要做到通用性,只能提供一个 className,这样就不受任何框架和环境的限制了,但这样也限制了声明语法的灵活性,显然不可以用内连方式定义样式。
而且这种编译时的方案本质上和 css-modules 是一样的,背后都是定义了一些静态样式名,只是说这些样式问题以 .sass
定义还是 .ts
定义,如果用 .ts
定义,配合编译工具可以使代码原生 import 的更加舒服。
所以使用了编译时 css-in-js 方案,本质上还是抛弃了运行时 css-in-js,投向了变种的 css-modules 阵营。
总结
css-in-js 本身方向是对的,即把 css 与 js 融合,但太过灵活的运行时 css-in-js 方案遇到了几乎不可解的性能问题,编译时的 css-in-js 方案可能是更好的出路。
css-in-js 这个名字本身就表示它拥有 in js 的灵活性,而编译时 css-in-js 方案本质因为是 css-module,所以不可避免拥有一些比较奇怪的限制,如果 js 里的代码不能像真的 js 一样灵活,可能还不如回到 .scss
或者 .less
的后缀更好理解一些。
讨论地址是:精读《我们为何弃用 css-in-js》· Issue #450 · dt-fe/weekly
如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)