DEV Community

Cover image for Comparison and Benchmarks of Node.js libraries to colorize text in terminal
webdiscus
webdiscus

Posted on

Comparison and Benchmarks of Node.js libraries to colorize text in terminal

In the Node.js world are huge number of different libraries for coloring text in the terminal. Every library write that "I am the best", "I am fastest", "I'm smallest", etc.

Today the most popular library and de facto standard is the chаlk. The chalk has rich functionality, is fast but not ideal. It lacks some useful features.

Let's explore this zoo and compare the availability of the most useful features.

Library
______________
- name
- code size
- named import
Naming colors ANSI 256
colors
True-
color
Chained
syntax
Nested
template strings
New
Line
Supports
CLI params
colors.js
18.1KB
❌ named import
non-standard
16 colors
only
FORCE_COLOR
--no-color
--color
colors-cli
8.6KB
❌ named import
non-standard
16 colors
only
--no-color
--color
cli-color
❌ named import
standard
16 colors
only
NO_COLOR
ansi-colors
5.8KB
❌ named import
standard
16 colors
only
FORCE_COLOR
colorette
3.3KB
✅ named import
standard
16 colors
NO_COLOR
FORCE_COLOR
--no-color
--color
picocolors
2.6KB
❌ named import
standard
8 colors
NO_COLOR
FORCE_COLOR
--no-color
--color
kleur
2.7KB
✅ named import
standard
8 colors
only
NO_COLOR
FORCE_COLOR
chalk
15KB
❌ named import
standard
16 colors
NO_COLOR
FORCE_COLOR
--no-color
--color
ansis
3.2KB
✅ named import
standard
16 colors
NO_COLOR
FORCE_COLOR
--no-color
--color

Smallest code size

This is the size of pure JS code from the node module, that is runned. This does not include tests, documentation or other files. The smaller is the imported code, the faster it will be loaded and executed, in theory.

Here are the smallest libs: picocolors (2.6KB), kleur (2.7KB), ansis (3.2KB) and colorette (3.3KB), the rest are at least double larger.

But size doesn't always matter. For example, the smallest picocolors has a minimum of features, while the equivalent by size ansis has a maximum of useful features.

Naming and number of supported colors

There are libraries with standard color names, similar to chalk (red, redBright), and non-standard ones, such as colors.js (brightRed) or colors-cli (red_bbt).

Only chalk and ansis support truecolor, others support for 16 or 256 colors, and smallest picocolors and kleur only support 8 base colors.

The ANSI 256 colors:

ANSI 256 colors

Code range Description
0 - 7 base colors
8 - 15 bright colors
16 - 231 6 × 6 × 6 cube
232 - 255 grayscale

Using the chalk or ansis you can colorize the text with truecolor.

import {hex, rgb} from 'ansis/colors';

// ruby color with bold style
hex('#E0115F').bold`Ruby`;
rgb(224, 17, 95).bold`Ruby`;
Enter fullscreen mode Exit fullscreen mode
import chalk from 'chalk';

// ruby color with bold style
chalk.hex('#E0115F').bold`Ruby`;
chalk.rgb(224, 17, 95).bold`Ruby`;
Enter fullscreen mode Exit fullscreen mode

Named import

Without a named import, you use the library name to access the colors, e.g.:

import color from 'some-lib.js';

let out = color.blue('file.js') + red(' not found!');
Enter fullscreen mode Exit fullscreen mode

Using the named import, you directly import only the colors you use, e.g.:

import {red, blue} from 'some-lib.js';

let out = blue('file.js') + red(' not found!');
// OR using template literals
let out = `${blue('file.js')} ${red('not found!')}`
Enter fullscreen mode Exit fullscreen mode

The colorette support only named import, kleur and ansis supports both named import and normal import, others does not have a named import.

Chained syntax

Any color and style can be combined in any order.
Almost all libraries support chained syntax, except for smallest picocolors and colorette.

import {blue, bold} from 'some-lib.js';

let out = blue.underline('file.js') + bold.red(' not found!');
Enter fullscreen mode Exit fullscreen mode

