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

 更新时间:2024年01月21日 11:11:54   作者:大家的林语冰 人猫神话  
这篇文章主要为大家介绍了JS 生态系统加速Tailwind CSS使用及工作原理探究,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

引言

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

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

  • PostCSS,SVGO 等等

  • 模块解析

  • 使用 eslint

  • npm 脚本

  • draft-js emoji 插件

  • polyfill 暴走

  • 桶装文件暴走

  • Tailwind CSS

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 内部流程的若干阶段:

  • 扫描 .css 文件中的 @tailwind 规则

  • 基于用户 tailwind 配置中提供的 glob 模式,查找所有文件,从中提取 tailwind 类名

  • 一旦找到这些文件,就会提取潜在的 tailwind 类名

  • 解析潜在的 tailwind 类名,检查它们是否是 tailwind 类名。如果是,那就从中生成某些 CSS

  • 将原本 css 文件中的 @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 打补丁,显示了某些有关提取器的提取值的统计数据。

  • 已解析文件:454

  • 候选字符串:26_466

但更有趣的是,瞄一下提取程序提取最常见的前 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 类名的语法遵循相当简单的规则:

  • 禁用空格
  • 变体必须以 : 冒号结尾
  • 任意值诉诸 [foo] 括号定义。它们必须位于类名末尾
  • 变体任意:[&>.foo]:border-2。禁止包含空格
  • 除括号内的值之外的其他东东,只能包含数字、字母字符或减号。我不确定是否允许下划线,但我猜它可能是用户定义的 tailwind 类名
  • 有效的 Tailwind 类名必须以 [-!a-z 或 0-9 开头

所有这些匹配客观存在某些时间开销,并将总提取时间增加到 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的资料请关注脚本之家其它相关文章!

相关文章

  • JavaScript Event学习第十一章 按键的检测

    JavaScript Event学习第十一章 按键的检测

    检测用户的按键是事件处理程序的一个很特别的环节。这一章我们着力解决一些非常棘手的问题,并且制定一个完备的表格。
    2010-02-02
  • 跟我学习javascript的闭包

    跟我学习javascript的闭包

    跟我学习javascript的闭包,这篇文章的目的就是让大家对javascript闭包有一个非常全面的了解,感兴趣的小伙伴们可以参考一下。
    2015-11-11
  • 微信小程序数据操作指南之从绑定到更新的操作方法

    微信小程序数据操作指南之从绑定到更新的操作方法

    在微信小程序开发中,数据操作是不可或缺的一环,文章详细介绍了数据绑定、更新等方法,并提供示例和注意事项,帮助开发者更好地应用这些技术,本文给大家介绍微信小程序数据操作指南之从绑定到更新,感兴趣的朋友跟随小编一起看看吧
    2024-10-10
  • Bootstrap3 datetimepicker控件使用实例

    Bootstrap3 datetimepicker控件使用实例

    这篇文章主要为大家详细介绍了Bootstrap3 datetimepicker控件使用实例,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-12-12
  • Javascript中 toFixed四舍六入方法

    Javascript中 toFixed四舍六入方法

    本篇文章主要介绍了Javascript中 toFixed四舍六入方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-08-08
  • JS检测数组类型的方法小结

    JS检测数组类型的方法小结

    这篇文章主要介绍了js检测数组类型的方法小结,有instanceof方法Array.isArray() 方法和Object.prototype.toString.call()方法,都是比较常用的,需要的朋友可以参考下
    2017-03-03
  • javascript中函数的写法实例代码详解

    javascript中函数的写法实例代码详解

    这篇文章主要介绍了javascript中函数的写法 ,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2018-10-10
  • 微信小程序实现两边小中间大的轮播效果的示例代码

    微信小程序实现两边小中间大的轮播效果的示例代码

    这篇文章主要介绍了微信小程序实现两边小中间大的轮播效果的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-12-12
  • javascript中方便增删改cookie的一个类

    javascript中方便增删改cookie的一个类

    把jquery.cookie.js改了一下,改成了纯javascript版本,以备我以后项目只需,增加了一个得到页面全部cookie键值的功能
    2012-10-10
  • js实现网页定位导航功能

    js实现网页定位导航功能

    这篇文章主要为大家详细介绍了js实现网页定位导航功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-03-03

最新评论