Flyweight
The Flyweight pattern shares immutable state between many objects to keep memory usage bounded. Most useful when you have huge numbers of similar objects (text glyphs, particle systems, tile maps) where storing per-instance copies of duplicated data would crush memory.
Intrinsic vs extrinsic, factory, pools
EXAMPLE
// 1) Problem — many objects, mostly identical state
class Tree {
constructor(x, y, species, mesh, texture, animation) {
this.x = x; this.y = y;
this.species = species;
this.mesh = mesh; // heavy data — geometry
this.texture = texture; // heavy data — image
this.animation = animation; // heavy data — bone data
}
}
// A forest of 100,000 trees stores 100,000 copies of mesh + texture + animation.
// If those are 5 MB each, that's 500 GB of references … but the underlying data is identical.
// 2) Flyweight — split intrinsic (shared) from extrinsic (per-instance) state
class TreeType { // INTRINSIC — shared
constructor(species, mesh, texture, animation) {
this.species = species;
this.mesh = mesh;
this.texture = texture;
this.animation = animation;
}
draw(ctx, x, y, age) {
ctx.drawMesh(this.mesh, x, y, this.texture, this.animation, age);
}
}
class Tree2 { // EXTRINSIC — unique per instance
constructor(x, y, age, type) {
this.x = x; this.y = y;
this.age = age;
this.type = type; // shared reference
}
draw(ctx) { this.type.draw(ctx, this.x, this.y, this.age); }
}
// 3) Factory caches the heavy intrinsic objects
class TreeTypeFactory {
static cache = new Map();
static get(species, mesh, texture, animation) {
const key = `${species}|${mesh}|${texture}|${animation}`;
if (!this.cache.has(key)) {
this.cache.set(key, new TreeType(species, mesh, texture, animation));
}
return this.cache.get(key);
}
}
const forest = [];
for (let i = 0; i < 100_000; i++) {
const species = ['oak', 'pine', 'birch'][i % 3];
const type = TreeTypeFactory.get(species, `meshes/${species}.gltf`, `tex/${species}.png`, `anim/sway.json`);
forest.push(new Tree2(Math.random() * 1000, Math.random() * 1000, Math.random() * 50, type));
}
console.log('unique types:', TreeTypeFactory.cache.size); // 3
console.log('tree count:', forest.length); // 100000
// Memory dropped from 100k * (mesh+tex+anim) to 3 * (mesh+tex+anim) + 100k * (x,y,age,ref).
// 4) Real-world: character glyphs in a text renderer
class Glyph { // INTRINSIC
constructor(codepoint, font, size, raster) {
this.codepoint = codepoint;
this.font = font;
this.size = size;
this.raster = raster; // bitmap or vector
}
}
class GlyphFactory {
static cache = new Map();
static get(codepoint, font, size) {
const key = `${codepoint}|${font}|${size}`;
if (!this.cache.has(key)) this.cache.set(key, new Glyph(codepoint, font, size, rasterize(codepoint, font, size)));
return this.cache.get(key);
}
}
class TextRun { // EXTRINSIC
constructor(x, y, codepoint, font, size, color) {
this.x = x; this.y = y; this.color = color;
this.glyph = GlyphFactory.get(codepoint, font, size);
}
}
// 5) Particle system — millions of particles, a handful of types
class ParticleType {
constructor(sprite, lifetime, blend) {
this.sprite = sprite;
this.lifetime = lifetime;
this.blend = blend;
}
}
class Particle {
constructor(x, y, vx, vy, age, type) {
this.x = x; this.y = y; this.vx = vx; this.vy = vy; this.age = age; this.type = type;
}
}
// Add Structure-of-Arrays for max performance: keep all x in one Float32Array, all y in another, etc.
class ParticleSystem {
constructor(capacity) {
this.x = new Float32Array(capacity);
this.y = new Float32Array(capacity);
this.vx = new Float32Array(capacity);
this.vy = new Float32Array(capacity);
this.age = new Float32Array(capacity);
this.typeId = new Uint16Array(capacity);
this.types = []; // intrinsic, max 65535 types
this.count = 0;
}
spawn(x, y, vx, vy, typeId) {
const i = this.count++;
this.x[i] = x; this.y[i] = y; this.vx[i] = vx; this.vy[i] = vy;
this.age[i] = 0; this.typeId[i] = typeId;
}
}
// 6) Map tiles — Mario / tile-based games
class Tile { // INTRINSIC
constructor(name, walkable, sprite) {
this.name = name; this.walkable = walkable; this.sprite = sprite;
}
}
const TILE = {
grass: new Tile('grass', true, 'sprites/grass.png'),
wall: new Tile('wall', false, 'sprites/wall.png'),
water: new Tile('water', false, 'sprites/water.png'),
};
// A 1024 x 1024 map stores 1M references to ~3 Tile objects.
// 7) Java's Integer.valueOf — the classic JDK flyweight
// Integer i = Integer.valueOf(42); → same cached object every call for -128 to 127
// String interning (intern()) is similar — shared String objects for identical literals
// 8) Combine with Object Pool for ALSO bounded EXTRINSIC count
// Flyweight reduces memory for SHARED state; object pool reduces allocation churn for
// short-lived extrinsic instances.
class BulletPool {
constructor(size) {
this.pool = [];
for (let i = 0; i < size; i++) this.pool.push({ x: 0, y: 0, vx: 0, vy: 0, type: null, alive: false });
}
spawn(x, y, vx, vy, type) {
const b = this.pool.find((p) => !p.alive);
if (!b) return null;
b.x = x; b.y = y; b.vx = vx; b.vy = vy; b.type = type; b.alive = true;
return b;
}
despawn(b) { b.alive = false; }
}
// 9) When to use flyweight
// • Many instances (10k+) where most state is duplicated
// • Read-heavy intrinsic state — no per-instance mutation
// • Memory is genuinely the constraint (profile first!)
// • The split between intrinsic and extrinsic is clear
// 10) When NOT to use flyweight
// • Each object has unique state — nothing to share
// • Object count is small (hundreds) — micro-optimisation territory
// • You'd have to make state mutable on the shared object — that breaks the pattern
// • Cache lookup cost dominates — for VERY tiny structs the indirection isn't worth it
// 11) Concurrency notes
// • Intrinsic state MUST be immutable (or thread-safe to read concurrently)
// • Factory should be thread-safe (ConcurrentHashMap, Map with synchronized blocks)
// • If you ever 'almost-mutate' the shared object, the pattern breaks invisibly
// 12) Common bugs
// • Mutating intrinsic state in one instance → all sharing instances change
// • Factory key forgets a field → wrong shared object reused (e.g. font but not size)
// • Cache grows unbounded for genuinely unique keys → LRU bound the cache
// • Forgetting to despawn pooled objects → leaks; pair flyweight with object pool
// • Premature use — flyweight adds complexity; measure memory pressure first
Why it matters
Use Flyweight when many objects duplicate the same heavy state — tile sets, glyphs, particle types. Split intrinsic (shared, immutable) from extrinsic (unique per instance), gate creation through a factory cache, and never mutate the shared half. Pair with an object pool if instance allocation churn is also a problem.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Share immutable parts across many objects to save memory. // Classic example: glyph rendering, where many characters share the same font face.Try it Yourself »
Discussion
Loading…