Saturon LogoSaturon

Tree Shakability & Bundle Size

Optimize your bundle size by using Saturon's standalone functional subpaths.

Saturon is designed to be highly modular. While the Color class provides a convenient, chainable, object-oriented API, importing it bundles the entire class and everything it depends on.

If you are building for the web and need strict bundle-size optimizations, Saturon lets you bypass the Color class entirely. The logic behind the class's methods is also exposed as standalone functions through utility subpaths.

The Trade-off: OOP vs. Functional

Methods live on a class's prototype, so your bundler (like Webpack, Rollup, or Vite) generally can't remove the ones you don't call. Importing Color therefore pulls in the whole library.

By importing specific subpaths, you adopt a functional style instead. Saturon ships as ES modules and declares sideEffects: false, so your bundler can drop any Saturon code that your imports don't reach. How much that saves depends on which subpaths you pick: see Measured bundle sizes.


Code Comparison

Let's look at a common workflow: parsing a color, converting it to LCH, lightening it, and formatting it as a CSS string.

1. The Object-Oriented Way (Convenient, but larger bundle)

This approach is highly readable and quick to write, but it pulls in the full Color class.

import { Color } from "saturon";

const myColor = Color.from("red")
    .in("lch")
    .with({ l: (l) => l + 20 })
    .toString();

console.log(myColor); // lch(...)

2. The Tree-Shakable Way (Smaller bundle)

By importing only the specific utilities you need, your bundler can leave out everything those utilities don't depend on. Each utility is a named export from its own subpath.

import { parse } from "saturon/parse";
import { convert } from "saturon/convert";
import { updateColor } from "saturon/updateColor";
import { format } from "saturon/format";

// 1. Parse the initial string into raw color data
const raw = parse("red");

// 2. Convert to the target color space
const lchCoords = convert(raw, "lch");

// 3. Update the lightness channel
const lighterLchCoords = updateColor({ model: "lch", coords: lchCoords }, { l: (l) => l + 20 });

// 4. Serialize back to a CSS string
const result = format({ model: "lch", coords: lighterLchCoords }, "lch");

console.log(result); // lch(...)

This example never touches the Color class, isValid, interpolate, scale, contrast, random, or the registry, so none of that code can end up in your bundle. It does import parse, though, and parsing depends on much of the library: the grammar, calc() support, color-mix(), and gamut fitting. Statically, the four imports above reach 25 of Saturon's 32 modules. If bundle size is the goal, import saturon/convert and the other lightweight subpaths where you can, and measure your own build.

Tip for modern JS

Because the subpaths are plain functions that take the color data as their first argument, they are easy to chain with functional composition helpers, or with the proposed JavaScript pipeline operator (|>). That operator is not part of the language yet and needs a transpiler, and the % placeholder shown here may change.

import { parse } from "saturon/parse";
import { convert } from "saturon/convert";
import { updateColor } from "saturon/updateColor";
import { format } from "saturon/format";

const result = "red"
  |> parse(%)
  |> convert(%, "lch")
  |> updateColor({ model: "lch", coords: % }, { l: (l) => l + 20 })
  |> format({ model: "lch", coords: % }, "lch");

console.log(result); // lch(...)

Measured bundle sizes

Production build with Vite 8 and Saturon 0.4.4, using the entry files in apps/demo. Sizes are in kB (1 kB = 1,000 bytes).

ImportSaturon modulesRawgzipbrotli
Color class (saturon)32118.4035.2831.04
convert (saturon/convert)518.835.514.90

Each entry re-exports a single name: export { Color } from "saturon" for the first row and export { convert } from "saturon/convert" for the second. Importing convert alone is about 6× smaller than importing Color.

How to read these numbers

  • This is the best case. convert has few dependencies. A pipeline that also imports parse, like the example above, reaches most of the library and will be larger.

  • The output is not minified. The demo uses a Vite library build, whose output keeps readable code, so absolute sizes are larger than what you would ship. Compare the relative sizes, not the absolute ones.

  • gzip and brotli are summed per module. The figures come from rollup-plugin-visualizer, which compresses each module separately. Compressing the final file as one stream gives slightly smaller numbers.

Reproduce the measurements

From the repository root:

pnpm install
pnpm build:lib
pnpm build:demo

This writes stats-full.html and stats-utils.html in apps/demo, one treemap per build. The entry files are apps/demo/src/full.ts and apps/demo/src/utils.ts.


Exploring Available Subpaths

You can build almost any color manipulation pipeline using just the utility subpaths. From calculating contrast to interpolating gradients, everything is available as a direct import.

To see the complete list of available utilities and how to use them, refer to the Utility Subpaths API Reference.

On this page