Skip to main content

Flyweight Pattern

Share common state across many similar objects to minimize memory usage.


The Problem​

You're building a text editor. Each character could be an object with font, size, color, and position. A million characters = a million objects = memory explosion.

The naive approach:

class Character {
constructor(
public char: string,
public font: string, // "Arial" repeated millions of times
public size: number, // 12 repeated millions of times
public color: string, // "#000000" repeated millions of times
public x: number,
public y: number
) {}
}

// Million characters = million copies of "Arial", 12, "#000000"

Intrinsic state (font, size, color) is duplicated. Massive memory waste.


The Solution​

Share intrinsic state. Store only extrinsic state (position) per instance:

import { memoize } from "@pithos/core/arkhe";

// Flyweight factory - returns shared style objects
const getStyle = memoize((font: string, size: number, color: string) => ({
font,
size,
color,
}));

// Characters only store position + reference to shared style
interface Character {
char: string;
style: ReturnType<typeof getStyle>; // shared!
x: number;
y: number;
}

// Million characters, but only a few unique styles
const char1: Character = {
char: "H",
style: getStyle("Arial", 12, "#000000"), // shared
x: 0,
y: 0,
};

const char2: Character = {
char: "i",
style: getStyle("Arial", 12, "#000000"), // same reference β€” not a copy
x: 10,
y: 0,
};

char1.style === char2.style; // true β€” same object in memory

Styles are shared via memoization. Memory usage drops dramatically.


Live Demo​

Type text, change styles with the presets, and watch the counters. Toggle Flyweight on and off - the memory bar shows the difference in real time.

Code
/**
* Flyweight pattern β€” style object pool via memoize.
*
* Same args = same object reference (shared intrinsic state).
* Without flyweight: every character gets its own copy.
*/

import { memoize } from "@pithos/core/eidos/flyweight/flyweight";
import type { CharStyle, EditorChar, EditorStats } from "./types";

/** Memoized style factory β€” same args = same object reference */
const memoizedCreateStyle = memoize(
(font: string, size: number, color: string): CharStyle => ({ font, size, color }),
(font: string, size: number, color: string) => `${font}|${size}|${color}`,
);

export function getSharedStyle(font: string, size: number, color: string): CharStyle {
return memoizedCreateStyle(font, size, color);
}

export function getCopiedStyle(font: string, size: number, color: string): CharStyle {
return { font, size, color };
}

export function resetPool(): void { memoizedCreateStyle.cache.clear(); }

const STYLE_OBJECT_SIZE = 64;

export function computeStats(chars: EditorChar[], useFlyweight: boolean): EditorStats {
const totalChars = chars.length;
if (totalChars === 0) return { totalChars: 0, styleObjects: 0, memoryBytes: 0, savedPercent: 0 };

if (useFlyweight) {
const refs = new Set(chars.map((c) => c.style));
const styleObjects = refs.size;
const memoryBytes = styleObjects * STYLE_OBJECT_SIZE;
const wouldBe = totalChars * STYLE_OBJECT_SIZE;
return { totalChars, styleObjects, memoryBytes, savedPercent: wouldBe > 0 ? Math.round(((wouldBe - memoryBytes) / wouldBe) * 100) : 0 };
}

return { totalChars, styleObjects: totalChars, memoryBytes: totalChars * STYLE_OBJECT_SIZE, savedPercent: 0 };
}

export function formatBytes(bytes: number): string {
if (bytes === 0) return "0 B";
if (bytes < 1024) return `${bytes} B`;
return `${(bytes / 1024).toFixed(1)} KB`;
}
Result

Real-World Analogy​

A font file. Your computer doesn't store a separate "A" image for every "A" on screen. It stores one "A" glyph and reuses it everywhere, just changing position and size.


When to Use It​

If you're creating thousands of objects that share most of their data (game entities, DOM nodes, text characters, map tiles), extract the shared part into a memoized factory. The more duplicates, the bigger the win.


When NOT to Use It​

If each object is unique with no shared state, there's nothing to pool. Flyweight adds a lookup cost that only pays off when many objects share the same intrinsic data.


In Functional TypeScript​

Flyweight is essentially memoization. memoize from Arkhe handles the pooling:

import { memoize } from "@pithos/core/arkhe";

// Any function that creates objects can become a flyweight factory
const getColor = memoize((r: number, g: number, b: number) => ({ r, g, b }));

// Same arguments = same object
getColor(255, 0, 0) === getColor(255, 0, 0); // true

API​

  • memoize β€” Cache function results, effectively creating an object pool