10 min read

原子css落地经验分享 | 让我的CSS 体积减少80%?

原子css落地经验分享 | 让我的CSS 体积减少80%?
目前Facebook、Twitter已经使用原子CSS重构了他们的项目,新的CSS编写和构建方式让Facebook的主页减少了80%的CSS体积,提高了视觉还原效率。我们团队项目中也落地了原子css,来讲述一下我们遇到的问题,分享我们落地的经验。

本文主要结构:

  • 原子CSS的历史与特性介绍
  • 前端在B端实践中的一些经验
  • 实践前的疑虑
  • 展望与设想

鉴于作者的局限性,文中可能会出现一些纰漏,敬请纠正。

原子化特性基本介绍与原理

这一章节主要介绍当代原子化CSS的特性,特性背景,对已经了解的人来说篇幅会过长,可以跳过。

所以省流后就是:

  • 按需构建
  • JIT(即时编译)
  • 主题/响应式支持

文艺复兴

在过去,我们会通过less/scss loop来生成一些重复功能性的css:

.loop(@cursor) when (@cursor > 0) {
  .loop((@cursor - 1));
  .m-@{cursor}{
    margin: (1px * @cursor); 
  }
}
.loop();

这样我就生产了50个不同px的margin,为了方便,也给padding的left,top等都遍历一次,在当时刀耕火种的时代,也算是提升研效的大杀器。

但这样也会产生一个问题,就是产生过多冗余的样式,css的利用率不高。

现在工程化的提升,这样的方式就大可不必了。

目前原子化css的原理就是在限定范围的文件内进行class正则匹配扫描,可以看下面的白名单匹配器。所以,文件中用到哪个class name就打包哪个。

{
  purge: {
    options: {
      whitelistPatterns: [
        /^bg-.*/,
        /^font-.*/
      ]
    }
  }
  /* ... */
}

这个就是原子css一大特性按需构建,解决了样式冗余导致的打包体积过大的问题。

所以,理念和几年前是一样的,只是现在有工程化的加持,可以更好解决原有原子化的缺陷。

JIT(即时编译)

在上面的less loop中,我们穷举了1-50的margin,可是我需要一个margin:72px怎么办?继续loop不太合理,因为我们没法穷举所有的单位,因此,我们可以根据marign后面的单位进行即时编译。

比如m-72px最后会被编译成:

margin:72px;

颜色也可以编译:

// 匹配
text-{color} -> color: rgba(...);
border-hex-{hex} -> border-color: rgba(...);

// class语法
.text-cyan-400
.border-hex-6dd1c7

// 生成
.border-hex-6dd1c7 {
  --tw-border-opacity: 1;
  border-color: rgba(109, 209, 199, var(--tw-border-opacity));
}
.text-cyan-400 {
  --tw-text-opacity: 1;
  color: rgba(34, 211, 238, var(--tw-text-opacity));
}

主题/响应式

这部分我们实践的比较少,所以不做过深讨论。

其他特性大家可以自己到官网了解,这里不做赘述:

有谁在用

国外较大的实践比较多,国内确实比较少,腾讯部分团队也在使用,可以搜索一下关键字。

主要是说明这玩意儿可以放心用:

facebook

twitter

积累的经验

我们使用的是windicss,所以实践经验都基于windicss的特性,其他CSS原子化框架都大同小异。

与设计规范的结合

从方言到普通话。

在我看来,tailwind不仅仅是一个utils css库,而是一套业界原子css的命名规范,这使得后来者windicss,unocss的命名都基本按照了tailwind的规范。

当前,设计侧也很注重组件和原子,比如sketch的symbol,figma的token。属性和布局也都有相对应的规范。

下面就讲设计的规范翻译成class的成果。我们在项目中就可以直接shadow来表示规范中的"shadow"。

布局

布局简直太舒适了,当我记住了这些词后,写页面的效率起码提升50%(对我而言,感觉效率可能还会再高一些):

  • 不用切换css上下文
  • 不用给组件起名字
  • 不用考虑css的scope产生的副作用
注意space的使用

playground

子元素的重复样式

子元素重复的class可以通过伪类写在父元素上,这样更加整洁,也减少体积。

同样的布局实现,也可以采用下面这种方式:

playground

伪类与组合

伪类可以通过组合的方式一起触发,基本覆盖了我们常用的场景:

playground

safelist

因为windicss的原理还是正则匹配,只能做静态扫描,无法利用js运行结果。因此classname不能做拆分写。

这样使用会导致样式不生效:

<div className={`mt-[${size === 'lg' ? '22px' : '17px' }]`}></div>

下面是正确的使用方式:

<div className={ size === 'lg' ? 'mt-[22px]' : 'mt-[17px]' }></div>

