javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JS Tailwind CSS

JS生态系统加速Tailwind CSS工作原理探究

作者:大家的林语冰 人猫神话

这篇文章主要为大家介绍了JS 生态系统加速Tailwind CSS使用及工作原理探究,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

引言

长话短说:自破蛋以来,Tailwind CSS 已成为一种人气爆棚的 Web 项目样式方案。这次我们来瞄一下为其提供支持的架构,以及可以优化的方案。

本期《前端翻译计划》共享的是“加速 JS 生态系统系列博客”,包括但不限于:

Tailwind CSS

本期共享的是第 8 篇博客 —— Tailwind CSS 方案。

诚然,我目前手头没有诉诸 Tailwind CSS 编写的大型项目。我那些使用 Tailwind 的项目太小,由此得出的性能分析不具备统计学意义。所以我有一个大胆的想法:用 Tailwind 自己的 tailwindcss.com 官网介绍 Tailwind 简直绝绝子!不过在下出师未捷身先死:Tailwind 官网诉诸 Next.js 构建,要获得有意义的调试比脱单还难。更重要的是,这些调试掺杂了一大坨与 TailwindCSS 毫无关系的干扰。

退而求其次,我决定使用完全相同的配置在项目上运行 Tailwind CLI,从而获取某些性能追踪。运行 CLI 构建总共需要 3.2 秒,而 Tailwind 在运行时花费了 1.4 秒。如下所示,我们可以找出某些时间开销的性能重灾区:

这里火焰图的 x 轴不表示“发生时”的时间,而表示此处合并在一起的每个调用堆栈的累积时间。性能重灾区一目了然。我正在使用 SpeedScope 来可视化 CPU 配置文件。

有一个处理提取潜在的解析候选的区块,一个配置和插件初始化的区块,CSS 生成,某些 PostCSS 的东东,当有 PostCSS 时,通常同时提及 autoprefixer,因为两者经常梦幻联动。粉丝请注意,在不执行任何操作的情况下加载 autoprefixer 似乎已经消耗了一大坨时间。

转换思路

瞄一下 Tailwind CSS 代码库,查看配置文件,肯定存在某些函数可以继续优化的地方。但如果我们这样做,我们能且仅能斩获几个个位数的百分比优化。

实现多因素加速、而不仅仅是低百分比提速的秘诀,不在于应用通用规则或习惯,比如“不要在 for 循环里创建闭包”。这是一个常见的误解,我们认为如果遵循所有这些“最佳实践”,代码就会变快,因为在大多数情况下(并非全部),令人不安的事实是,这绝非关键优化。使代码变快的原因是,充分理解代码的作用,然后采取最短路径实现该目标。

因此,作为一个挑战,私以为如果我们兼顾性能从零构建,那么看看 Tailwind 代码的架构会很有趣。我们会做出不同的决定吗?但为了找到最佳架构,我们需要知道 Tailwind 解决的是哪个问题,并考虑实现该目标的最短路径。

Tailwind CSS 工作原理

从本质上讲,Tailwind CSS 的工作机制是,我们向它传递某些 CSS 文件,然后它在其中查找 @tailwind 规则。如果它邂逅匹配的规则,那么它会爬取项目中的其他文件,查找 tailwind 类名,并将其注入到找到该 @tailwind 规则的 CSS 文件中。它还有其他方方面面,但为了简单起见,我们暂且无视其他规则。

/* 输入 */
@tailwind base;
@tailwind components;
@tailwind utilities;

.foo {
  color: red;
}

这会被转化为:

.border {
  border-width: 1px;
}
.border-2 {
  border-width: 2px;
}

/* 等等...... */
.foo {
  color: red;
}

基于此机制,我们可以确定 Tailwind CSS 内部流程的若干阶段:

优化提取阶段

由于有且仅有三个有效的 @tailwind 规则值,我们可以使用一个基本的正则,绕过整个 PostCSS 解析步骤:

;/@tailwind\s+(base|components|utilities)(?:;|$)/gm

