插入css-再见,CSS-in-JS

2023-08-20 0 5,861 百度已收录

大家好,我是小智。 在过去的开发中,我仍然使用 styled-component 库作为 CSS 解决方案。 它有很多优点,比如灵活,复用性强,功能强大,可以接受动态JS变量传入组件等等……但是我昨天看到一篇文章,说Spot团队的人已经决定放弃了CSS-in-JS 的解决方案,因为对于他们来说,性能损失已经远远超过了灵活性的优势。 拿起它,让我和你分享这个WhyWereBreakingUpwithCSS-in-JS[1]

大家好,我是 Sam,来自 Spot[2] 的软件工程师,也是广泛使用的 ReactCSS-in-JS 库 Emotion 的第二位活跃维护者。 本文将深入探讨最初吸引我使用 CSS-in-JS 的原因,以及为什么我(以及 Spot 团队的其他成员)决定放弃它。

我们将简要概述 CSS-in-JS 以及它应该关注的内容。 之后我们会深入分析Spot中CSS-in-JS带来的性能问题,以及如何预防此类问题。

什么是 CSS-in-JS

顾名思义,CSS-in-JS 允许您通过在 JavaScript 或 TypeScript 代码中编写 CSS 来设计 React 组件样式

// @emotion/react (css prop), 使用对象样式
function ErrorMessage({ children }{
  return (
    <div
      css={{
        color: "red",
        fontWeight: "bold",
      }}
    >

      {children}
    </div>

  );
}

// styled-components 或 @emotion/styled, 使用字符串样式
const ErrorMessage = styled.div`
  color: red;
  font-weight: bold;
`
;

在 React 社区中,最流行的 CSS-in-JS 库是 styled-components[3] 和 Emotion[4]。 实际上我只使用过 Emotion,但我认为本文中的几乎所有要点都适用于样式组件。

本文重点介绍运行时 CSS-in-JS,该类别包括样式组件和 Emotion。 运行时 CSS-in-JS 意味着库在运行时解释和应用样式。 本文最后简要讨论了编译时 CSS-in-JS。

CSS-in-JS 的优点和缺点

在我们深入研究特定的 CSS-in-JS 编码模式及其对性能的影响之前,让我们描述并概述使用此技术的原因以及可能的缺点。

优势

本地范围的样式。 使用 PureCSS 时,很容易使样式应用过于笼统。 例如,如果你想为列表视图的每一行设置内边距和边框,你可以这样编写 CSS:

.row {
  padding0.5rem;
  border1px solid #ddd;
}

几个月后,您完全忘记了列表视图插入css,然后在另一个组件中创建 row 元素并设置 className="row" 。 如今,一排排新组件都有意想不到的边框,但您不知道为什么! 看起来这个问题可以通过更长的类名或者更具体的选择器来解决,但是作为开发人员你仍然需要确保不存在类名冲突。

CSS-in-JS 完全解决了这个问题,样式默认是本地作用域的。 如果您像这样组成列表视图:

...</div>

这使得边框和填充不可能意外地应用于不相关的元素。

注意:CSSModules 还提供本地范围样式。

Colocation:如果使用 PureCSS,则所有 .css 文件可能会放置在 src/styles 目录中,而 React 组件则放置在 src/components 目录中。 随着应用程序规模缩小,可能很难知道每个组件使用什么样式。 由于没有简单的方法来确定样式是否正在使用,因此 CSS 中经常会留下未使用的代码。

组织代码的更好方法是将相关的组件代码放在一起。 这些做法称为共置(Colocation),KentC.Dodds 的这篇博文 [5] 已经进行了讨论。

问题是 PureCSS 很难实现奇偶校验,因为 CSS 和 JavaScript 必须位于不同的文件中,并且无论 .css 文件位于何处,样式都会全局应用。 不同的是,CSS-in-JS 可以用来在使用样式的 React 组件中直接编译样式代码! 如果使用得好,将会大大提高应用程序的可维护性。

注意:CSSModule 还允许样式与组件位于同一位置,但不能位于同一文件中。

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>

  );
}

