zplCore.comzplCore.com

36Performance

zplCore.com generates 200,000+ real shipping labels per second (100 × 150 mm, 33 elements, QR + Code 128 + GS1-128) from dictionary or JSON records into a ZPL file – under JIT and NativeAOT, measured on an Apple M3 Max (14 cores), .NET 10, median of 5 runs, samples/ShippingBench.

Real shipping label (samples/ShippingBench)

Measured on 21 Oct 2026: Apple M3 Max (10 performance + 4 efficiency cores), macOS 26.6, .NET 10, Release, workstation GC; one warm-up run, then the median of 5 runs. End to end: records → ZPL → file on disk (StreamWriter, 64 KB buffer). All data sources write a byte-identical 131 MB file (SHA-256 checked before measuring), under JIT and NativeAOT alike.

The benchmark label: 100 × 150 mm shipping label with QR code, Code 128 and GS1-128 SSCC
Benchmark label, 203 dpi

The label: samples/templates/printjob.json, a 100 × 150 mm (4 × 6 in) shipping label with 33 elements – sender block, inverted service box, recipient (country via expression), routing code, weight, parcels, ship date (Format(ToDate([ShipDate]), "dd.MM.yyyy")), QR code (tracking URL), Code 128 (tracking number), GS1-128 SSCC, page counter, frames and rules – plus a start and an end label. The records vary like real data (umlauts, double names, 9 places in AT/DE/CH/IT, 6 services, 2.5–29 kg, valid SSCCs). Downloads: label as PDF / ZPL, print job (start + 3 shipments + end) as PDF / ZPL (300 dpi), image PNG 300 dpi.

200,000+shipping labels/s (dictionary/JSON, JIT and NativeAOT)
< 0.5 s100,000 shipping labels (dictionary 0.40 s, JSON 0.46 s)
1,000+PNG previews/s (14 cores, 203 dpi)
100,000+labels/s from C# objects (POCO, JIT)
Data source (100,000 labels + start/end → ZPL file)JIT labels/sNativeAOT labels/sTime
IReadOnlyDictionary<string, object?> records249,286238,5900.40–0.42 s
JSON records (JsonElement)216,171213,6370.46–0.47 s
POCO with Map() per field128,16591,7840.78–1.09 s
POCO, For<T>() (reflection)124,99583,9940.80–1.19 s
Stored format (^DF/^XF), POCO109,53172,1130.91–1.39 s
Rendering, 5,000 labels, 14 cores, 203 dpi: PNG preview1,2241,182
Rendering, 5,000 labels, 14 cores, 203 dpi: PCX (1 bit)1,058462

For mass data, pass dictionaries or JSON records: they stay above 200,000 labels/s under JIT and NativeAOT (slowest single run 208,068/s). POCO binding reaches about 125,000/s under JIT and 84,000/s under NativeAOT.

5 million records and Docker (NativeAOT): records as JSON Lines (1.56 GB, one record per line), read line by line, bound as JsonElement, streamed into a 6.53 GB ZPL file. Same Apple M3 Max; Docker Desktop runs the container in a VM, so file I/O to the volume is slower than native.

RunLabels/sTimePeak memory
NativeAOT on macOS, 5 million records132,16437.8 s248 MB RSS
Docker, NativeAOT linux-arm64, --memory 1g, 5 million records91,13354.9 s90 MB
Docker, 100,000 labels, dictionary / JSON records157,489 / 119,2710.64 / 0.84 s
Docker, PNG preview, 14 cores1,164

Memory stays constant for any number of records (a streamed source is neither counted nor buffered). Image build/Dockerfile.shippingbench: 127 MB, runtime-deps:10.0-noble-chiseled-extra, native binary only, non-root.

dotnet run -c Release --project samples/ShippingBench                       # JIT
dotnet publish samples/ShippingBench -c Release -r osx-arm64 -p:PublishAot=true -o out   # NativeAOT
docker build -f build/Dockerfile.shippingbench -t zplcore-shippingbench .
docker run --rm -v zplcore-bench:/data zplcore-shippingbench 1000 10 1 5000000   # + 5 million JSON Lines records
dotnet run -c Release --project samples/ShippingBench -- sample out/sample        # label as PNG/PDF/ZPL

Small 7-element label (samples/PerfTest)

samples/PerfTest generates 100,000 labels in a single print job (start label + 100,000 labels + end label, cut only after the last label) – from objects, streamed from EF Core/SQLite and from LiteDB. It uses a small 100 × 60 mm label with 7 elements (text, expression, number format, Code 128, counter), not the shipping label above. Measured in a Release build on 14 cores (Apple M3 Max, .NET 10), single run.

< 1 s100,000 labels as ZPL
14.7 MBjob file (stored format)
24–64 MBheap (streaming)
Test (100,000 labels, one job)TimeLabels/sFileHeap
Objects → ZPL, stored format (^DF/^XF)0.73 s136,00014.7 MB24 MB
Objects → ZPL, full format per label0.39 s256,00029.1 MB24 MB
EF Core / SQLite AsAsyncEnumerable() → file0.57 s176,00014.7 MB29 MB
LiteDB query → file0.50 s202,00014.7 MB64 MB
PNG preview, sequential (799 × 480 px, grayscale PNG)≈ 3.4 min49018.2 MB / 2,00084 MB
PNG preview, parallel (14 cores)≈ 25 s4,03317.1 MB / 2,000106 MB