虽然但是,一旦读取了那些文件,且我们需要提取潜在的 tailwind 类名候选,我们就有优化空间。但有一个问题:我们如何判断候选是否为 tailwind 类名?这表面上易如反掌,但实际上比脱单还难。问题在于,没有作者或任何其他证据表明,字符序列乃有效的 tailwind 类名。可能存在与 tailwind 类名具有相同格式、但不存在的单词组合。

举个栗子,有效的 tailwind 类名如下所示:

ml-2

border-b-green-500

dark:text-slate-100

dark:text-slate-100/50

[&:not(:focus-visible)]:focus:outline-none

那么 foo-bar 是有效的 tailwind 类名吗?它并非 tailwind 默认语法的一部分,但它可以由用户添加。因此,我们在这里有且仅有的真正选择是,尽量减少搜索空间,然后向解析器“投喂”剩余的候选。如果解析器生成了某些 CSS,那么我们就知道类名有效。反之无效。这反过来意味着,我们需要优化解析器,在检测到没有定义的字符串值时,尽快退出。

粉丝请注意:目前在 Tailwind CSS 中,这大约需要 388ms

我在本地给 Tailwind CSS 打补丁,显示了某些有关提取器的提取值的统计数据。

但更有趣的是,瞄一下提取程序提取最常见的前 10 个值:

