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-convert:npm 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 亿次,慢的是库,不是语言。