Generating ZPL for the printer is practically free and streams with constant memory. The stored format halves the amount of data transferred.

The ZPL tests bind classes (Shipment objects) and therefore generate sequentially; dictionary, JSON and DataRow records are generated on all cores – see parallel job generation.

Render optimization

Black-and-white labels are written as 8-bit grayscale PNG (with anti-aliasing) or true 1-bit PNG (AntiAlias = false) instead of 32-bit RGBA (ZplRenderSettings.CompactPng, PngCompression Fastest/Optimal/SmallestSize). Simple ^GB boxes are drawn directly, and the label image is only copied when ^MC N needs it.

Per label (100 × 60 mm, 203 dpi)beforenow
Render to bitmap1.28 ms0.39 ms
Render + PNG8.19 ms1.86 ms
PNG size18 KB9 KB
100,000 previews in parallel2.2 min25 s

Parallel scaling

SkiaSharp serializes two things globally: Skia's glyph cache (every DrawText) and SkiaSharp's handle table (every newly created SKPaint/SKFont/SKBitmap – and DrawBitmap, which internally creates an SKImage every time). zplCore.com therefore rasterizes each glyph once per font/size into a lock-free cache of SKImage masks and draws text via DrawImage; fonts are passed through explicitly instead of being queried via SKFont.Typeface. Results measured with samples/RenderBench -- 150 scale (Apple M3 Max, 10 performance + 4 efficiency cores):

ThreadsRender + PNG beforeRender + PNG nowText only beforeText only now
1499/s (1.0×)549/s (1.0×)5,665/s (1.0×)6,306/s (1.0×)
41,984/s (4.0×)2,225/s (4.1×)3,770/s (0.7×)28,183/s (4.5×)
82,245/s (4.5×)4,090/s (7.5×)3,162/s (0.6×)42,434/s (6.7×)
101,826/s (3.7×)5,022/s (9.2×)3,150/s (0.6×)39,384/s (6.2×)
dotnet run -c Release --project samples/RenderBench -- 150 scale [label|text|barcode|box|empty]
dotnet run -c Release --project samples/RenderBench -- 500   # micro-benchmark per element
dotnet run -c Release --project samples/PerfTest -- 100000 perf-out

Web API throughput

ASP.NET Core minimal API from samples/WebApiDemo: 14 cores (Apple M3 Max), server GC, 14 workers, small 100 × 60 mm label at 203 dpi (not the shipping label above); client and server in the same process.

ScenarioLabels/sMB/sp50p95GC heap
JSON payload → ZPL, 1,000 records/request62,85720.316.2 ms16.7 ms178 MB
JSON payload → ZPL, 10,000 records/request112,48436.476.1 ms136.9 ms183 MB
JSON payload → ZPL, 100,000 records/request186,57260.5527.5 ms565.2 ms414 MB
DB streaming → ZPL, 10,000 records151,74449.169.6 ms73.1 ms361 MB
DB streaming → ZPL, 100,000 records199,10264.6504.3 ms505.2 ms372 MB
JSON 1k, 1 / 4 / 16 clients196,112 / 240,453 / 155,34463.4 / 77.7 / 50.24.8 / 16.4 / 100.8 ms8.6 / 21.7 / 120.8 ms404–419 MB
DB 10k, 4 / 16 clients215,699 / 179,86069.7 / 58.2186.2 / 878.0 ms202.4 / 907.6 ms395–408 MB
Preview, 50 labels/request: PDF 1 / 14 clients583 / 5,0855.0 / 43.587.0 / 131.8 ms94.0 / 154.4 ms267–472 MB
Preview, 50 labels/request: PNG (zip) 1 / 14 clients484 / 3,9954.6 / 38.1108.3 / 162.0 ms108.7 / 188.1 ms269–473 MB
Job queue: 256 × JSON 1k → ZPL (153 jobs/s)153,43849.698.8 ms365.5 ms885 MB
Job queue: 64 × DB 10k → ZPL (18.6 jobs/s)186,36160.32,236.5 ms3,411.1 ms828 MB
Job queue: 64 × JSON 50 → PDF (104 jobs/s)5,39646.2345.2 ms445.6 ms467 MB

ZPL generation saturates at about 199,000 labels/s (from 4 parallel requests); more clients only add latency. Rendering costs about 40× more and scales with the cores. Queue latency includes the wait time (all jobs submitted at once).

Memory and soak test

WorkloadLabelsMemory
Soak test, small labels, rendered in parallel1,000,000RSS 128–138 MB · GC heap 2–3 MB, flat (no leak)
Soak test, 4 × 6″ PNGs, 14 threads1,000,000RSS 438–481 MB, flat
ZPL streaming from objects100,000heap 24 MB
Streaming from EF Core / SQLite into a ZPL file100,000heap 29 MB
Streaming from LiteDB into a ZPL file100,000heap 64 MB

All figures are measurements with the benchmarks in the repository, not a guarantee.