36Performance
zplCore.com erzeugt über 200.000 echte Versandetiketten pro Sekunde (100 × 150 mm, 33 Elemente, QR + Code 128 + GS1-128) aus Dictionary- oder JSON-Datensätzen in eine ZPL-Datei – unter JIT und NativeAOT, gemessen auf einem Apple M3 Max (14 Kerne), .NET 10, Median aus 5 Läufen, samples/ShippingBench.
Echtes Versandetikett (samples/ShippingBench)
Gemessen am 21.10.2026: Apple M3 Max (10 Performance- + 4 Effizienzkerne), macOS 26.6, .NET 10, Release, Workstation-GC; ein Aufwärmlauf, dann der Median aus 5 Läufen. Ende zu Ende: Datensätze → ZPL → Datei auf der Platte (StreamWriter, 64-KB-Puffer). Alle Datenquellen schreiben eine byte-identische Datei mit 131 MB (vor der Messung per SHA-256 geprüft), unter JIT und NativeAOT gleichermaßen.

Das Etikett: samples/templates/printjob.json, ein Versandetikett 100 × 150 mm (4 × 6 Zoll) mit 33 Elementen – Absenderblock, invertiertes Servicefeld, Empfänger (Land per Ausdruck), Routing-Code, Gewicht, Packstücke, Versanddatum (Format(ToDate([ShipDate]), "dd.MM.yyyy")), QR-Code (Tracking-URL), Code 128 (Sendungsnummer), GS1-128-SSCC, Seitenzähler, Rahmen und Linien – dazu ein Start- und ein End-Label. Die Datensätze variieren wie echte Daten (Umlaute, Doppelnamen, 9 Orte in AT/DE/CH/IT, 6 Services, 2,5–29 kg, gültige SSCCs). Downloads: Etikett als PDF / ZPL, Druckauftrag (Start + 3 Sendungen + Ende) als PDF / ZPL (300 dpi), Bild PNG 300 dpi.
| Datenquelle (100.000 Etiketten + Start/Ende → ZPL-Datei) | JIT Etiketten/s | NativeAOT Etiketten/s | Zeit |
|---|---|---|---|
IReadOnlyDictionary<string, object?>-Datensätze | 249.286 | 238.590 | 0,40–0,42 s |
JSON-Datensätze (JsonElement) | 216.171 | 213.637 | 0,46–0,47 s |
POCO mit Map() je Feld | 128.165 | 91.784 | 0,78–1,09 s |
POCO, For<T>() (Reflection) | 124.995 | 83.994 | 0,80–1,19 s |
Gespeichertes Format (^DF/^XF), POCO | 109.531 | 72.113 | 0,91–1,39 s |
| Rendern, 5.000 Etiketten, 14 Kerne, 203 dpi: PNG-Vorschau | 1.224 | 1.182 | |
| Rendern, 5.000 Etiketten, 14 Kerne, 203 dpi: PCX (1 Bit) | 1.058 | 462 |
Für Massendaten Dictionary- oder JSON-Datensätze übergeben: Sie bleiben unter JIT und NativeAOT über 200.000 Etiketten/s (langsamster Einzellauf 208.068/s). POCO-Bindung erreicht unter JIT rund 125.000/s, unter NativeAOT 84.000/s.
5 Millionen Datensätze und Docker (NativeAOT): Datensätze als JSON Lines (1,56 GB, ein Datensatz je Zeile), zeilenweise gelesen, als JsonElement gebunden, in eine 6,53 GB große ZPL-Datei gestreamt. Derselbe Apple M3 Max; Docker Desktop führt den Container in einer VM aus, daher ist Datei-I/O auf das Volume langsamer als nativ.
| Lauf | Etiketten/s | Zeit | Spitzenspeicher |
|---|---|---|---|
| NativeAOT unter macOS, 5 Millionen Datensätze | 132.164 | 37,8 s | 248 MB RSS |
Docker, NativeAOT linux-arm64, --memory 1g, 5 Millionen Datensätze | 91.133 | 54,9 s | 90 MB |
| Docker, 100.000 Etiketten, Dictionary- / JSON-Datensätze | 157.489 / 119.271 | 0,64 / 0,84 s | |
| Docker, PNG-Vorschau, 14 Kerne | 1.164 |
Der Speicher bleibt bei jeder Anzahl an Datensätzen konstant (eine gestreamte Quelle wird weder gezählt noch gepuffert). Image build/Dockerfile.shippingbench: 127 MB, runtime-deps:10.0-noble-chiseled-extra, nur das native Programm, ohne 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 Millionen JSON-Lines-Datensätze dotnet run -c Release --project samples/ShippingBench -- sample out/sample # Etikett als PNG/PDF/ZPL
Kleines Label mit 7 Elementen (samples/PerfTest)
samples/PerfTest erzeugt 100.000 Labels in einem Druckauftrag (Start-Label + 100.000 Labels + End-Label, Schnitt nur nach dem letzten Label) – aus Objekten, gestreamt aus EF Core/SQLite und aus LiteDB. Es verwendet ein kleines Label 100 × 60 mm mit 7 Elementen (Text, Ausdruck, Zahlenformat, Code 128, Zähler), nicht das Versandetikett oben. Gemessen im Release-Build auf 14 Kernen (Apple M3 Max, .NET 10), Einzellauf.
| Test (100.000 Labels, ein Job) | Zeit | Labels/s | Datei | Heap |
|---|---|---|---|---|
Objekte → ZPL, gespeichertes Format (^DF/^XF) | 0,73 s | 136.000 | 14,7 MB | 24 MB |
| Objekte → ZPL, volles Format je Label | 0,39 s | 256.000 | 29,1 MB | 24 MB |
EF Core / SQLite AsAsyncEnumerable() → Datei | 0,57 s | 176.000 | 14,7 MB | 29 MB |
| LiteDB-Query → Datei | 0,50 s | 202.000 | 14,7 MB | 64 MB |
| Vorschau PNG sequenziell (799 × 480 px, Graustufen-PNG) | ≈ 3,4 min | 490 | 18,2 MB / 2.000 | 84 MB |
| Vorschau PNG parallel (14 Kerne) | ≈ 25 s | 4.033 | 17,1 MB / 2.000 | 106 MB |
Die ZPL-Erzeugung für den Drucker ist praktisch kostenlos und streamt mit konstantem Speicher. Das gespeicherte Format halbiert die übertragene Datenmenge.
Die ZPL-Tests binden Klassen (Shipment-Objekte) und erzeugen deshalb sequenziell; Dictionary-, JSON- und DataRow-Datensätze entstehen auf allen Kernen – siehe parallele Job-Erzeugung.
Render-Optimierung
Schwarz-weiße Labels werden als 8-Bit-Graustufen-PNG (mit Kantenglättung) bzw. echtes 1-Bit-PNG (AntiAlias = false) statt 32-Bit-RGBA geschrieben (ZplRenderSettings.CompactPng, PngCompression Fastest/Optimal/SmallestSize). Einfache ^GB-Rahmen werden direkt gezeichnet, und das Label-Bild wird nur kopiert, wenn ^MC N es braucht.
| Pro Label (100 × 60 mm, 203 dpi) | vorher | jetzt |
|---|---|---|
| Rendern zu Bitmap | 1,28 ms | 0,39 ms |
| Rendern + PNG | 8,19 ms | 1,86 ms |
| PNG-Größe | 18 KB | 9 KB |
| 100.000 Vorschauen parallel | 2,2 min | 25 s |
Parallele Skalierung
SkiaSharp serialisiert zwei Dinge global: Skias Glyph-Cache (jedes DrawText) und SkiaSharps Handle-Tabelle (jedes neu erzeugte SKPaint/SKFont/SKBitmap – und DrawBitmap, das intern jedes Mal ein SKImage anlegt). zplCore.com rastert deshalb jede Glyphe einmal pro Schrift/Größe in einen sperrfreien Cache aus SKImage-Masken und zeichnet Text per DrawImage; Schriften werden explizit durchgereicht statt über SKFont.Typeface abgefragt. Ergebnis gemessen mit samples/RenderBench -- 150 scale (Apple M3 Max, 10 Performance- + 4 Effizienzkerne):
| Threads | Rendern + PNG vorher | Rendern + PNG jetzt | nur Text vorher | nur Text jetzt |
|---|---|---|---|---|
| 1 | 499/s (1,0×) | 549/s (1,0×) | 5.665/s (1,0×) | 6.306/s (1,0×) |
| 4 | 1.984/s (4,0×) | 2.225/s (4,1×) | 3.770/s (0,7×) | 28.183/s (4,5×) |
| 8 | 2.245/s (4,5×) | 4.090/s (7,5×) | 3.162/s (0,6×) | 42.434/s (6,7×) |
| 10 | 1.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 # Mikro-Benchmark pro Element
dotnet run -c Release --project samples/PerfTest -- 100000 perf-out
Durchsatz der Web-API
ASP.NET-Core-Minimal-API aus samples/WebApiDemo: 14 Kerne (Apple M3 Max), Server-GC, 14 Worker, kleines Label 100 × 60 mm bei 203 dpi (nicht das Versandetikett oben); Client und Server im selben Prozess.
| Szenario | Labels/s | MB/s | p50 | p95 | GC-Heap |
|---|---|---|---|---|---|
| JSON-Payload → ZPL, 1.000 Datensätze/Request | 62.857 | 20,3 | 16,2 ms | 16,7 ms | 178 MB |
| JSON-Payload → ZPL, 10.000 Datensätze/Request | 112.484 | 36,4 | 76,1 ms | 136,9 ms | 183 MB |
| JSON-Payload → ZPL, 100.000 Datensätze/Request | 186.572 | 60,5 | 527,5 ms | 565,2 ms | 414 MB |
| DB-Streaming → ZPL, 10.000 Datensätze | 151.744 | 49,1 | 69,6 ms | 73,1 ms | 361 MB |
| DB-Streaming → ZPL, 100.000 Datensätze | 199.102 | 64,6 | 504,3 ms | 505,2 ms | 372 MB |
| JSON 1k, 1 / 4 / 16 Clients | 196.112 / 240.453 / 155.344 | 63,4 / 77,7 / 50,2 | 4,8 / 16,4 / 100,8 ms | 8,6 / 21,7 / 120,8 ms | 404–419 MB |
| DB 10k, 4 / 16 Clients | 215.699 / 179.860 | 69,7 / 58,2 | 186,2 / 878,0 ms | 202,4 / 907,6 ms | 395–408 MB |
| Vorschau, 50 Labels/Request: PDF 1 / 14 Clients | 583 / 5.085 | 5,0 / 43,5 | 87,0 / 131,8 ms | 94,0 / 154,4 ms | 267–472 MB |
| Vorschau, 50 Labels/Request: PNG (zip) 1 / 14 Clients | 484 / 3.995 | 4,6 / 38,1 | 108,3 / 162,0 ms | 108,7 / 188,1 ms | 269–473 MB |
| Job-Queue: 256 × JSON 1k → ZPL (153 Jobs/s) | 153.438 | 49,6 | 98,8 ms | 365,5 ms | 885 MB |
| Job-Queue: 64 × DB 10k → ZPL (18,6 Jobs/s) | 186.361 | 60,3 | 2.236,5 ms | 3.411,1 ms | 828 MB |
| Job-Queue: 64 × JSON 50 → PDF (104 Jobs/s) | 5.396 | 46,2 | 345,2 ms | 445,6 ms | 467 MB |
Die ZPL-Erzeugung sättigt bei etwa 199.000 Labels/s (ab 4 parallelen Requests); mehr Clients erhöhen nur die Latenz. Rendern kostet etwa 40× mehr und skaliert mit den Kernen. Die Queue-Latenz enthält die Wartezeit (alle Jobs auf einmal eingereicht).
Speicher und Dauertest
| Last | Labels | Speicher |
|---|---|---|
| Dauertest, kleine Labels, parallel gerendert | 1.000.000 | RSS 128–138 MB · GC-Heap 2–3 MB, flach (kein Leck) |
| Dauertest, 4 × 6″-PNGs, 14 Threads | 1.000.000 | RSS 438–481 MB, flach |
| ZPL-Streaming aus Objekten | 100.000 | Heap 24 MB |
| Streaming aus EF Core / SQLite in eine ZPL-Datei | 100.000 | Heap 29 MB |
| Streaming aus LiteDB in eine ZPL-Datei | 100.000 | Heap 64 MB |
Alle Zahlen sind Messungen mit den Benchmarks im Repository, keine Zusicherung.

