Encoders
zstd at three levels against brotli on the shared board, and the heap each encoder holds per stream: the bytes on the wire, the CPU and the memory each setting trades.
Every stream holds an encoder for as long as its tab is open, so an encoder costs three things: bytes on the wire per frame, CPU per frame, and heap per stream. sse.Options.ZstdLevel, ZstdWindow, ZstdLowMemory, BrotliQuality and BrotliWindow choose among them.
On the shared board
BenchmarkBoardZstdLevels is the shared board with every stream on zstd, at the fastest level (lith's default), the default level and the better-compression level. Measured on 2026-10-13, medians of three runs; brotli from BenchmarkSharedBoard the same day:
| Encoder | Bytes per frame | Cores busy | Heap per stream | Tick p99 |
|---|---|---|---|---|
| zstd fastest (lith's default) | 2,266 (2,183 to 2,283) | 1.32 (1.05 to 1.41) | 1.68 MiB | 5.2 ms |
| zstd default | 1,138 (1,087 to 1,153) | 1.07 (1.07 to 1.15) | 2.76 MiB | 2.5 ms |
| zstd better compression | 365 (364 to 366) | 3.11 (3.08 to 3.20) | 5.56 MiB | 2.8 ms |
| brotli quality 4 | 955 | 3.31 | 2.13 MiB | 2.7 ms |
- zstd's default level halves the bytes of the fastest for no more CPU in these runs, and holds 1.1 MiB more per stream: about 220 MiB more for 200 tabs.
- Better compression sends a third of the default's bytes, fewer than brotli, for about brotli's CPU and two and a half times its heap.
- Every run sent 3,920 to 3,969 frames a second and dropped none. The fastest level varied the most between runs, as other agents' work on the machine changed.
Heap per stream
TestHeapPerStream (sse/heap_test.go) measures the encoders on one core, without the render loop: 200 frames of a 57 KB page, of which five rows change from one frame to the next, to each of 20 streams. Three runs gave the same bytes each time:
| Encoder | Bytes per frame | Time per frame | Heap per stream |
|---|---|---|---|
| zstd fastest | 405 | 36 to 41 µs | 1.65 MiB |
zstd fastest, ZstdLowMemory |
405 | 55 to 58 µs | 1.02 MiB |
| zstd default | 228 | 31 µs | 2.73 MiB |
| zstd better | 108 | 184 to 201 µs | 5.54 MiB |
| brotli quality 4 | 121 | 251 to 265 µs | 2.02 MiB |
- zstd keeps a history of twice its window, 1 MiB for the 512 KiB window.
ZstdLowMemorykeeps it at the window: 0.63 MiB less per stream, for half again the CPU per frame here, and twice with 200 streams, whose encoders no longer fit in the CPU's cache. - Datastar's Go SDK, given
WithBrotliwithoutWithBrotliLGWin, keeps brotli's 4 MiB window: 9.9 MiB of heap per stream, measured the same way at quality 5. 1,000 open tabs hold about 9.7 GiB there, against 1.0 GiB with zstd andZstdLowMemory, or 1.6 GiB with lith's default.
Choosing
- Memory is the constraint for most apps: an open tab holds its encoder for as long as it stays open. The stream budget decides the window per stream from the memory there is.
- lith's default is zstd's fastest level at 2^19, 1.65 MiB a stream. Where bandwidth costs more than memory,
ZstdLevel: zstd.SpeedDefaulthalves the bytes for 1.1 MiB more a stream. The budget's tiers are sized for the fastest level, though: at another level a stream holds more than the budget reserves for it, so leave room under the limit. - brotli serves the browsers without zstd, Safari before 26.3 among them. Its CPU per frame is four to six times zstd's.
Measuring again
taskset -c 0-11 go test ./loadtest -run '^$' -bench BoardZstdLevels -count 3
go test ./sse -run TestHeapPerStream -v -count 3