Tailwind CSS 类名压缩:应用与局限性

695 阅读4分钟

Tailwind CSS 类名压缩:应用与局限性

注意: 本文中涉及 Tailwind CSS 使用的是 v3.x 版本

背景

去年,在正忙于一个用 Tailwind CSS 构建的项目时,我偶然看到了一篇让我兴奋的文章,标题赫然写着"通过压缩类名减少了 70% 的 CSS 体积"。这引发了我的兴趣,促使我寻找优化我的代码体积的可能性。

就像 Google 做的那样

有时候,我们可以从业内标杆公司那里汲取灵感。你是否曾经看过 google.com 的源代码吗?如果你仔细分析,你可能会注意到一些令人惊讶的细节,其中之一就是,CSS 类名不过几个字符的长度。这是一个巧妙的优化策略,可以极大地减少 CSS 文件的体积,从而加快页面加载速度。

css-loader 的局限

我开始尝试一些可用的工具来压缩我的 Tailwind CSS 代码,但是很快我发现,虽然像 css-loader 这样的工具可以帮助进行 CSS 代码的压缩,但它们大多数都无法正确地压缩 Tailwind CSS 的类名。这是因为 css-loader 只能用来压缩 CSS Modules 生成的类名。那么,如何解决这个问题呢?

unpluign-tailwindcss-shortener-plugin 的实现

这时,需要尝试一个针对 Tailwind CSS 的压缩工具,我实现了一个有效的解决方案 - unpluign-tailwindcss-shortener-plugin。它将 Tailwind CSS 的类名映射到较短的类名,从而实现了压缩的效果。

实现过程中发现的问题

静态类名:指在编写 HTML 或者模板文件时就已经确定的类名。这些类名并不会在运行时发生变化,代码中明确写出了具体的类名。

动态类名:在运行时根据某些条件动态生成或修改的类名。通常在 JavaScript 或前端框架(例如 React、Vue 等)的使用中,通过条件判断来动态赋予 HTML 元素不同的类名,从而实现样式的动态变化

  • 处理静态类名实际上很简单,将 html、vue 的 template 以及 jsx 使用 AST 或者 正则找到类名替换它。
  • 对于动态类名的话,比较麻烦。后面提出一个约定:
    • 在 template、js、jsx 里,动态类名必须需要使用 cx、cva 包裹
    • cx、cva (support class-variance-authority) 可以使用 class-variance-authority、classanems、clsx、tailwind-merge 等等用来 类名拼接的工具,你甚至可以自定义一个。但是名字必须是 cx 或者 cva。

示例

应用后的效果

HTML

<!-- before -->
<div class="flex items-center justify-center h-screen px-6 bg-gray-200"></div>
<!-- after -->
<div class="h u bu i s j"></div>

JavaScrip

// before
import { cx } from "class-variance-authority";

const boxClass = cx(
  "flex items-center justify-center h-screen px-6 bg-gray-200"
);

// after
import { cx } from "class-variance-authority";

const boxClass = cx("h u bu i s j");
CSS
.px-6 {
  padding-left: 1.5rem;
  padding-right: 1.5rem;
}

.bg-gray-200 {
  --tw-bg-opacity: 1;
  background-color: rgb(229 231 235 / var(--tw-bg-opacity));
}

.justify-center {
  justify-content: center;
}

.items-center {
  align-items: center;
}

.h-screen {
  height: 100vh;
}

.flex {
  display: flex;
}

/* after */
.s {
  padding-left: 1.5rem;
  padding-right: 1.5rem;
}

.j {
  --tw-bg-opacity: 1;
  background-color: rgb(229 231 235 / var(--tw-bg-opacity));
}

.bu {
  justify-content: center;
}

.u {
  align-items: center;
}

.i {
  height: 100vh;
}

.h {
  display: flex;
}


使用

用开源项目 shadcn-admin (TailwindCSS + shadcn/ui) 作为例子。

  1. 拉取代码
git clone https://github.com/satnaing/shadcn-admin.git
  1. 下载依赖
pnpm i

pnpm i unplugin-tailwindcss-shortener
  1. 修改 vite.config.js 配置
import path from "path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import TailwindcssShortener from "unplugin-tailwindcss-shortener/vite";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    TailwindcssShortener({
      tailwindCSS: "./src/index.css",
      keyword: {
        cva: true,
        extra: "cn",
      },
    }),
  ],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});
  1. 检查项目内类名是否符合规范

修改 sidebar.tsx,按照约定修改

// 代码较多,只列出部分代码

// 修改前
<div
  onClick={() => setNavOpened(false)}
  className={`absolute inset-0 transition-[opacity] delay-100 duration-700 ${navOpened ? 'h-svh opacity-50' : 'h-0 opacity-0'} w-full bg-black md:hidden`}
/>

// 修改后
<div
  onClick={() => setNavOpened(false)}
  className={cn('absolute inset-0 transition-[opacity] delay-100 duration-700 w-full bg-black md:hidden', navOpened ? 'h-svh opacity-50' : 'h-0 opacity-0')}
/>
  1. 类名冲突问题
// src/lib/utils.ts
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'

const cssMap = (import.meta.env.TWST_CSS_MAP ?? {}) as Record<string, string>

const shortToOriginMap = new Map<string, string>(Object.entries(cssMap))
const originToShortMap = new Map<string, string>(
  Object.entries(cssMap).map(([k, v]) => [v, k])
)

export function cn(...inputs: ClassValue[]) {
  const classnames = clsx(inputs)
  const before = classnames
    .split(' ')
    .map((classname) => {
      const short = originToShortMap.get(classname)
      if (short) {
        return short
      }
      return classname
    })
    .join(' ')

  const merged = twMerge(before);

  const after = merged
    .split(' ')
    .map((classname) => {
      const short = shortToOriginMap.get(classname)
      if (short) {
        return short
      }
      return classname
    })
    .join(' ')

    return after
}

具体项目代码

其他示例

vue-cli-vue3

v-dashboard

shadcn-admin

类名压缩的局限和思考

一个不可忽视的事实是:尽管类名压缩确实能减少文件尺寸,但实际上可能对整体体积缩减的影响有限。一般情况下类名所占体积缩减可能在 70%左右,但实际上在多数项目中,类名体积在几十到一百字节之间,这意味着实际减少的仅是几十字节

最重要的一点是只采用 Tailwind CSS 类似 CSS 原子化的项目,本身 CSS 文件就已经非常小。

最后普,为了减小CSS文件的体积,我们有多项策略可行,而类名压缩仅是众多方法中的一种。此外,我们还可以通过代码分割、应用gzip压缩等其他技术手段来实现相同的目标。

最后,文中若有不足或错误之处,敬请批评指正。感谢您的阅读和宝贵意见!