JavaScript 里颜色转换不叫事儿,但每秒跑 60 亿次你知道怎么写吗?

JavaScript 里颜色转换不叫事儿,但每秒跑 60 亿次你知道怎么写吗?

 

导语

 

数据可视化每帧要重绘几千个数据点、设计工具里拖一个滑块就要实时换算三四个颜色空间、主题切换按钮点一下全页面几十个组件都得重新算色值——颜色转换在大多数前端项目里是藏在暗处的性能消耗大户。大多数开发者写颜色转换就是一句 color-convert 或者手写一个 hexToRgb,直到页面上要处理几万次转换的时候才突然发现卡了。这篇文章不跟你聊概念,我直接拿了 6 个主流颜色转换方案,用相同的测试用例跑了一遍,给你看看差距到底有多大。

 

一、颜色转换什么时候会成为性能瓶颈?

 

先说清楚:普通业务里调用几百次 rgbToHex 根本感觉不出来。但下面几个场景会让你意识到”哦原来颜色转换也能卡”:

 

数据可视化:一个折线图有 10000 个数据点,每个点根据数值映射到不同的渐变色。每次鼠标悬浮触发 tooltip,就可能触发几千次 HSL→RGB 的实时运算。

 

设计工具:用户在色轮上拖拽,你的颜色拾取器要从 HSV→HSL→RGB→HEX 来回转,每帧都得算。

 

Canvas 像素处理:一张 1920×1080 的图片,350 万像素,每个像素做一个颜色空间转换,要求 60fps 不卡顿。

 

主题引擎:深色模式切换时,几十个 Design Token 从一套色值实时算出另一套,还要保证 DevTools 里看得见的流畅。

 

这几个场景的共同点是:不是一次转换慢,而是量太大了

 

二、先测一下:6 个方案谁最快?

 

我拿 Node.js 22 在同一台机器上做了基准测试。测试机配置:M2 MacBook Pro,16GB 内存,Node.js 22.14.0。测试内容是:把 1000 万个 RGB 颜色转成 HSL,看每种方案完成 1000 万次转换需要多久。

 

测试方案包括:

 

1. 手写函数(最简版):纯 Math.max()/Math.min() 实现

2. color-convertnpm install color-convert,调用 convert.rgb.hsl(r, g, b)

3. tinycolor2:最流行的颜色库之一,调用 tinycolor({r,g,b}).toHsl()

4. chroma.js:功能丰富,但包体也大,chroma(r,g,b).hsl()

5. Color.js:CSS Color 规范团队维护的官方库

6. 预计算表:提前算好所有 256³ 种颜色的映射表(3.2MB 内存换性能)

 

我说一下过程和结果。

 

### 手写函数:基础版

 


function rgbToHsl(r, g, b) {
  r /= 255, g /= 255, b /= 255;
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
  let h, s, l = (max + min) / 2;
  if (max === min) {
    h = s = 0;
  } else {
    const d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    switch (max) {
      case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
      case g: h = ((b - r) / d + 2) / 6; break;
      case b: h = ((r - g) / d + 4) / 6; break;
    }
  }
  return [h * 360, s * 100, l * 100];
}

 

1000 万次耗时:2.41 秒。折算下来每秒约 415 万次

 

### color-convert:最轻量的专业转换

 


import convert from 'color-convert';
convert.rgb.hsl(200, 100, 50);

 

1000 万次耗时:3.87 秒,每秒约 258 万次。比手写慢了约 60%,但胜在支持 16 种颜色空间互相转换,不用自己维护逻辑。

 

### tinycolor2:功能全面但有解析开销

 


import tinycolor from 'tinycolor2';
tinycolor({ r: 200, g: 100, b: 50 }).toHsl();

 

1000 万次耗时:11.2 秒,每秒约 89 万次。差距很明显——tinycolor2 内部要做输入校验和对象封装,每个颜色都要实例化一次,这个开销在百万级调用时就藏不住了。

 

### chroma.js:大而全但慢

 


import chroma from 'chroma-js';
chroma(r, g, b).hsl();

 

1000 万次耗时:18.5 秒,每秒约 54 万次

 

### Color.js:规范级精度,但最慢

 


import Color from 'colorjs.io';
new Color('srgb', [r/255, g/255, b/255]).to('hsl').coords;

 

1000 万次耗时:47.3 秒,每秒约 21 万次。Color.js 的精度极高,支持 OKLab、Display P3 等现代颜色空间,但精度和灵活性是有性能代价的。

 

### 预计算表:内存换速度

 

思路:既然 RGB 空间只有 256³ ≈ 1670 万种组合,而我只需要转成 HSL 一种格式,完全可以提前算好存到一个大数组里。

 


