加速 JavaScript 生态系统 - Tailwind CSS

更新日期: 2023-11-08阅读: 705标签: Tailwind

诚然,我目前手头没有使用 Tailwind css 编写的更大项目。那些使用 Tailwind 的公司范围太小,无法进行有意义的性能分析。所以我想还有什么比在 Tailwind 自己的 tailwindcss.com 网站上介绍 Tailwind 更好的方法呢!不过,一开始我就遇到了一个问题:该项目是使用 Next.js 构建的,这使得很难获得有意义的跟踪。更重要的是,这些痕迹包含太多与 TailwindCSS 完全无关的噪音。

相反,我决定使用完全相同的配置在项目上运行 Tailwind CLI,以获得一些性能跟踪。运行 CLI 构建总共需要 3.2 秒,而在运行时,Tailwind 花费了 1.4 秒。提取数字的机器是我个人的 MacBook M1 Air。查看个人资料,我们可以找出花费时间的一些关键领域:


像我的帖子中一样,火焰图的 x 轴不显示“发生时”的时间,而是显示此处合并在一起的每个调用堆栈的累积时间。这使得一目了然地看到问题区域变得容易得多。我在用着测速仪以可视化 CPU 配置文件。

有一个块处理提取潜在的解析候选块,一个块用于配置和插件初始化,CSS 生成,一些 PostCSS 的东西,当有 PostCSS 时,通常可以同时提到 autoprefixer,因为两者经常一起使用。值得注意的是,在不执行任何操作的情况下加载 autoprefixer 似乎已经消耗了相当多的时间。


改变现状

浏览一下 Tailwind CSS 代码库并查看配置文件,肯定有一些功能可以进一步优化的地方。但如果我们这样做,我们只会获得几个个位数的百分比改进。在之前的帖子中,个人资料中通常会出现一些明显的内容,但是如果没有任何明显的迹象表明时间花在哪里,我们该怎么办?

实现多个因素加速(而不仅仅是低百分比)的秘诀不在于应用通用规则或习惯,例如“不要在 for 循环内创建闭包”。这是一个常见的误解,即如果您遵循所有这些“最佳实践”,您的代码就会很快,因为在大多数情况下(不是全部)令人不安的事实是它并不重要。使代码真正快速的原因是意识到它应该解决什么问题,然后采取最短的路径来实现该目标。

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

小剧透:



Tailwind CSS 的工作原理

@tailwind从本质上讲,Tailwind CSS 的工作方式是向它传递一些 CSS 文件,然后它在其中查找规则。如果遇到这样的规则,它将爬行项目中的其他文件以查找 tailwind 类名并将其注入到找到该@tailwind规则的 CSS 文件中。它还有其他一些方面,但为了本文的简单起见,我们现在将忽略其他 at 规则。

/* Input */
@tailwind base;
@tailwind components;
@tailwind utilities;

.foo {
color: red;
}

转化为:

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

/* …etc */
.foo {
color: red;
}

基于此,我们可以确定 Tailwind CSS 内部工作的几个阶段:

  1. 扫描.css文件中的@tailwind规则
  2. 根据用户在 tailwind 配置中提供的 glob 模式查找所有文件以从中提取 tailwind 类名称
  3. 找到这些文件后,提取潜在的顺风类名称
  4. 解析潜在的顺风类名称以检查它们是否确实是顺风类名称。如果是,则从中生成一些 CSS
  5. @tailwind将原始 css 文件中的规则替换为生成的 CSS


优化提取阶段

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

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

使用此正则表达式,查找@tailwind规则及其在所有 CSS 文件中的位置基本上是免费的,因为它大约需要0.02ms. 与 Tailwind CSS 所需的 3.2 秒完整时间相比,这个时间并不重要。当涉及到基于用户指定的 glob 模式查找所有文件时,我们无能为力,因为我们无论如何都需要访问文件系统,并且受到运行时函数的限制readFile。为我们提供。

然而,一旦读取了这些文件并且我们需要提取潜在的顺风类名称候选者,我们就可以做很多事情。但有一个问题:我们如何检测什么是顺风类名称,什么不是?表面上听起来很简单,但实际上并不那么容易。问题是没有制造商或任何其他指示表明字符序列是有效的顺风类名称。可能存在与 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 语法的一部分,但它可以由用户添加。因此,我们在这里唯一真正的选择是尽可能减少搜索空间,然后向解析器提供剩余的候选者。如果解析器生成了一些 CSS,那么我们就知道类名是有效的。如果没有,那么它就无效。这反过来意味着我们需要优化解析器,以便在检测到没有定义的字符串值时尽快退出。

提醒我们自己:388ms目前这是在 Tailwind CSS 中实现的。


我在本地修补了 Tailwind CSS,以显示有关提取器提取的值的一些统计数据

  • 已解析文件:454
  • 候选字符串:26466

但更有趣的是查看提取代码提取的最常见的前 10 个值:

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

换句话说:在 26466 个匹配的字符串中,其中 19630 个显然是无效的 tailwind 类名。公平地说,Tailwind CSS 有一些缓存来减轻检查某些内容是否误报的情况。并且已经有一个代码注释说,对其正则表达式的任何改进都可以将 Tailwind CSS 速度提高多达 30%。


正则表达式所有的东西

在这里使用正则表达式的好处和坏处是它不具有语言感知能力。它不知道我们是否正在操作.js或.html文件,更糟糕的是语言可以相互嵌入。一个.html文件可以同时托管 HTML、JavaScript 和 CSS。文件中的 JSX 也是如此.jsx。当涉及 JavaScript 代码时,我们可以假设我们只需要查看字符串。