Nested template strings

In some complex cases you can use the nested template strings.
This feature is supported by ansis only.

import { red, green, blue } from 'ansis/colors';

let out = red`R ${green`G ${blue.bold`B`} G`} R`;
Enter fullscreen mode Exit fullscreen mode

Using the chunk it would look like:

import chalk from 'chalk';

let out = chalk.red`R` + 
            chalk.green`G` + 
              chalk.blue.bold`B` + 
            chalk.green`G` +
          chalk.red`R`;
Enter fullscreen mode Exit fullscreen mode

New lines

Not all libraries support correct style break at the end of line, see the table above. For example, the smallest picocolors, colorette and kleur cannot do this.

import { bgGreen } from 'ansis/colors';

bgGreen`\nAnsis\nNew Line\nNext New Line\n`;
Enter fullscreen mode Exit fullscreen mode

screenshot "New lines"

Benchmark

Setup and run

git clone https://github.com/webdiscus/ansis.git
cd ./ansis/bench
npm i
npm run bench
Enter fullscreen mode Exit fullscreen mode

Tested on

MacBook Pro 16" M1 Max 64GB
macOS Monterey 12.1
Node.js v16.13.1
Terminal iTerm2

Colorette bench

c.red(`${c.bold(`${c.cyan(`${c.yellow('yellow')}cyan`)}`)}red`);
Enter fullscreen mode Exit fullscreen mode
+ colorette           4,572,582 ops/sec   very fast
  picocolors          3,841,124 ops/sec   very fast
  ansis               2,669,734 ops/sec   fast
  kleur/colors        2,281,415 ops/sec   fast
  chalk               2,287,146 ops/sec   fast
  kleur               2,228,639 ops/sec   fast
  ansi-colors         1,265,615 ops/sec   slow
  colors.js           1,158,572 ops/sec   slow
  cli-color             470,320 ops/sec   too slow
  colors-cli            109,811 ops/sec   too slow
Enter fullscreen mode Exit fullscreen mode

Base colors

const colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'];
colors.forEach((color) => c[color]('foo'));
Enter fullscreen mode Exit fullscreen mode
+ picocolors          8,265,628 ops/sec  very fast
  ansis               6,197,754 ops/sec  fast
  kleur               5,455,121 ops/sec  fast
  chalk               4,428,884 ops/sec  fast
  kleur/colors        2,074,111 ops/sec  slow
  colorette           1,874,506 ops/sec  slow
  ansi-colors         1,010,628 ops/sec  slow
  colors.js             640,101 ops/sec  too slow
  cli-color             305,690 ops/sec  too slow
  colors-cli            104,962 ops/sec  too slow
Enter fullscreen mode Exit fullscreen mode

Chained styles

colors.forEach((color) => c[color].bold.underline.italic('foo'));
Enter fullscreen mode Exit fullscreen mode
+ ansis               5,515,868 ops/sec  very fast
  chalk               1,234,573 ops/sec  fast
  kleur                 514,035 ops/sec  slow
  ansi-colors           158,921 ops/sec  too slow
  cli-color             144,837 ops/sec  too slow
  colors.js             138,219 ops/sec  too slow
  colors-cli             52,732 ops/sec  too slow
  kleur/colors  (not supported)
  colorette     (not supported)
  picocolors    (not supported)
Enter fullscreen mode Exit fullscreen mode

Nested calls

colors.forEach((color) => c[color](c.bold(c.underline(c.italic('foo')))));
Enter fullscreen mode Exit fullscreen mode
+ picocolors            942,592 ops/sec  very fast
  colorette             695,350 ops/sec  fast
  kleur                 648,195 ops/sec  fast
  kleur/colors          561,111 ops/sec  fast
  ansis                 558,575 ops/sec  fast
  chalk                 497,292 ops/sec  fast
  ansi-colors           260,316 ops/sec  slow
  colors.js             166,425 ops/sec  slow
  cli-color              65,561 ops/sec  too slow
  colors-cli             13,800 ops/sec  too slow
Enter fullscreen mode Exit fullscreen mode

