Compression
此内容尚不支持你的语言。
Object storage supports at-rest compression — bodies are compressed before put, decompressed on get. Three modes; pick the CPU-vs-size trade that fits the payload.
import { ObjectStorageDurableStateStore, S3ObjectStorageBackend,} from 'actor-ts';
const store = new ObjectStorageDurableStateStore({ backend: new S3ObjectStorageBackend({ /* ... */ }), compression: { algorithm: 'zstd', level: 6, // optional; zstd 1–22 (default 3) },});Now every persisted state is zstd-compressed before upload. Reads transparently decompress.
Algorithms
Section titled “Algorithms”| Algorithm | Compression ratio | CPU cost | When |
|---|---|---|---|
zstd | Best | Low–moderate (decode very fast) | Large state blobs — best ratio at competitive speed. |
gzip | Good (typical 50-70 %) | Moderate | Universal default — node:zlib, works on every runtime. |
none | None | Zero | Already-compressed or tiny payloads (see caveat below). |
The store default is gzip — it needs no native runtime
support and no extra dependency. For text-heavy state
(JSON), both gzip and zstd reach ~70 % reduction; zstd gets there
faster and decompresses quicker. For already-compressed data
(encrypted bytes, image data), compression is nearly free of
value and wastes CPU — use none.
There is no
brotliordeflatemode — onlynone,gzip, andzstd.
Configuration
Section titled “Configuration”type CompressionAlgo = 'none' | 'gzip' | 'zstd';
interface CompressionConfig { algorithm: CompressionAlgo; level?: number; // algorithm-specific; clamped; undefined = default}Levels are algorithm-specific. Out-of-range values are
clamped; omitting level uses the implementation default:
- gzip: 0 (no compression) – 9 (best). Default 6.
- zstd: 1 (fastest) – 22 (best). Default 3.
- none: ignored.
For most workloads the defaults are fine — bump only when storage cost dominates.
Runtime support
Section titled “Runtime support”zstd support differs by direction:
- Compress (write) — native only: Bun (
Bun.zstdCompressSync) or Node ≥ 22.15 (zlib.zstdCompressSync). There is no pure-JS fallback for writing; the optionalfzstdpeer dependency is decompress-only. Selectingzstdon a runtime without native support fails fast — eagerly at plugin registration, not cryptically on first persist. - Decompress (read) — native first, then the optional
fzstdpeer-dep, so a runtime without native zstd can still read zstd bodies written elsewhere.
gzip uses node:zlib and works everywhere (Bun, Node, Deno) with
no extra dependency.
How it works
Section titled “How it works”On put: serialize value → JSON bytes → compress(bytes) → [encrypt] → frame with ATS1 manifest → backend.put(framed)
On get: backend.get → framed bytes → read ATS1 manifest → [decrypt] → decompress per the manifest's algorithm flag → JSON → deserializeEvery body carries a small ATS1 manifest header; a 2-bit
field in its flags byte records the compression algorithm
(none / gzip / zstd). Decompression is driven entirely by
that flag — not by any HTTP header. (On S3 the framework also
sets Content-Encoding as a courtesy marker, but it is never the
source of truth for decoding.)
Because each body self-describes its algorithm, mixing none /
gzip / zstd bodies in the same bucket works — every object
decodes per its own manifest.
Changing the level
Section titled “Changing the level”The level is an encoder-only setting. It is not stored on the wire (the manifest records the algorithm, not the level), and the decompressor never needs it — gzip and zstd frames are self-describing.
So changing the configured level requires no migration: bodies written at the old level keep decoding, new bodies use the new level, and the two mix freely. You can also raise or lower the level per-actor at any time without touching existing data.
Compression + serialization
Section titled “Compression + serialization”serialize → JSON bytes → compress → [encrypt] → storeCompression runs after serialization and before encryption
— compressing ciphertext is pointless (it’s high-entropy), and the
fixed order also avoids CRIME-style side channels. For maximum
size reduction on large text-heavy state, zstd at a higher level
is usually the best single knob to turn.
When to use each
Section titled “When to use each”| Scenario | Algorithm |
|---|---|
| Large text-heavy state (big JSON, long descriptions) | zstd |
| Mixed text + numbers | zstd or gzip |
| Maximum portability, no native zstd guaranteed | gzip |
| Already-encrypted / already-compressed bytes | none (no benefit) |
| Storage-cost-bounded, write-once-read-many | zstd (level 15-19) |
| CPU-bounded write path | gzip level 1 or zstd level 1 |
CPU cost (rough guidance)
Section titled “CPU cost (rough guidance)”Single thread, ~100 KB blob — order-of-magnitude only; real numbers depend on payload and runtime:
- gzip level 6 — ~1-3 ms encode, ~0.5 ms decode.
- gzip level 1 — ~0.5 ms encode, fast decode.
- zstd level 3 (default) — encode comparable to gzip, better ratio, very fast decode.
- zstd level 19 — noticeably slower encode, decode still fast.
- zstd levels 20-22 (ultra) — much slower encode for marginal gains; reserve for write-once-read-many bulk data.
zstd’s asymmetry (cheap decode at every level) makes it a strong default for read-heavy state. For typical state-store workloads (small objects, modest write rate) the CPU cost is invisible.
Per-actor override
Section titled “Per-actor override”class SmallStateActor extends DurableStateActor<...> { protected compression() { return { algorithm: 'none' as const }; }}
class LogStateActor extends DurableStateActor<...> { protected compression() { return { algorithm: 'zstd' as const, level: 19 }; }}Override the store-level default per actor. Useful when:
- Most actors have small state (no compression needed).
- A few have large text-heavy state (high compression).
See Per-actor policies.
Where to next
Section titled “Where to next”- Object storage overview — the bigger picture.
- Encryption — the complementary at-rest feature.
- Per-actor policies — configuring compression per-actor.