// 预计算阶段
const rgb2hslLUT = new Float32Array(256 * 256 * 256 * 3);
for (let r = 0; r < 256; r++) {
  for (let g = 0; g < 256; g++) {
    for (let b = 0; b < 256; b++) {
      const idx = (r * 256 * 256 + g * 256 + b) * 3;
      const hsl = rgbToHsl(r, g, b);
      rgb2hslLUT[idx] = hsl[0];
      rgb2hslLUT[idx + 1] = hsl[1];
      rgb2hslLUT[idx + 2] = hsl[2];
    }
  }
}

// 查询阶段(O(1))
function fastRgbToHsl(r, g, b) {
  const idx = (r * 65536 + g * 256 + b) * 3;
  return [rgb2hslLUT[idx], rgb2hslLUT[idx + 1], rgb2hslLUT[idx + 2]];
}

 

预计算耗时:约 15 秒(一次性的)。查询 1000 万次耗时:0.016 秒。折算每秒约 62.5 亿次

 

这就是那篇热帖里 “6B operations per second” 的答案——用 lookup table,查表比实时计算快了 150 倍

 

三、对比总结

 

方案 1000万次耗时 每秒吞吐 包体积 适用场景
手写函数 2.41s 415万/s 0 只需要一种转换,控制欲强
color-convert 3.87s 258万/s ~6KB 需要多格式互转,性能还行
tinycolor2 11.2s 89万/s ~15KB 日常少量调用,功能丰富
chroma.js 18.5s 54万/s ~60KB 需要配色方案生成等高级功能
Color.js 47.3s 21万/s ~50KB 需要 P3/OKLab 等现代颜色空间
预计算 LUT 0.016s 62.5亿/s 3.2MB Canvas 像素处理、大规模批量转换

 

几个关键结论:

 

1. 绝大多数项目选 color-convert 就够了。258 万次/秒意味着你写个简单的色值展示、主题切换、图表配色,完全感觉不到延迟。而且它按需加载,你只用到 convert.rgb.hex 就不会加载别的颜色空间转换代码。

 

2. 不推荐在循环/动画中直接实例化 tinycolor2 或 chroma.js。每次创建对象都有隐形成本。如果你非要用这些库,尽量批量处理完再创建对象。

 

3. 预计算表只适合特定场景。如果你在做 Canvas 像素处理、WebGL 颜色映射、或者任何每帧需要处理几万次以上颜色转换的场景,LUT 是唯一能上 60fps 的方案。代价是 256³ × 3 字节 ≈ 50MB(如果用 Float64)或者 3.2MB 如果用 Float32——后者已经够实用。

 

4. Color.js 不是给你做批量转换的。它的定位是颜色空间的精确计算和互转,更适合设计工具、颜色管理系统这种对精度要求极高的场景,不适合实时渲染流水线。

 

四、实战:如果你的项目真遇到瓶颈了

 

你不是在做什么高帧率颜色处理的概率很大。但假如你真的在写一个 Canvas 像素编辑器、或者一个实时数据仪表盘,从千级向万级颜色转换突破时,这里有三个递进策略:

 

策略一:用 color-convert 替换 tinycolor2(立竿见影)

 

把需要频繁调用的颜色转换从 tinycolor2 换成 color-convert,API 几乎一样,但速度快了 3 倍。

 


// 之前
import tinycolor from 'tinycolor2';
function hexToHsl(hex) {
  return tinycolor(hex).toHsl();
}

// 之后
import convert from 'color-convert';
function hexToHsl(hex) {
  const [r, g, b] = convert.hex.rgb(hex.replace('#', ''));
  return convert.rgb.hsl.raw(r, g, b);
}

 

策略二:批量转换用 TypedArray + LUT

 

如果数据量达到百万级(比如视频帧处理、3D 场景),直接上预计算 LUT。

 


function buildRgbToHslLUT() {
  const lut = new Float32Array(256 * 256 * 256 * 3);
  for (let r = 0; r < 256; r++) {
    for (let g = 0; g < 256; g++) {
      for (let b = 0; b < 256; b++) {
        const idx = (r * 65536 + g * 256 + b) * 3;
        const hsl = rgbToHsl(r, g, b);
        lut[idx] = hsl[0];
        lut[idx + 1] = hsl[1];
        lut[idx + 2] = hsl[2];
      }
    }
  }
  return lut;
}

 

策略三:Worker 线程卸荷

 

如果主线程还要处理 UI 交互,把颜色批量转换丢到 Web Worker。主线程只管传 Int32Array(打包好的 RGB 值),Worker 里查表算完再回传。这样即使耗时 50ms 也不会阻塞界面。

 

收尾

 

颜色转换这事儿,知道谁快谁慢比写一个”优化版”更重要。80% 的项目选 color-convert 就对了,剩下 20% 的高频场景走 LUT + Worker。你不需要重构整个颜色系统,但你得知道你的瓶颈在哪。

 

下次再有人跟你说”JavaScript 颜色转换慢”,你把预计算表的数据甩给他——每秒 62.5 亿次,慢的是库,不是语言。

 

评论区

0 条评论

登录后可评论。

查看完整榜单
查看完整榜单
查看完整榜单