Performance

Two ways to keep a hot path off the critical frame: time-slice a large computation so it does not block, and recycle allocations so a derivation that runs constantly stops churning the garbage collector.

@mmstack/primitivesnpm

#chunked, work spread over frames

You have a signal holding a large array and something downstream that is expensive per item: a @for that renders a heavy row, a computation that walks the whole list. Feeding all of it in at once means one long frame and a visible stall while the browser catches up.

chunked returns a signal that reveals the source array a slice at a time. The first read gives you the first chunkSize items right away, then it schedules the next slice on the next frame, and the next, until the whole array is present. Downstream work sees a growing list and stays responsive between slices instead of paying for everything on one tick.

import { signal } from '@angular/core';
import { chunked } from '@mmstack/primitives';

const rows = signal(loadThousandsOfRows());

// reveals 100 rows per frame instead of all at once
const visible = chunked(rows, { chunkSize: 100 });

// render @for over visible(); it grows across frames, no long stall

The default delay is 'frame' (a requestAnimationFrame between slices); pass a number of milliseconds or 'microtask' to change the cadence. It also takes the shared pause option, so under an *mmActivity boundary the scheduling stops while the subtree is hidden and resumes from the slice it reached, rather than churning through the list for a view nobody is looking at.

Reach for it when a list is large enough that mounting it in one go drops a frame. For a list that fits comfortably in a frame this only adds latency to the tail, so leave it off.

#Object pools for hot derivations

A computed that builds a fresh array, Set, or Map every time it runs allocates a new container on every recomputation. For a derivation that fires many times a second (a filter over a live list, an aggregate recomputed on every keystroke) that steady allocation is pressure the garbage collector has to clean up later, in pauses you do not control.

pooled backs a signal with a two-slot buffer pool. It allocates the container at most twice over the whole life of the signal and swaps between the two on each recompute, resetting the dirty one before your computation writes into it. Consecutive reads still return different instances, so ordinary Object.is equality keeps flagging changes; you just stop minting a new object each time.

import { signal } from '@angular/core';
import { pooled } from '@mmstack/primitives';

const items = signal<{ active: boolean }[]>([]);

const counts = pooled<{ total: number; active: number }>({
  create: () => ({ total: 0, active: 0 }),
  reset: (c) => {
    c.total = 0;
    c.active = 0;
  },
  computation: (c) => {
    for (const item of items()) {
      c.total++;
      if (item.active) c.active++;
    }
    return c;
  },
});

The tradeoff is the retention contract: the value a pooled signal hands you is only valid until its next recomputation, when the container is recycled and overwritten. Do not store it, hand it to async code, or pass it to anything that outlives the current reactive tick. Read it, derive from it, and let it go.

The common containers have presets so you skip the create/reset boilerplate: pooledArray, pooledMap, and pooledSet each supply their own factory and reset (length = 0 for arrays, .clear() for the rest). Pass a computation function for the common case, or the full options object when you need eager pre-allocation or a custom container.

import { signal } from '@angular/core';
import { pooledArray } from '@mmstack/primitives';

const items = signal<{ id: number; active: boolean }[]>([]);

// recycles one array per slot; just write into the buffer and return it
const activeIds = pooledArray<number[]>((buf) => {
  for (const item of items()) if (item.active) buf.push(item.id);
  return buf;
});

Use a pool only where the allocation actually shows up in a profile: a hot, high-frequency derivation whose output is consumed and dropped within the same tick. For anything read once and kept, a plain computed is simpler and the retention contract does not get in your way.