Inside clsx

7 min read

clsxclassnames 都是在 react 中常見,處理 className 的工具

不過 clsx 在 README.md 提到

Also serves as a faster & smaller drop-in replacement for the classnames module.

讓我好奇 clsx 比較快的原因,這裡記錄下觀察到的資訊,參考版本為:

Implementation detail

先從 clsx 開始看起:

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 的實作:

💡

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 的實作

  1. 每次迴圈計算出的字串,會先放進 classes 陣列
  2. 呼叫 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 的做法,將 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

Reference