- 9774x ''
- 2634x </div>
- 1858x }
- 1692x ```
- 1065x },
- 820x ---
- 694x ```html
- 385x {
- 363x >
- 345x </p>

换而言之,在 26_466 个匹配的字符串中,其中 19_630 个显然是无效的 tailwind 类名。平心而论,Tailwind CSS 存在某些缓存,可以减轻检查某些东东是否存在“假阳性”。并且已经有一个代码注释道,对其正则的任何优化,都能将 Tailwind CSS 提速高达 30%。

万物皆可正则

这里使用正则的“双刃剑"是,它不具有语言感知能力。它不知道我们是在 .js 还是 .html 文件上操作,更糟糕的是,该语言还可以互相嵌入。.html 文件可以同时托管 HTML、JS 和 CSS。.jsx 文件中的 JSX 同理可得。当涉及 JS 代码时,我们可以假设我们只需查看字符串。

经过简单粗暴的正则处理后,我们将搜索空间从 26_466 减少到 9_633 个候选。这仍不是极致优化,但比我们开始时要更胜一筹。现在,一大坨提取字符串类似于更多潜在的 tailwind 候选字符串:

relative not-prose [a:not(:first-child)>&]:mt-12

none

break-after

grid-template-rows

...

每个提取字符串都包含一个或多个潜在候选。我们可以通过在每个提取字符串上触发另一个正则,继续减少搜索空间,提取可能是有效 tailwind 类名的部分。对我们而言幸运的是,有效的 tailwind 类名的语法遵循相当简单的规则:

所有这些匹配客观存在某些时间开销,并将总提取时间增加到 92ms。在努力减少搜索空间后,我们仍剩下大约 8_000 个潜在的 tailwind 类名(粉丝请记住,之前提取的字符串可以包含多个候选)。

目前为止,我们斩获了值得褒奖的成果。我们将提取时间从 Tailwind 的原本 388ms 减少到 98ms。这大约优化了 4 倍。

类名转 CSS

在这个阶段,我们尚未生成任何 CSS 规则。我们仍需要某些规则,替换起初原始 CSS 文件中的 @tailwindcss 规则。但我们现在可以诉诸潜在的 tailwind 类名列表来实现。其中一大坨可能是“假阳性”,因此我们需要确保,如果我们检测到不渲染 CSS 的类名,我们可以尽快退出。

第一步是解析前面的变体(如果有的话)。粉丝请记住,可以通过 : 尾冒号字符来检测变体。变体的要点之一是,如果变体存在,它们能且仅能影响选择器,且可能影响周围的媒体查询。它们本身不用于生成 CSS 属性。解析变体是一项平平无奇的体力活。如果我们检测到假定的变体不存在,我们就可以提前退出。

比变体更有趣的是规则生成方面。大多数 tailwind 类名没有变体。由于 Tailwind 映射了一大坨 CSS 属性,因此我们需要匹配的潜在数量相当惊人。我尝试了各种方案,比如预先匹配所有静态 tailwind 类名,将所有内容放入一个对象中,该对象的方法类似虚拟函数表。但最终,私以为既敏捷又易维护的方案是,一坨既大又笨的 switch 语句。

function parse(lexer, config, hasNegativePrefix) {
  const first = lexer.nextSegment()
  switch (first) {
    case “aspect”:
      //...
    case “block”:
      if (!lexer.isEnd) return // 退出
      return `display: block`
    case “inline”:
      if (lexer.isEnd) return `display: inline`
      const second = lexer.nextSegment();

      if (
        second !== “block” || second !== “flex” || second !== “table”
        || second !== “grid”
      ) {
        return // 退出
      }

      return `display: inline-${second}`

    // 剩下的 1000 行类似的代码
  }
}

这看起来可能是非常标准的解析器代码,但存在某些有趣的东东。显而易见,我们会逐步检查我们是否仍在有效路径上。这增加了一大坨额外检查,但我发现这些成本能够被提前退出的收益抵消。在之前的某些迭代中,我在提取部分犯错了,最终向该 parse 函数“投喂”了一大坨已知的“假阳性”字符串。但是因为 parse 函数很快就在无效的类名及时止损,所以我花了一段时间才注意到,它整体而言仍然很快。

粉丝请注意传递给 parse() 函数的 hasNegativePrefix 参数。一大坨数字筑基的属性(比如 padding)可以通过在类名前加上 - 减号字符来接收负值。

'pl-2' // -> padding-left: 0.5rem;
'-pl-2' // -> padding-left: -0.5rem;

前置减号字符在传递给 parse() 函数之前会被移除,这样我们可以为正常或反常情况重用相同的 case 分支。这里没有显示,但解析器还支持任意值、important 声明、透明度的 color 值等等。

尽管我没有实现所有规则,但所有语法变体都支持。不过,我确实实现了相当一部分规则,大约有 126 条。这大约占 tailwind 语法的 80%。尽管这主要是一个原型,但我想更好地了解解析器如何扩展。

有了生成的规则,我们现在终于可以替换原始 CSS 文件中的 @tailwind 规则了。如果我们希望它能够感知源码映射,那么我们可以使用 Magic String

万事俱备后,以下是最终测量结果:

提取:98ms

解析:21ms

总时间:192ms(包括运行时启动时间)

整个项目由 5 个文件组成(不包括测试),代码不足 3_000 行。

Rust 又如何呢?

我们这里的迷你项目比 og Tailwind CSS cli 更快的原因是,我们完全避免了用 PostCSS 解析任何内容,而是聚焦于尽快生成 CSS 规则。Tailwind 团队目前正在用 Rust 重写 Tailwind CSS,据我所知,它们已经取得了很大进展。我没有任何相关数据,因为它尚未发布。就像任何诉诸 Rust 重写的 JS 工具一样,亟待解决的是它们的插件的生态。Tailwind 确实支持在其配置中定义的自定义变体或完整规则。一旦发布,测评两者将会很有趣。

完结撒花

对我而言,Tailwind CSS 是 CSS 中的 jQuery。并不是所有人都喜欢它,但它为网络行业注入正能量毋庸置疑。它使全新一代开发者能够进军 Web 开发领域。

当我入门 Web 开发时,jQuery 正血气方刚,没有它我就永远不会和 JS 贴贴。直到职业生涯两年后,我才真正入坑 JS,并学习了基础知识。在 CSS 方面,Tailwind CSS 正在为当今的开发者做类似的事情。

免责声明

本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Speeding up the JavaScript ecosystem - Tailwind CSS[1]。

以上就是JS 生态系统加速Tailwind CSS工作原理探究的详细内容,更多关于JS Tailwind CSS的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文