如示例所示,JavaScript 常量(如颜色)和 React props/state(如 fontSize )可以在 CSS-in-JS 样式中使用。 在某些情况下,能够在样式中使用 JavaScript 常量可以减少代码重复,因为同一个常量不需要在 CSS 变量和 JavaScript 常量中定义一次。 使用 props 和 state 的能力允许您创建具有高度可定制样式的组件,而无需使用内联样式。 (当相同的样式应用于许多元素时,内联样式会降低性能。)

中性方面

插入css-再见,CSS-in-JS

这是热门的新技术。 许多 Web 开发人员,包括我自己,都渴望采用 JavaScript 社区的最新趋势。 这在一定程度上是有道理的,因为在许多情况下,新的库和框架已经证明它们比它们的前辈(早期的库和框架,如 jQuery)有显着的改进。 另一方面,我们痴迷于闪亮的新工具的另一个触发因素是痴迷本身。 我们害怕错过下一个流行趋势,并且在决定采用新的库或框架时可能会忽视真正的缺点。 我觉得这确实是广泛采用 CSS-in-JS 的一个触发因素——至少对我来说是这样。

坏处

CSS-in-JS 减少了运行时开销。 当组件呈现时,CSS-in-JS 库必须将样式“序列化”为可插入到文档中的 PureCSS。 事实上,这需要额外的CPU消耗,但这会对应用程序性能产生重大影响吗? 我们将在下一节中深入探讨这个问题。

CSS-in-JS 减少了包体积。 这是显而易见的 - 每个访问您网站的用户现在都需要下载 CSS-in-JS 库的 JavaScript。 压缩后Emotion为7.9kB[6],styled-components为12.7kB[7]。 所以这两个库不大,但是添加起来还是有影响的。 (相比之下,react+react-dom 为 44.5kB)。

CSS-in-JS 破坏了 React DevTools。 对于使用 cssprop 的每个元素,Emotion 将进行渲染和组件。 如果您在许多元素上使用 cssprop,Emotion 的内部组件可能会使 React DevTools 变得混乱,如图所示:

混乱

坏处

频繁插入CSSRules会导致浏览器做很多额外的工作。 React 核心团队成员兼 Hook 设计师 Sebastian Markbåge 在 React18 工作组 [8] 的这次特别有价值的讨论中表示:

在并发渲染中,React 可以在渲染之间向浏览器产生线程。 如果你在组件中插入新的 CSS,在 React 放弃线程后,浏览器必须检查该 CSS 是否适用于现有的树。 所以它重新评估了样式规则。 然后 React 渲染下一个组件,然后该组件找到新的 CSS,这个过程再次发生。

这会导致 React 渲染时 DOM 节点每帧重新评估 CSS 规则,这是非常昂贵的

这句话特指React并发模式下的性能,不使用useInsertionEffect。 为了深入了解,我建议查看完整的讨论。 感谢 Dan Abramov 在 Twitter 上强调了这一不准确之处[9]。

这个问题是无法解决的,并且在运行时 CSS-in-JS 环境中很难修复。 运行时 CSS-in-JS 库的工作方式是在组件渲染时插入新的样式规则,这从根本上与性能相反。

使用CSS-in-JS,更容易出错,尤其是在使用SSR和组件库时。 在 Emotion 的 GitHub 存储库中,我们收到了很多这样的问题:

我正在使用启用了服务器端渲染的 Emotion 和 MUI/Mantine/(另一个基于 Emotion 的组件库),因为……出现了问题。

具体原因因问题而异,但有一些共同点:

相信我,这种复杂性只是冰山一角。 (如果您足够勇敢,请查看“@emotion/styled”的 TypeScript 定义[13]。)

性能深度分析

到目前为止,很明显 CSS-in-JS 既有重要的优点,也有重要的缺点。 为了理解为什么我们决定放弃这项技术,我们需要探索 CSS-in-JS 的实际性能影响。

本节重点介绍 Spot 代码库中 Emotion 的性能影响。 因此,下面给出的性能数据可能不一定适用于您的代码库 - 使用 Emotion 的方式有很多种,每种方式都有自己的性能特征。

渲染内序列化与渲染外序列化

