本文首发于微信公众号:大迁世界, 我的微信:qq449245884,我会第一工夫和你分享前端行业趋势,学习路径等等。
更多开源作品请看 GitHub https://github.com/qq449245884/xiaozhi,蕴含一线大厂面试残缺考点、材料以及我的系列文章。
快来收费体验 ChatGpt plus 版本的,咱们出的钱
体验地址:https://chat.waixingyun.cn
能够退出网站底部技术群,一起找 bug.
本文是由 Emotion 的第二大沉闷维护者 Sam 分享,本文第一人称都指的是 Sam。Emotion 是一个宽泛风行的 CSS-in-JS 库,用于 React。文文章 Sam 会带大家深入探讨 CSS-in-JS 最后吸引人的起因,以及为什么作者(以及 Spot 团队的其余成员)决定放弃它。
什么是 CSS-in-JS?
顾名思义,CSS-in-JS 就是在 JS 或 TS 中间接编写 CSS,为 React 组件提供款式,如下所示:
// Object Styles 形式
function ErrorMessage({children}) {
return (
<div
css={{
color: 'red',
fontWeight: 'bold',
}}
>
{children}
</div>
);
}
// String Styles 形式
const ErrorMessage = styled.div`
color: red;
font-weight: bold;
`;
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 编码模式及其对性能的影响之前,先来看看为什么有的开发者会应用 CSS-in-JS,有的不会应用。
益处
1. 部分作用域的款式。在写一般的 CSS 时,很容易不小心将款式利用到其它文件中。例如,假如咱们正在写一个列表,每一行都应该有一些 padding
和 border
。咱们可能会这样写:
.row {
padding: 0.5rem;
border: 1px solid #ddd;
}
几个月后,当咱们齐全遗记了这个列表时,又创立了一个列表。而后也设置了 className="row"
。当初,新组件的行有一个难看的边框,而咱们却不晓得为什么! 尽管这类问题能够通过应用较长的类名或更具体的选择器来解决,但作为开发者还是要确保没有类名抵触。
CSS-in-JS 齐全解决了这一问题,它使款式默认为本地作用域。如果把下面的款式写成这样:
<div css={{padding: '0.5rem', border: '1px solid #ddd'}}>...</div>
这样 padding
和 border
就不可能利用到其它元素了。
2. 托管。如果应用一般的 CSS,则能够将所有 .css
文件放在 src/styles
目录中,而所有的 React 组件都在 src/components
中。随着应用程序的大小的增长,很难判断每个组件应用哪些款式。很多时候,你的 CSS 中会呈现死代码,因为没有简略的办法能够说出这些款式没有应用。
一个更好的组织代码的办法是将所有与单个组件相干的货色放在同一个中央。这种做法被称为 colocation (托管)。
问题是,在应用一般的 CSS 时,很难实现 colocation,因为 CSS 和 JavaScript 必须放在独自的文件中,而且无论 .css
文件在哪里,你的款式都会全局利用。另一方面,如果应用 CSS-in-JS,能够间接在应用它们的 React 组件中编写款式 如果操作切当,这将极大地提高应用程序的可维护性。
3.能够在款式中应用 JavaScript 变量。CSS-in-JS 能够在款式规定中援用 JavaScript 变量,例如:
// colors.ts
export const colors = {
primary: '#0d6efd',
border: '#ddd',
/* ... */
};
// MyComponent.tsx
function MyComponent({fontSize}) {
return (
<p
css={{
color: colors.primary,
fontSize,
border: `1px solid ${colors.border}`,
}}
>
...
</p>
);
}
如本示例所示,能够在 CSS-in-JS 款式中同时应用 JS 常量(例如 colors
)和 React Props/state
(例如 fontSize
)。
在款式中应用 JS 常量的能力在某些状况下能够升高反复,因为同一个常量不须要同时定义为 CSS 变量和 JS 常量。
应用 props
和 state
的能力能够创立具备高度可定制的款式的组件,而无需应用内联款式。(当雷同的款式利用于许多元素时,内联款式的性能并不现实)。
中立
这是一项热门的新技术。许多 Web 开发者,包含我本人,个别会社区中最热门的新趋势。局部起因是这样的,因为在很多状况下,新的库和框架曾经被证实比它们的前辈有微小的改良(想想 React 比晚期的库如 jQuery 进步了多少生产力就晓得了)。
另一方面,咱们对新工具的痴迷是胆怯错过下一个大事件,在决定采纳一个新的库或框架时,咱们可能疏忽了真正的毛病。我认为这必定是 CSS-in-JS 被宽泛采纳的一个因素 – 至多对我来说是这样。
不好
1.CSS-in-JS 减少了运行时的开销。当组件渲染时,CSS-in-JS 库必须将款式 “ 序列化 ” 为能够插入到文档中的一般 CSS。很显著,这须要占用额定的 CPU 周期,但这是否足以对应用程序的性能产生显著的影响?咱们在下一节中深入研究这个问题。
2 CSS-in-JS 减少的包的大小。这是一个显著的问题 – 每个拜访你网站的用户都必须下载 CSS-in-JS 库的 JavaScript。Emotion 的最小压缩量是7.9 kB
,styled-components 是12.7 kB
。
3.CSS-in-JS 会打乱 React DevTools。对于每个应用 css prop 的元素,Emotion 会渲染 <EmotionCssPropInternal>
和<Insertion>
组件。如果你在许多元素上应用 css prop,Emotion 的外部组件会使 React DevTools 变得十分凌乱,如图所示。
丑
1.频繁插入 CSS 规定迫使浏览器做很多额定的工作。React 外围团队成员、React Hooks 的最后设计师 Sebastian Markbåge 在 React 18 工作组中写了一篇十分有见地的探讨,内容是对于 CSS-in-JS 库须要如何扭转能力与 React 18 一起工作,以及总体上对于运行时 CSS-in-JS 的将来。特地是,他说:
在并发渲染中,React 能够在渲染之间向浏览器退让。如果在一个组件中插入一个新的规定,如果 React 退让了,那么浏览器就必须看看这些规定是否实用于现有的树。所以它会从新计算款式规定。而后 React 渲染下一个组件,而后该组件发现了一个新的规定,再次发生。
援用
这无效地导致在 React 渲染时,每一帧都要针对所有 DOM 节点从新计算所有 CSS 规定。这是很慢的。
这个问题最蹩脚的中央在于,它不是一个可修复的问题(在运行时 CSS-in-JS 的上下文中)。运行时 CSS-in-JS 库通过在组件渲染时插入新的款式规定来工作,这在根本层面上不利于性能。
2. 对于 CSS-in-JS,可能出错的中央还有很多,尤其是在应用 SSR 或组件库的时候。在 Emotion 的 GitHub 仓库里,咱们收到了大量这样的问题。
我正在应用 Emotion 与服务器端渲染和 MUI/Mantine/(另一个 Emotion 驱动的组件库),它不能工作,因为 …
尽管每个问题的根本原因各不相同,但有一些独特的起因:
- Emotion 的多个实例被同时加载。即便多个实例都是同一版本的 Emotion,这也会导致问题。(issue)
- 组件库通常不能齐全管制插入款式的程序。(issue)
- Emotion 的 SSR 反对在 React 17 和 React 18 之间的工作形式不同。为了与 React 18 的流式 SSR 兼容,这是必要的。(issue)
这些复杂性只是冰山一角。
性能
运行时 CSS-in-JS 既有显著的长处也有显著的毛病。为了了解咱们的团队为什么要放弃这项技术,咱们须要摸索 CSS-in-JS 的理论性能影响。
本节重点介绍 Emotion 对性能的影响,因为它被用于 Spot 代码库。因而,如果认为下给出的性能数据也实用于你的代码库,那就错了 – 有很多办法能够应用 Emotion,而且每一种办法都有本人的性能特点。
渲染内的序列化与渲染外的序列化
款式序列化是指 Emotion 将 CSS 字符串或对象款式转换为能够插入文档的一般 CSS 字符串的过程。在序列化过程中,Emotion 也会计算出一个一般 CSS 的哈希值 – 这个哈希值就是你在生成的类名中看到的,例如css-15nl2r3
。
尽管我没有测量过这一点,但我置信影响 Emotion 如何执行的最重要因素之一是款式序列化是在 React 渲染循环外部还是内部执行的。
Emotion 文档中的例子是在 render
外面进行序列化的,像这样。
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 的次要卖点之一。
在 Spot,咱们在 render
中进行了款式序列化,所以上面的性能剖析将集中于这种状况。
对 Member Browser 进行基准测试
当初通过对 Spot 的一个真正的组件进行剖析来使事件具体化。咱们将应用 Member Browser,这是一个相当简略的列表视图,能够显示你的团队中的所有用户。
为了测试:
- Member Browser 显示 20 个用户
React.memo
四周的列表我的项目将被删除,并且强制最下面的<BrowseMembers>
组件每秒钟渲染一次,并记录前 10 次渲染的工夫。- React 严格模式是敞开的。(它能够效地让咱们在分析器中看到的渲染工夫翻倍)。
我应用 React DevTools 对该页面进行了剖析,前 10
次渲染工夫的平均值为54.3ms
。
我集体的教训是,一个 React 组件的渲染工夫应该在 16
毫秒以内,因为每秒 60 帧的 1 帧是 16.67
毫秒。Member Browser 目前是这个数字的 3 倍多,所以它是一个相当重量级的组件。
这个测试是在 M1 Max CPU 上进行的,它比普通用户的速度要快很多。我失去的 54.3
毫秒的渲染工夫在性能较差的机器上可能很容易达到 200
毫秒。
应用火焰图 (FlameGraph) 分析程序性能
上面是上述测试中单个列表项的火焰图:
正如你所看到的,有大量的 <Box>
和<Flex>
组件被渲染 – 这些是咱们的 “tyle primitives”,应用 css prop。尽管每 个 <Box>
只须要 0.1-0.2 毫秒的渲染工夫,但因为 <Box>
组件的总数十分大,所以这就减少了。
不应用 Emotion,对 Member Browser 进行测试
为了理解这种低廉的渲染有多少是由 Emotion
造成的,我应用 Sass Modules 而不是 Emotion 重写了 Member Browser 的款式。(Sass 模块在构建时被编译成一般的 CSS,所以应用它们简直没有性能损失)。
我反复了上述同样的测试,前 10 次渲染的均匀工夫为27.7ms
。这比原来的工夫缩小了 48%!
所以,这就是咱们与 CSS-in-JS 说拜拜的起因:运行时的性能老本切实是太高了。
反复我下面的免责申明:这个后果只间接实用于 Spot 代码库和咱们应用 Emotion 的形式。如果你的代码库以一种更无效的形式应用 Emotion(例如在 render 之外的款式序列化),你可能会看到从方程中移除 CSS-in-JS 后的更小益处。
上面是一些数据,供那些好奇的人参考:
咱们新的款式零碎
在咱们下定决心不再应用 CSS-in-JS 之后,一个新的问题就会呈现:咱们应该用什么来代替?现实状况下,咱们心愿款式零碎的性能与一般 CSS 相似,同时尽可能多地保留 CSS-in-JS 的长处:
- 部分作用域
- 款式与它们所利用的组件放在同个中央
- 能够在款式中应用 JS 变量
如果你认真看了那一节,你会记得我说过,CSS Module 还提供了部分作用域的款式和同位。而且,CSS Module 能够编译成一般的 CSS 文件,所以应用它们没有运行时的性能老本。
在我看来,CSS 模块的次要毛病是,说到底,它们依然是一般的 CSS– 而一般的 CSS 不足改善 DX 和缩小代码反复的性能。尽管嵌套选择器行将呈现在 CSS 中,但它们还没有呈现,而这个性能对咱们来说是一个微小开发品质的晋升。
侥幸的是,这个问题有一个简略的解决方案 –Sass 模块,它只是用 Sass 编写的 CSS 模块。你能够失去 CSS 模块的部分范畴的款式和 Sass 弱小的构建工夫性能,而且基本上没有运行工夫老本。这就是为什么 Sass 模块将成为咱们将来的通用款式解决方案。
实用类
对于从 Emotion 切换到 Sass Modules,团队的一个放心是,利用极其常见的款式,如display: flex
,会不太不便。以前,咱们会写。
<FlexH alignItems="center">...</FlexH>
为了只应用 Sass 模块做到这一点,咱们必须关上.module。SCSS 文件并创立一个利用款式 display: flex 和 align-items: center 的类。尽管不是世界末日,但的确不那么不便了。
如果只应用 Sass 模块,咱们不得在新建 .module.scss
文件,并创立一个类,利用款式 display: flex
和 align-items: center
。这并不是劫难,但必定不那么不便。
为了改良 DX,咱们决定引入一个实用类零碎。实用类就是是在元素上设置一个繁多的 CSS 属性的 CSS 类。通常状况下,联合多个实用类来取得所需的款式。对于下面的例子,能够这样写。
<div className="d-flex align-items-center">...</div>
Bootstrap 和 Tailwind 是提供实用程序类的最风行的 CSS 框架。这些库在其实用程序零碎中投入了大量的设计工作,所以采纳其中一个而不是推出咱们本人的实用程序是最有意义的。我曾经应用 Bootstrap 多年了,所以咱们抉择了 Bootstrap。尽管你能够把 Bootstrap 的实用类作为一个预建的 CSS 文件,但咱们须要定制这些类来适应咱们现有的款式零碎,所以我把 Bootstrap 源代码的相干局部复制到咱们的我的项目中。
咱们应用 Sass 模块和实用类的新组件曾经有几个星期了,对它相当称心。DX 与 Emotion 类似,而运行时的性能则大大优于 Emotion。
对于编译时 CSS-in-JS 的阐明
本文次要介绍运行时的 CSS-in-JS 库,如 Emotion 和 s tyled-components。最近,咱们看到越来越多的 CSS-in-JS 库在编译时将款式转换为一般 CSS。这些库包含:
- Compiled
- Vanilla Extract
- Linaria
这些库旨在提供相似于运行时 CSS-in-JS 的益处,而没有性能老本。
尽管我本人没有应用过任何编译时的 CSS-in-JS 库,但我依然认为它们与 Sass 模块相比有毛病。以下是我在察看 Compiled
时看到的毛病:
- 款式依然是在组件第一次挂载时插入的,这迫使浏览器在每个 DOM 节点上从新计算款式。(这个毛病曾经在 “ 丑 ” 一节中探讨过了)。
- 像本例中的 color prop 这样的动静款式不能在构建时提取,所以 Compiled 应用 style prop(又称内联款式)将该值增加为 CSS 变量。家喻户晓,当利用许多元素时,内联款式会导致次优的性能
- 该库依然将模板组件插入你的 React 树中,如图所示。这将使 React DevTools 变得凌乱,就像运行时的 CSS-in-JS 一样。
总结
任何技术一样,它有其长处和毛病。归根结底,作为一个开发者,你应该评估这些长处和毛病,而后就该技术是否适宜你的应用状况做出一个理智的决定。对于咱们 Spot 公司来说,Emotion 的运行时性能老本远远超过了 DX 的益处,特地是当你思考到 Sass 模块 + 实用类的代替计划依然有一个很好的 DX,同时提供微小的性能。
代码部署后可能存在的 BUG 没法实时晓得,预先为了解决这些 BUG,花了大量的工夫进行 log 调试,这边顺便给大家举荐一个好用的 BUG 监控工具 Fundebug。
交换
有幻想,有干货,微信搜寻 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。
本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。