---
title: 'worker-http v21.2.0: TOON serializer — 30–60% smaller postMessage payloads for uniform arrays'
publishedAt: '2026-04-26'
tags: ['worker-http', 'toon', 'serialization', 'performance', 'angular', 'postMessage']
excerpt: 'Most API responses are uniform arrays of objects, and most of those bytes are repeated keys. We added TOON (Token-Oriented Object Notation) as a first-class serializer for the worker↔main boundary. The auto-serializer now picks it automatically when it makes sense — and falls back to structured-clone or seroval when it does not.'
---
# worker-http v21.2.0: TOON serializer — 30–60% smaller postMessage payloads for uniform arrays
Most API responses your app fetches are arrays of similarly-shaped objects: `User[]`, `Product[]`, paginated lists, audit logs, time-series rows. And most of the bytes in those JSON arrays are **repeated keys**:
```json
[
{ "id": 1, "name": "Alice", "role": "admin" },
{ "id": 2, "name": "Bob", "role": "member" },
{ "id": 3, "name": "Carol", "role": "member" }
]
```
`id`, `name`, and `role` appear in every row. The structure is fully redundant — a CSV-style table would carry the same information in a fraction of the bytes.
That is exactly what **TOON** ([Token-Oriented Object Notation](https://toonformat.dev)) does, and as of `@angular-helpers/worker-http@21.2.0` it is a first-class serializer for the worker↔main `postMessage` boundary.
---
## The problem
The serializer entry point already shipped two strategies:
- `structuredCloneSerializer` — zero overhead, default
- `createSerovalSerializer()` — full type fidelity (`Date`, `Map`, `Set`, circular refs)
Plus `createAutoSerializer()`, which routes between them based on a depth-1 type check.
But there was a third common case neither covered well: **uniform arrays of plain objects**. Structured clone copies every key string for every row. seroval does the same, just with extra type metadata. For a 1000-row `User[]`, that is a meaningful chunk of the structured-clone budget — and remember, `postMessage` runs on the main thread.
The package had `'toon'` declared in `SerializerStrategy` and the keyword list since day one. The implementation was missing.
---
## The objective
Ship `createToonSerializer()` as **opt-in directly** and as an **automatic branch of `createAutoSerializer()`** — without breaking any existing behaviour, without bloating non-consumers' bundles, and without false positives that would mangle non-uniform payloads.
---
## API surface
Show factory + auto-detection examples
### Direct usage
```typescript
import { createToonSerializer } from '@angular-helpers/worker-http/serializer';
const serializer = await createToonSerializer();
const payload = serializer.serialize([
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'member' },
{ id: 3, name: 'Carol', role: 'member' },
{ id: 4, name: 'Dave', role: 'guest' },
{ id: 5, name: 'Eve', role: 'admin' },
]);
// payload.data is the TOON string:
// [5]{id,name,role}:
// 1,Alice,admin
// 2,Bob,member
// 3,Carol,member
// 4,Dave,guest
// 5,Eve,admin
```
Round-trips losslessly via `serializer.deserialize(payload)`.
### Auto-detection
```typescript
const auto = await createAutoSerializer();
auto.serialize(users); // 1000 uniform users → format: 'toon'
auto.serialize(user); // single object → format: 'structured-clone'
auto.serialize(state); // contains a Date → format: 'seroval'
```
The routing priority (top-down, first match wins):
1. `hasComplexType(data)` → seroval
2. `isUniformObjectArray(data)` (length ≥ 5, primitive values, identical key set) → toon
3. Else → structured-clone
---
## Decisions and tradeoffs
Threshold, depth-1 strictness, peer dep, fidelity
### Conservative threshold (length ≥ 5)
TOON's encoding overhead — header, length, key declaration, comma escaping — is real. For a 2-element array it can produce a _larger_ string than JSON. We picked **5** as the minimum after looking at typical payload shapes:
- Arrays of 1–4 items are usually one-shot fetches (a single resource and its dependencies)
- Arrays of 5+ are usually lists, search results, or paginated batches
- The transition point where TOON starts winning is in that range
The constant is exported as `MIN_UNIFORM_ARRAY_LENGTH` for consumers who want to override it via custom auto-routing.
### Strict depth-1 detection
The detection helper is intentionally pure and shallow:
- Items must be plain non-null objects (no arrays, no Dates, no Maps)
- Item key sets must be identical
- All values must be primitives (string, number, boolean, null)
This means **no false positives**. If TOON cannot represent the payload tabularly, the auto-serializer leaves it on structured-clone. Consumers with deeply nested data still use seroval directly.
### Optional peer dependency
`@toon-format/toon` is declared as an **optional peer dependency**. The factory uses dynamic import via a variable assignment so neither Vite nor the Angular CLI eagerly resolves it. Bundles for consumers who never call `createToonSerializer()` are unchanged.
If the auto-serializer detects a uniform array but the peer is missing, it silently falls back to structured-clone — no crash, just the previous behaviour.
### Complex types win
When a payload contains both a uniform array and a `Date` at depth-1, **seroval wins**. TOON only encodes the JSON data model; preserving fidelity is a stronger requirement than compactness.
---
## What is NOT in scope
- **TOON over the wire**: this serializer governs the worker↔main boundary only. The HTTP response is still JSON. TOON is applied after the worker parses the response, before postMessage.
- **Date/Map/Set inside TOON rows**: TOON is JSON-shaped; complex types stay on seroval.
- **Schema validation, type generation, custom delimiters**: out of scope. Use `@toon-format/toon` directly if you need those.
- **Auto-routing for non-uniform arrays or arrays < 5 items**: structured-clone is still the right answer for those.
---
## Verifying the win
Reproducible benchmark snippet
```typescript
const data = Array.from({ length: 50 }, (_, i) => ({
id: i,
name: `user-${i}`,
role: 'member',
active: true,
}));
const toon = await createToonSerializer();
const payload = toon.serialize(data);
console.log('JSON size:', JSON.stringify(data).length);
console.log('TOON size:', (payload.data as string).length);
// JSON size: 1948
// TOON size: 736
// → ~62% smaller
```
The exact ratio depends on key length, value distribution, and array size. For deeply nested payloads it stays out of TOON's path. For uniform `User[]`-style responses, the schema-once savings dominate.
---
## Try it
The demo app now ships **two new comparison cards** at `/demo/worker-http`:
1. **Serializer comparison** — pick a sample dataset and watch the auto-serializer pick TOON, seroval, or structured-clone, with output size and encoding time side-by-side.
2. **Worker vs HttpClient** — fire the same request from `HttpClient` (main thread) and `WorkerHttpClient` (worker pool), with a live RAF jank counter showing the main thread staying responsive.
Numbers vary by hardware and payload, but the trend is robust: uniform arrays compress hard, and the worker keeps the main thread free regardless of which serializer is in play.
---
## Upgrade
Install commands and explicit opt-in
```bash
npm install @angular-helpers/worker-http@21.2.0
# Optional, only if you want TOON
npm install @toon-format/toon
```
No code changes required. If you use `createAutoSerializer()`, uniform arrays of 5+ plain-object items will start routing through TOON automatically once the peer dep is installed.
If you prefer to opt in explicitly:
```typescript
provideWorkerHttpClient(
withWorkerConfigs([{ id: 'api', workerUrl: new URL('./workers/api.worker', import.meta.url) }]),
withWorkerSerialization(await createToonSerializer()),
);
```
---
## Related
- [TOON spec](https://toonformat.dev) and [`@toon-format/toon`](https://www.npmjs.com/package/@toon-format/toon) on npm
- Previous post: [worker-http v1.0.0: The Journey from Proof-of-Concept to Production](/blog/worker-http-v1-0)
- Architecture deep-dive: [SDD — Angular HTTP over Web Workers](https://github.com/Gaspar1992/angular-helpers/blob/main/docs/sdd-angular-http-web-workers.md)