Inside clsx
•7 min read
clsx 及 classnames 都是在 react 中常見,處理 className
的工具
不過 clsx 在 README.md 提到
Also serves as a faster & smaller drop-in replacement for the classnames module.
讓我好奇 clsx 比較快的原因,這裡記錄下觀察到的資訊,參考版本為:
Implementation detail
先從 clsx
開始看起:
clsx
是一個可以將數量不定的參數,轉換成字串的 function- 呼叫
clsx(...)
,透過arguments
得到所有參數進行 for-loop,每次 loop:- 將
arguments[i]
依序暫存至變數tmp
- 如果
tmp
為 truthy,調用toVal(tmp)
,將結果暫存至變數x
- 如果
x
為 truthy,進行 string concatenation - 變數
str
為 truthy 表示非空字串,在 string concatenation 前補上空格 (space-separated classes)
- 將
export function clsx() {
var i=0, tmp, x, str='', len=arguments.length;
for (; i < len; i++) {
if (tmp = arguments[i]) {
if (x = toVal(tmp)) {
str && (str += ' ');
str += x
}
}
}
return str;
}
接著看到 toVal
的實作:
- 透過
typeof
判斷string | number
(L8) typeof mix === 'object'
,mix
可能為null
,ClassArray
,ClassDictionary
(L10)- 透過
Array.isArray(mix)
判斷mix
是否為ClassValue
(L11) - 如果
mix
為ClassArray
,透過 for-loop 搭配遞迴得到 string concatenation 的結果 (L13-20) - 如果
mix
為ClassDictionary
或null
,透過 for…in,將 value 為 truthy 的 key 做字串串接
💡
A for…in loop only iterates over enumerable, non-symbol properties.
type ClassValue = ClassArray | ClassDictionary | string | number | bigint | null | boolean | undefined;
type ClassDictionary = Record<string, any>;
type ClassArray = ClassValue[];
function toVal(mix: ClassValue) {
var k, y, str='';
if (typeof mix === 'string' || typeof mix === 'number') {
str += mix;
} else if (typeof mix === 'object') {
if (Array.isArray(mix)) {
var len=mix.length;
for (k=0; k < len; k++) {
if (mix[k]) {
if (y = toVal(mix[k])) {
str && (str += ' ');
str += y;
}
}
}
} else {
for (y in mix) {
if (mix[y]) {
str && (str += ' ');
str += y;
}
}
}
}
return str;
}
Benchmarks
benchmark suite 是參考 clsx,運行環境為 Apple M1 Pro,Node 版本 v20.13.1
classnames v2.3.0 的實作:
- 每次迴圈計算出的字串,會先放進
classes
陣列 - 呼叫
classes.join(' ')
,在每個字串之間加上空格
function classNames () {
var classes = [];
for (var i = 0; i < arguments.length; i++) {
// parse arguments
}
return classes.join(' ');
}
下方 benchmarks 可以看出,clsx 的效率明顯優於 classnames v2.3.0
# Strings
classnames x 8,677,286 ops/sec ±0.71% (93 runs sampled)
clsx x 16,846,632 ops/sec ±0.41% (96 runs sampled)
# Objects
classnames x 8,137,930 ops/sec ±0.25% (101 runs sampled)
clsx x 12,294,389 ops/sec ±0.46% (96 runs sampled)
# Arrays
classnames x 4,593,609 ops/sec ±0.34% (96 runs sampled)
clsx x 12,292,711 ops/sec ±0.23% (100 runs sampled)
# Nested Arrays
classnames x 2,678,768 ops/sec ±0.38% (93 runs sampled)
clsx x 9,643,874 ops/sec ±0.25% (92 runs sampled)
# Nested Arrays w/ Objects
classnames x 4,103,597 ops/sec ±0.28% (97 runs sampled)
clsx x 9,928,715 ops/sec ±0.39% (100 runs sampled)
# Mixed
classnames x 5,417,170 ops/sec ±0.29% (98 runs sampled)
clsx x 10,918,336 ops/sec ±0.38% (99 runs sampled)
# Mixed (Bad Data)
classnames x 2,464,890 ops/sec ±0.22% (99 runs sampled)
clsx x 3,936,224 ops/sec ±0.23% (100 runs sampled)
classnames v2.4.0 後,也改用 string concatenation 來優化效能 (pull#36),實作方式與 clsx 有九成像
調整後的 benchmarks 如下:
# Strings
classnames x 14,972,691 ops/sec ±0.30% (93 runs sampled)
clsx x 16,634,521 ops/sec ±0.35% (98 runs sampled)
# Objects
classnames x 10,998,150 ops/sec ±4.18% (91 runs sampled)
clsx x 12,014,884 ops/sec ±3.30% (96 runs sampled)
# Arrays
classnames x 11,106,140 ops/sec ±0.26% (99 runs sampled)
clsx x 12,253,649 ops/sec ±0.28% (100 runs sampled)
# Nested Arrays
classnames x 8,471,529 ops/sec ±0.24% (100 runs sampled)
clsx x 9,639,725 ops/sec ±0.26% (94 runs sampled)
# Nested Arrays w/ Objects
classnames x 9,079,067 ops/sec ±0.40% (98 runs sampled)
clsx x 9,947,194 ops/sec ±0.25% (98 runs sampled)
# Mixed
classnames x 10,000,790 ops/sec ±0.33% (98 runs sampled)
clsx x 11,042,621 ops/sec ±0.24% (99 runs sampled)
# Mixed (Bad Data)
classnames x 3,263,129 ops/sec ±0.23% (100 runs sampled)
clsx x 3,921,987 ops/sec ±0.31% (100 runs sampled)
從上方的結果得知,clsx 在速度上還是優於 classnames,我認為可能的差異:
- 在 clsx 的 pull#26 中看到,將 for-loop 需要的
Array.length
暫存在變數中,可以節省每次透過原型鏈遍歷length
屬性的時間 (雖然不多) - classnames 的實作,會建立更多 function execution context
針對上述幾點,參考 clsx 的做法,將 classnames 改寫
var hasOwn = {}.hasOwnProperty;
function classNames () {
var classes = '';
var len = arguments.length;
for (var i = 0; i < len; i++) {
var arg = arguments[i];
if (arg) {
const parsed = parseValue(arg);
if (parsed) {
if (classes) classes += ' ';
classes += parsed;
}
}
}
return classes;
}
function parseValue (arg) {
if (typeof arg === 'string' || typeof arg === 'number') {
return arg;
}
if (typeof arg !== 'object') {
return '';
}
var classes = '';
if (Array.isArray(arg)) {
var len = arg.length;
for (var i = 0; i < len; i++) {
if (arg[i]) {
const parsed = parseValue(arg[i]);
if (parsed) {
if (classes) classes += ' '
classes += parsed
}
}
}
return classes;
}
if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) {
return arg.toString();
}
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
if (classes) classes += ' '
classes += key
}
}
return classes;
}
在 benchmarks 上可以看到非常相近的結果
# Strings
classnames x 15,140,323 ops/sec ±0.42% (96 runs sampled)
clsx x 16,104,379 ops/sec ±0.26% (101 runs sampled)
# Objects
classnames x 10,319,647 ops/sec ±0.27% (98 runs sampled)
clsx x 10,623,505 ops/sec ±0.26% (100 runs sampled)
# Arrays
classnames x 10,976,681 ops/sec ±0.24% (100 runs sampled)
clsx x 10,998,613 ops/sec ±0.26% (101 runs sampled)
# Nested Arrays
classnames x 8,298,519 ops/sec ±0.99% (97 runs sampled)
clsx x 8,486,556 ops/sec ±0.32% (98 runs sampled)
# Nested Arrays w/ Objects
classnames x 8,227,124 ops/sec ±0.35% (97 runs sampled)
clsx x 8,331,587 ops/sec ±0.30% (100 runs sampled)
# Mixed
classnames x 8,945,241 ops/sec ±1.13% (98 runs sampled)
clsx x 9,109,764 ops/sec ±0.35% (97 runs sampled)
# Mixed (Bad Data)
classnames x 2,991,448 ops/sec ±0.24% (95 runs sampled)
clsx x 3,483,495 ops/sec ±0.25% (100 runs sampled)
Summary
- classnames v2.4.0 後,在效能上與 clsx 已經沒有顯著差別。要選哪一個也許可以檢查 lockfile 有沒有既存的依賴 (可能是某個套件有使用到)
- 頻繁讀取物件上的某個屬性,可以考慮暫存在某個變數,節省遍歷原型鏈的時間
- 多數情況下,直接對字串做 string concatenation,比操作一個陣列,透過
Array.join()
轉成字串還快 - 多數情況下,追求極致效能或極小 bundle size,會犧牲可讀性 (但在小工具上或許很適合,視情境權衡)