可以有2种方式解决这个问题:

  1. 放在js变量中
const CURRENCY_COLOR_MAP = {
  [CurrencyType.USD]: 'bg-[#003491]',
  [CurrencyType.CAD]: 'bg-[#D3073C]',
  [CurrencyType.GBP]: 'bg-[#16367D]',
  [CurrencyType.EUR]: 'bg-[#1B9077]',
  [CurrencyType.JPY]: 'bg-[#E8374F]',
  [CurrencyType.NZD]: 'bg-[#2778B9]',
  [CurrencyType.AUD]: 'bg-[#215ABF]',
};
// ⚠️注意,因为这几个颜色只用到一次,且不在规范当中,如果是设计规范的最好按规范来比如 bg-red-1

这样扫描的时候就能加入构建。

  1. 使用safelist
export default defineConfig({
  safelist: 'p-1 p-2 p-3 p-4 bg-[#003491]',
})

这样,windicss会提前把这些样式加入构建,无论你在哪里使用,或者拆得更细。

组合的两种方式

拥有了原子化,你仍然可以写less/css,解决原子化不擅长的地方:

  1. 在style中
  1. 在配置文件中
export default {
  theme: {
    /* ... */
  },
  shortcuts: {
    'btn': 'py-2 px-4 font-semibold rounded-lg shadow-md',
    'btn-green': 'text-white bg-green-500 hover:bg-green-700',
  },
}

杂项

绘制比例矩形

// 正方形
aspect-square
// 16:9
aspect-video

https://windicss.org/plugins/official/aspect-ratio.html

文本溢出处理

// 一行超过就....
line-clamp-1
// 两行超过就....
line-clamp-2

https://windicss.org/plugins/official/line-clamp.html

鼠标箭头

export default {
  theme: {
    cursor: {
      'auto': 'auto',
      'default': 'default',
      'pointer': 'pointer',
      'wait': 'wait',
      'text': 'text',
      'move': 'move',
      'not-allowed': 'not-allowed',
      'crosshair': 'crosshair',
      'zoom-in': 'zoom-in',
    },
  },
}

你可能担心的问题

除了这些,你可能仍然还有疑虑,也是我之前遇到的一些问题,不过都找到一些初步解决方案。

如果你还有其他的疑虑,可以评论,我们一起思考解决。

css体积转嫁到html的问题

下面的图是使用原子化css和不使用的体积对比图,很明显,原子css大小是有极限的,其最大值就是所有内置属性的大小,但是一般只会用到20-30%左右的属性。

如果使用常规的方式开发,css的体积会跟项目的时间呈正相关。

可能有人就说了,css小了,那么html上的class名称变长了。

压缩的原理大家都知道,gzip 的核心Deflate使用的是LZ77 算法和 Huffman 编码来压缩文件,重复度越高的文件可压缩的空间就越大(引用网络)。

我们可以看下facebook的首页,ssr渲染的:

1.4MB->249kB 比较可观的压缩比

学习成本

学习一门新的规范,学习成本肯定是有的,也是需要面对的,不过这些东西可以帮助你快速上手。

cheatsheet速查表
可以快速查询规范中命名所对应的css属性,以及反查css属性所对应的命名。

https://tailwindcomponents.com/cheatsheet/

提示插件 webstorm/vscode
可以在html中提示规范中的属性,以及属性所对应的css

兼容性
目前大多数方案都不兼容IE,其中可能会出现的兼容性问题主要是:

css的新特性:这部分不使用就好
css变量:社区也有相关
浏览器兼容前缀:用autoprefix
所以还是可以继续放心食用

展望与设想
玩过设计的朋友其实知道,组件的设计其实也是一些属性的堆叠,比如颜色,背景,圆角,阴影,在figma中,他们可以统称为token。

这些figma token其实和css属性一样,组合后形成各自的界面

所以,前端视觉还原其实就是人工翻译这些属性(figma token)的过程

当前阶段我只能称为人工翻译,因为对于相同组件,不同人翻译的css表达肯定都有所不同

那么这个阶段是否可以用机器翻译呢?

是的,可以的,可以看下下面这张图片,在figma中,相关的视觉设计规范可以翻译成对应css规范

如果这个规范可以实现同步,那么UI变动后,前端还原滞后的问题能够有所改观。

想象一个场景:

设计需要做情人节活动,所有link和border要改成pink,只需要修改下figma的规范,同步到前端,前端做好审查然后发布,不用一个个找颜色修改了,完美!!

当然,这个想法目前正在实践中,后续有进展会继续出文章分享

大家如果有什么想法可以一起讨论下。

参考:

Facebook 重构:抛弃 Sass / Less ,迎接原子化 CSS 时代