经过快速而肮脏的正则表达式后,我们将搜索空间从26466下到9633候选减少。仍然不是最佳的,但比我们开始时要好得多。现在,许多提取的字符串类似于更多潜在的顺风候选字符串:

  • relative not-prose [a:not(:first-child)>&]:mt-12
  • none
  • break-after
  • grid-template-rows
  • ...

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

  • 不允许有空格
  • 变体必须以冒号结尾:
  • 任意值用括号定义[foo]。它们必须位于类名的末尾
  • 变体也可以是任意的:[&>.foo]:border-2. 仍不得包含空格
  • 除括号内的值之外的任何内容都只能包含数字、字母字符或减号。我不确定是否允许下划线,但我猜它可能是用户定义的顺风类名
  • 有效的Tailwind类名称必须以[、-、!或开头a-z0-9

所有这些匹配确实会花费一些时间,并将总提取时间增加到92ms。在我们努力减少搜索空间之后,我们仍然剩下大约 8000 个潜在的顺风类名称(请记住,之前提取的字符串可以包含多个候选者)。

到目前为止,我们取得了相当值得称赞的成果。我们将 Tailwind 的原始提取时间缩短388ms至98ms. 这大约是 4 倍的改进。


将类名转换为 CSS

在这个阶段我们还没有生成任何 CSS 规则。我们仍然需要一些规则来替换以支持@tailwindcss我们开始的原始 CSS 文件中的规则。但我们现在可以通过潜在的顺风类别名称列表来做到这一点。其中很多可能都是误报,因此我们需要确保在检测到某个类名不渲染 CSS 时能够尽快摆脱困境。

第一步是解析前面的变体(如果有)。请记住,可以通过尾随冒号字符来检测变体:。变体的一个关键方面是它们仅影响选择器,并且可能影响周围的媒体查询(如果存在)。它们本身不用于生成 CSS 属性。解析变体是一项繁重的工作,没什么特别的。如果我们检测到假定的变体不存在,我们就可以提前退出。

比变体更有趣的是规则生成方面。大多数顺风类名称没有变体。由于 Tailwind 反映了许多 CSS 属性,因此我们需要进行的潜在匹配数量相当大。我尝试了各种方法,例如预先匹配所有静态顺风类名称,将所有内容放入一个对象中,并使用虚拟函数表等方法来使用。但最终,最快且我觉得最容易维护的是一个巨大的愚蠢的 switch 语句。

function parse(lexer, config, hasNegativePrefix) {
const first = lexer.nextSegment()
switch (first) {
case “aspect”:
//...
case “block”:
if (!lexer.isEnd) return // bail out
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 // bail out
}
return `display: inline-${second}`
// ...1000 lines more of this
}
}

这可能看起来非常标准的解析器代码,但有一些有趣的方面。显而易见的是,每一步我们都会检查是否仍在有效路径上。这增加了很多额外的检查,但我发现这些成本被能够更早摆脱困境的收益所抵消。在之前的一些迭代中,我在提取部分犯了一个错误,最终向该解析函数提供了太多已知的误报字符串。但是因为解析函数很快就摆脱了无效的类名,所以我花了一段时间才注意到它总体上仍然很快。

值得注意的是hasNegativePrefix传递给parse()函数的参数。许多基于数字的属性(例如 padding)可以通过在类名称前添加减号字符来接收负值-。

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

前导减号字符在传递给函数之前会被删除parse(),以便我们可以为正常情况和负情况重用相同的情况分支。此处未显示,但解析器还支持任意值、重要声明、具有不透明度的颜色值等。

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

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

一切就绪后,以下是最终测量结果:

  • Extract: 98ms 

  • Parse: 21ms 

  • Total time: 192ms (including runtime startup time)

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


Rust 如何?

我们这里的小项目比 og Tailwind CSS cli 更快的原因是我们完全避免了用 PostCSS 解析任何内容,而是专注于尽快生成 CSS 规则。Tailwind 团队目前正在用 Rust 重写 Tailwind CSS,据我所知,他们已经取得了很大进展。我没有任何数字,因为它还没有发布。就像任何被重写为 Rust 的 JavaScript 工具一样,仍然需要解决的是它们的插件故事将会是什么样子。Tailwind 确实支持在其配置中定义的自定义变体或完整规则。一旦它出来,比较两者将会很有趣。


结论

这是一次有趣的小探索,旨在了解针对性能进行调整的 Tailwind CSS 架构会是什么样子。不可否认,这篇文章比之前的文章花了我更长的时间来写,因为要达到我满意的效果需要大量的原型设计。无论 Tailwind CSS 团队决定做什么,我都非常期待。

对我来说,Tailwind CSS 是 CSS 中的 jquery。并不是每个人都喜欢它,但它对网络行业的积极影响是不可否认的。它使全新一代开发人员能够进入 Web 开发领域。

我真的很欣赏他们正在尝试做的事情,因为这反映了我自己成为开发人员的道路。当我开始 Web 开发时,jQuery 正处于鼎盛时期,没有它我就永远不会接触 JavaScript。直到进入职业生涯两年后,我才对 JavaScript 本身产生了兴趣并学习了基础知识。在 CSS 方面,Tailwind CSS 正是为当今的开发人员做的。

我真的很高兴它们的存在,即使它们的编译器可以更快。

翻译来自:https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-8/

链接: https://fly63.com/article/detial/12544

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!