样式序列化是 Emotion 将 CSS 字符串或对象样式转换为可插入文档中的 PureCSS 字符串的过程。 Emotion 还会在序列化期间计算 CSS 哈希值 - 该哈希值就是您在生成的类名称中看到的内容,例如 css-15nl2r3。

事实上,我没有测量过,但我认为影响 Emotion 性能的最重要因素之一是样式序列化是在 React 渲染周期内部还是外部执行。

Emotion文档中的示例是在渲染内序列化,例如:

function MyComponent({
  return (
    <div
      css={{
        backgroundColor: "blue",
        width: 100,
        height: 100,
      }}
    />

  );
}

每次 MyComponent 渲染时,对象样式都会重新序列化。 如果 MyComponent 频繁渲染(例如每次击键),重复序列化可能会产生很高的性能成本。

更有效的方法是将样式移到组件之外,以便序列化仅在加载模块时完成一次,而不是每次渲染时完成。 这可以使用 @emotion/react 中的 css 函数来实现:

const myCss = css({
  backgroundColor"blue",
  width100,
  height100,
});

function MyComponent({
  return <div css={myCss} />;
}

事实上,这会阻止您访问样式中的 props,因此您失去了 CSS-in-JS 的主要卖点之一。

在Spot中,我们在渲染时进行序列化,因此下面的性能分析将重点关注这些情况。

查看成员列表组件

是时候通过剖析 Spot 的真实组件来使问题具体化了。 让我们使用成员列表组件作为示例,这是一个相当简单的列表视图,显示团队中的所有用户。 几乎所有成员列表样式都使用 Emotion,尤其是 cssprop。

Spot 中的成员列表组件

测试:

我使用 React Developer Tools 对其进行了分析,前 10 次渲染的平均时间为 54.3 微秒。

我的经验是插入css,React 组件应该在 16 纳秒或更短的时间内渲染,因为在每秒 60 帧的情况下,每帧为 16.67 微秒。 当前的成员列表组件的渲染量是这个数字的3倍多,因此它是一个非常“昂贵”的组件。

而且这个测试是在M1MaxCPU上进行的,它比普通用户的设备要快得多。 在较弱的机器上,54.3 微秒的渲染时间很容易达到 200 微秒。

剖析火焰图

下面是上述测试中单个列表项的火焰图:

成员列表组件项目组件的性能火焰图

正如你所看到的,有很多渲染和组件 - 这些是我们使用 cssprop 的“样式子句”。 每一位只需要0.1-0.2微秒的渲染时间,但由于组件总量巨大,加起来会是一笔不小的开支。

没有情感的评论成员列表组件

为了不归咎于 Emotion,我使用 Sass 模块重新设计了成员列表组件。 (Sass 模块编译为 PureCSS,几乎没有性能损失。)

我重复了同样的测试,前 10 次渲染的平均时间为 27.7 微秒,增加了 48%!

所以,这就是我们决定“分手”CSS-in-JS 的原因:运行时性能成本太高了。

重复我之前的免责声明:此结果仅直接适用于 Spot 代码库和我们使用 Emotion 的方式。 如果您的代码库以更有效的方式使用 Emotion(例如渲染序列化外的样式),那么放弃 CSS-in-JS 的好处可能会小得多。

如果您有兴趣,这是原始数据:

表格显示Emotion和非Emotion成员列表组件的渲染时间

我们的新风格系统

决定放弃 CSS-in-JS 后,显而易见的问题是:我们应该使用什么? 理想情况下,我们希望样式系统的性能接近 PureCSS,同时尽可能多地保留 CSS-in-JS 的优点。 我在“优点”部分提到的 CSS-in-JS 的主要用途是:

样式是局部范围的

样式和组件位于同一位置

JavaScript 变量可以在样式中使用

如果您细心的话,您会记得我说过 CSSModule 还提供本地范围样式和奇偶校验。 CSSModules 编译为 PureCSS 文件,使用它们不会带来运行时性能损失。

在我看来,CSSModules 的主要缺点是它实际上是 PureCSS,而 PureCSS 缺乏一些可以改善开发体验和减少代码重复的功能。 尽管嵌套选择器[14]被正式引入,但它们仍在下降。 这个功能对我们来说是一个质的提升。

幸运的是,SassModules 是这个问题的一个简单解决方案——用 Sass 编写的 CSSModules [15]。 您可以获得 CSSModules 的本地范围样式和 Sass 强大的构建时功能,而几乎没有运行时成本。 SassModules 将是我们的通用样式解决方案。

注意:使用 SassModules,您将失去 CSS-in-JS 的第三个好处(在样式中使用 JavaScript 变量)。 但是,您可以在 Sass 文件中使用 :export 将 Sass 代码中的常量公开给 JavaScript。 它不太方便,但它保持了 DRY 原则。

公用事业

该团队担心从 Emotion 切换到 SassModules 会导致应用非常常见的样式(例如 display:flex)变得不方便。 曾几何时,我们会这样写:

<FlexH alignItems="center">...</FlexH>

要使用SassModules,您需要打开.module.scss 文件并创建一个使用display:flex 和align-items:center 样式的类。 这是不可避免的,但肯定很麻烦。

为了改进这一点,我们决定引入实用程序类系统。 实用程序类是设置单个 CSS 属性的类。 通常,您可以组合多个实用程序类来获得所需的样式。 对于前面的反例,你可以这样写:

<div className="d-flex align-items-center">...</div>

Bootstrap[16] 和 Tailwind[17] 是提供实用程序类的最流行的 CSS 框架。 这些库在实用系统的设计上投入了大量精力,因此直接使用它们比自己实现它们更有意义。 我已经使用 Bootstrap 多年了,所以我们选择了 Bootstrap。 我们需要自定义这个类来适应现有的样式系统,所以我将Bootstrap源码的相关部分复制到了项目中。

我们已经在新组件中使用 SassModules 和实用程序类几个星期了,并且对此非常满意。 开发体验接近Emotion,但运行时性能却比它好很多。

注意:我们还使用 typed-scss-modules[18] 包为 SassModule 生成 TypeScript 定义。 最有用的功能之一是它允许我们定义类似于 classnames[19] 的 utils() 辅助函数,不同之处在于它只接受有效的实用程序类名称作为参数。

关于编译时 CSS-in-JS 的说明

本文重点介绍运行时 CSS-in-JS 库,例如 Emotion 和 styled-components。 最近,我们看到越来越多的 CSS-in-JS 库在编译时将样式转换为 PureCSS,包括:

此类库声称提供运行时类似 CSS-in-JS 的优势,且不会造成性能损失。

虽然我自己没有使用过任何编译时 CSS-in-JS 库,但我觉得它们与 SassModule 相比仍然有缺点。 我在查看 Compiled 时注意到的缺点包括:

总结

感谢您阅读这篇关于运行时 CSS-in-JS 的深入分析。 任何技术都有其优点和缺点。 最终,作为开发者,你需要评估这些异同,确定该技术是否适合你的使用场景,然后做出决定。 对于我们在Spot中的开发来说,Emotion的运行时性能成本远远超过了开发体验的提升,特别是考虑到更换SassModules+实用类仍然拥有良好的开发体验,并且提供远远超过Emotion的性能。

插入css-再见,CSS-in-JS

欢迎长按图片加碗智慧为好友,定期分享VueReactTs。

终于:

参考

[1]

为什么我们要放弃 CSS-in-JS:

[2]

点:

[3]

样式组件:

[4]

情感:

[5]

这篇博文:

[6]

7.9kB:@情感/反应@11.10.4

[7]

12.7kB:@5.3.6

[8]

这个特别有价值的讨论:

[9]

强调:

[10]

问题示例:

[11]

问题示例:

[12]

问题示例:

[13]

@emotion/styled 的 TypeScript 定义:

[14]

嵌套选择器:

[15]

萨斯:

[16]

引导程序:

[17]

顺风:

[18]

类型化的 scss 模块:

[19]

类名:

[20]

编译:

[21]

香草精:

[22]

林纳里亚:

[23]

这个反例:#speed-up-your-styles

[24]

在这里:#speed-up-your-styles

标记模板是模板文字的更中间版本css模板,允许您使用函数解析模板文字。 标签函数的第一个参数包含一个字符串字段,其余参数是表达式。 您可以使用标记函数对这些参数执行任何操作并返回操作后的字符串(或者返回完全不同的内容,请参阅下面的示例)。 用作标签的函数名称没有限制。

js

const person = "Mike";
const age = 28;
function myTag(strings, personExp, ageExp) {
  const str0 = strings[0]; // "That "
  const str1 = strings[1]; // " is a "
  const str2 = strings[2]; // "."
  const ageStr = ageExp > 99 ? "centenarian" : "youngster";
  // 我们甚至可以返回使用模板字面量构建的字符串
  return `${str0}${personExp}${str1}${ageStr}${str2}`;
}
const output = myTag`That ${person} is a ${age}.`;
console.log(output);
// That Mike is a youngster.

标签不必是普通标识符,您可以使用小于 16 个的任何表达式,包括 、函数调用、 ,甚至其他带标签的模板文字。

js

console.log`Hello`; // [ 'Hello' ]
console.log.bind(1, 2)`Hello`; // 2 [ 'Hello' ]
new Function("console.log(arguments)")`Hello`; // [Arguments] { '0': [ 'Hello' ] }
function recursive(strings, ...values) {
  console.log(strings, values);
  return recursive;
}
recursive`Hello``World`;
// [ 'Hello' ] []
// [ 'World' ] []

虽然语法在技术上允许这样做,但未标记的模板文字是字符串,并且在链接时会抛出异常。

js

console.log(`Hello``World`); // TypeError: "Hello" is not a function

唯一的例外是可选链,它会抛出语法错误。

js

console.log?.`Hello`; // SyntaxError: Invalid tagged template on optional chain
console?.log`Hello`; // SyntaxError: Invalid tagged template on optional chain

请注意css模板,这两个表达式仍然是可解析的。 这意味着它们不会受到 的影响,它只会插入分号来修补未解析的代码。

js

// 仍是语法错误
const a = console?.log
`Hello`

标签函数甚至不需要返回字符串!

js

function template(strings, ...keys) {
  return (...values) => {
    const dict = values[values.length - 1] || {};
    const result = [strings[0]];
    keys.forEach((key, i) => {
      const value = Number.isInteger(key) ? values[key] : dict[key];
      result.push(value, strings[i + 1]);
    });
    return result.join("");
  };
}
const t1Closure = template`${0}${1}${0}!`;
// const t1Closure = template(["","","","!"],0,1,0);
t1Closure("Y", "A"); // "YAY!"
const t2Closure = template`${0} ${"foo"}!`;
// const t2Closure = template([""," ","!"],0,"foo");
t2Closure("Hello", { foo: "World" }); // "Hello World!"
const t3Closure = template`I'm ${"name"}. I'm almost ${"age"} years old.`;
// const t3Closure = template(["I'm ", ". I'm almost ", " years old."], "name", "age");
t3Closure("foo", { name: "MDN", age: 30 }); // "I'm MDN. I'm almost 30 years old."
t3Closure({ name: "MDN", age: 30 }); // "I'm MDN. I'm almost 30 years old."

标签函数接收的第一个参数是字符串字段。 对于任何模板文字,其宽度等于替换次数(${…} 的出现次数)加一,因此它始终为非空。 对于任何特定的标记模板文字表达式,无论该文字被计算多少次,标记函数都将始终使用完全相同的文字字段进行调用。

js

const callHistory = [];
function tag(strings, ...values) {
  callHistory.push(strings);
  // Return a freshly made object
  return {};
}
function evaluateLiteral() {
  return tag`Hello, ${"world"}!`;
}
console.log(evaluateLiteral() === evaluateLiteral()); // false; each time `tag` is called, it returns a new object
console.log(callHistory[0] === callHistory[1]); // true; all evaluations of the same tagged literal would pass in the same strings array

这允许标记函数将其第一个参数作为标志来缓存结果。 为了进一步确保字段值不更改,第一个参数及其值都将被忽略,因此您将无法更改它们。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 css 插入css-再见,CSS-in-JS https://www.wkzy.net/game/127072.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务