Nested styles

c.red(
  `a red ${c.white('white')} red ${c.red('red')} red ${c.cyan('cyan')} red ${c.black('black')} red ${c.red(
    'red'
  )} red ${c.green('green')} red ${c.red('red')} red ${c.yellow('yellow')} red ${c.blue('blue')} red ${c.red(
    'red'
  )} red ${c.magenta('magenta')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red(
    'red'
  )} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red(
    'red'
  )} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.green(
    'green'
  )} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red(
    'red'
  )} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.magenta(
    'magenta'
  )} red ${c.red('red')} red ${c.red('red')} red ${c.cyan('cyan')} red ${c.red('red')} red ${c.red(
    'red'
  )} red ${c.yellow('yellow')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red(
    'red'
  )} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} message`
);
Enter fullscreen mode Exit fullscreen mode
+ picocolors            243,975 ops/sec  very fast
  colorette             243,139 ops/sec  very fast
  kleur/colors          234,132 ops/sec  very fast
  kleur                 221,446 ops/sec  very fast
  ansis                 211,868 ops/sec  very fast
  chalk                 189,960 ops/sec  fast
  ansi-colors           121,451 ops/sec  slow
  colors.js              89,633 ops/sec  too slow
  cli-color              41,657 ops/sec  too slow
  colors-cli             14,264 ops/sec  too slow
Enter fullscreen mode Exit fullscreen mode

Deep nested styles

c.green(
  `green ${c.cyan(
    `cyan ${c.red(
      `red ${c.yellow(
        `yellow ${c.blue(
          `blue ${c.magenta(`magenta ${c.underline(`underline ${c.italic(`italic`)} underline`)} magenta`)} blue`
        )} yellow`
      )} red`
    )} cyan`
  )} green`
);
Enter fullscreen mode Exit fullscreen mode
+ colorette           1,131,757 ops/sec  very fast
  picocolors          1,002,649 ops/sec  very fast
  ansis                 882,220 ops/sec  fast
  chalk                 565,965 ops/sec  fast
  kleur/colors          478,547 ops/sec  fast
  kleur                 464,004 ops/sec  fast
  colors.js             451,592 ops/sec  fast
  ansi-colors           362,733 ops/sec  slow
  cli-color             213,441 ops/sec  slow
  colors-cli             40,340 ops/sec  too slow

Enter fullscreen mode Exit fullscreen mode

HEX colors

Only two libraries supported truecolor methods: ansis and chalk

c.hex('#FBA')('foo');
Enter fullscreen mode Exit fullscreen mode
+ ansis               4,944,572 ops/sec  very fast
  chalk               2,891,684 ops/sec  fast
  colors.js             (not supported)
  colorette             (not supported)
  picocolors            (not supported)
  cli-color             (not supported)
  colors-cli            (not supported)
  ansi-colors           (not supported)
  kleur/colors          (not supported)
  kleur                 (not supported)
Enter fullscreen mode Exit fullscreen mode

Conclusion

There is definitely no fastest, smallest and most functional library.
But we can identify 3 main leaders:

  • picocolors smallest and fastest for simple use cases
  • chalk most popular and fast for commonly use cases
  • ansis small, very fast (faster than chalk) and powerful for simple and complex use cases

Don't be afraid to be modern and experiment with new things.

P.S.

The ansis is not as popular (yet) as chalk, but give a chance to a new, modern, powerful library. Just try this in your new project.

import { black, cyan, green, inverse, red } from 'ansis/colors';

let out = green`Hello ${inverse`ANSI`} World!
${black.bgYellow`Warning:`} ${cyan`file.js`} ${red`not found!`}`;

console.log(out);
Enter fullscreen mode Exit fullscreen mode

The output:

screenshot "Hello ANSI World!"

View and edit this example in browser:

Open in StackBlitz

Top comments (1)

Collapse
 
thestoicdeveloper profile image
The Stoic Developer

That is a very comprehensive comparison! Shout out to Sindre's other color library that competes with picocolors, github.com/sindresorhus/yoctocolors