DEVTOOLBOX / BENCHMARKS
Measured numbers: hashing, Base64, gzip, JSON, PDF and image encoding
Every number below was produced by a script in this repository, on the machine described first. The point is not the absolute values — it is which trade-offs are structural (Base64 always grows by a third) and which ones are hardware-dependent (SHA-256 beating MD5 here).
Where and how these were measured
All timings are medians of three to five runs on an idle machine, the same machine for every row. Nothing is extrapolated from another article or estimated: the script builds its own inputs (random buffers, synthetic record arrays, freshly generated PDFs and a canvas-rendered image) and reports what it observed.
This matters because published benchmark numbers are usually not comparable. A throughput figure without the algorithm, data size, runtime version and hardware is not a measurement, it is an anecdote. If you re-run the script on your own machine you should expect the ratios to hold and the absolute speeds to differ — sometimes by an order of magnitude.
| Item | Value |
|---|---|
| CPU | Apple M5 |
| Cores / memory | 10 / 32 GB |
| Operating system | macOS 27.0.1 |
| Runtime | Node v20.20.2 |
| Browser (image test) | Chromium 151.0.7922.34 |
| Data generated | 2026-10-22 |
Hash throughput: SHA-256 is faster than MD5 here
This is the most counter-intuitive result on the page. On this hardware SHA-256 processes the same 64 MB buffer roughly 3.7× faster than MD5, and SHA-1 lands in the same place as SHA-256. The explanation is not that SHA-256 is "lightweight" — it is that modern ARM chips ship dedicated SHA-1/SHA-256 instructions, and the algorithms they accelerate are essentially free compared with a hash that has no hardware path.
Two practical consequences. First, never choose MD5 because you assume it is faster; measure, because on current hardware the assumption is often false. Second, throughput is not the reason to avoid MD5 anyway — collision resistance is, and MD5 has none left. SHA-512 sits in between here because it hashes 64-bit words: on a 64-bit CPU it processes fewer words per byte, and without wide hardware support it does not reach the SHA-256 rate.
| Algorithm | Data | Time | Throughput |
|---|---|---|---|
| MD5 | 64 MB | 82 ms | 778 MB/s |
| SHA1 | 64 MB | 22 ms | 2,922 MB/s |
| SHA256 | 64 MB | 22 ms | 2,910 MB/s |
| SHA512 | 64 MB | 39 ms | 1,648 MB/s |
Throughput is a property of hardware and runtime, not of the algorithm name. Re-measure before you justify a choice with it.
Base64: the 4/3 rule is exact, so you can compute it in advance
Every row lands on a ratio of 1.3333 (with the small exception of tiny inputs where padding dominates: 1024 bytes become 1368 characters, a ratio of 1.3359). The rule is ceil(bytes / 3) × 4, so you can predict the encoded length without encoding anything. A 10 MB file becomes 13.98 MB; a 5 MB screenshot embedded in CSS becomes 6.67 MB of stylesheet that must be parsed on every page load.
The padding column is the practical trap: one or two = characters appear whenever the input length is not a multiple of three, and strict decoders reject payloads whose length is not a multiple of four. When you concatenate two Base64 strings — or split one across lines — you break that invariant, which is why line-wrapped payloads need the whitespace stripped before decoding.
| Input | Encoded output | Ratio | Padding |
|---|---|---|---|
| 1.0 KB | 1.3 KB | 1.3359 | 2 × "=" |
| 64.0 KB | 85.3 KB | 1.3334 | 2 × "=" |
| 1.00 MB | 1.33 MB | 1.3333 | 2 × "=" |
| 10.00 MB | 13.33 MB | 1.3333 | 2 × "=" |
Encoded length = ceil(bytes / 3) × 4. If the result does not fit your budget, the answer is compression or a binary channel, not "smaller Base64".
gzip: JSON compresses to a seventh, random bytes do not compress at all
Structured, repetitive data compresses enormously: a 302 KB JSON array of 5,000 records lands at 41 KB, a ratio of 0.137. Repeating text does better still. Random bytes, however, sit at roughly 1.0 — gzip cannot find redundancy that does not exist, and the few bytes of framing overhead are all you get.
This is the useful mental check before you ship an API or a downloadable asset: compressing JSON, HTML, CSS, SVG and logs is nearly always worth it, while compressing already-compressed formats (PNG, JPEG, ZIP, gzip itself) costs CPU for no gain. It also explains why Base64 in a gzipped HTTP response is less wasteful than Base64 on disk: the expansion still exists, but the encoded text itself compresses.
| Sample | Raw size | gzip size | Ratio |
|---|---|---|---|
| JSON array (5000 records) | 295.1 KB | 40.4 KB | 13.7% |
| Repeated text | 87.9 KB | 342 B | 0.4% |
| Random bytes | 256.0 KB | 256.1 KB | 100.0% |
Compress once, at the right layer. Re-compressing an already compressed format only burns CPU.
JSON scale: 100,000 records parse in tens of milliseconds
Parsing 10.4 MB of JSON takes about 33 ms, and stringifying the same structure takes 17 ms — fast enough that raw CPU time is rarely the reason a browser tab dies with a large JSON file. The costs that actually bite are memory and the main thread: the parsed object graph occupies several times the byte size of the text, and a single synchronous parse blocks rendering for its whole duration.
That is why JSON Lines exists. If you split the payload into one object per line, each line is an independent parse you can do incrementally, discard after use, and resume after an error — you trade a slightly larger total size for constant memory and a non-blocking pipeline. For a one-off 10 MB paste, an array is simpler; for a 2 GB export, it is not a choice at all.
| Records | Bytes | JSON.parse | JSON.stringify |
|---|---|---|---|
| 1,000 | 95.9 KB | 0.3 ms | 0.2 ms |
| 10,000 | 988.0 KB | 2.6 ms | 1.9 ms |
| 100,000 | 9.93 MB | 33.3 ms | 17.2 ms |
Parse time scales linearly, so measure bytes rather than assuming a limit. Memory, not CPU, is what breaks large documents.
PDF merge: page copying is cheap, page content is not
Merging 200 generated pages takes about 24 ms because the operation copies page objects and never rasterizes: text stays text, images keep their original encoding. The output size here (39 KB for 200 pages) reflects the synthetic inputs — each page holds one short text run.
A real-world merge behaves differently for a reason worth internalizing: time scales with the number of pages and objects, but file size is dominated by whatever images the pages already contain. Two 50 MB scans merge just as quickly and stay near 100 MB. So "merging is fast" and "the merged file is small" are unrelated claims, and only the first one is about the merge itself.
| Input | Merged pages | Time | Output size |
|---|---|---|---|
| 2 × 5 pages | 10 | 6 ms | 2.5 KB |
| 5 × 20 pages | 100 | 16 ms | 19.7 KB |
| 10 × 20 pages | 200 | 24 ms | 38.8 KB |
Merging preserves content; it does not re-encode it. Expect the result to be about the sum of the inputs.
Image encoding: one 1600×1200 image, six ways to store it
The same canvas content stored as PNG is 2.36 MB. At JPEG quality 0.75 it is 80 KB; at WebP quality 0.75 it is 39.6 KB — a 59× reduction from the PNG. PNG is lossless and therefore keeps every noise speck the test image contains, which is exactly why screenshots of photos balloon in size while screenshots of text stay small.
The decision rule that follows: photographs and gradients go to WebP (or JPEG where compatibility demands it), and only images with sharp edges, transparency, or text that must stay pixel-exact should stay PNG. Note also that quality is not linear — dropping WebP from 0.9 to 0.75 halves the file, and going from 0.75 to 0.5 saves another quarter with visible softness in gradients.
| Format | Quality | File size | vs PNG |
|---|---|---|---|
| PNG | lossless | 2.25 MB | 1.000× |
| JPEG | 0.92 | 159.2 KB | 0.069× |
| JPEG | 0.75 | 78.3 KB | 0.034× |
| WebP | 0.90 | 75.8 KB | 0.033× |
| WebP | 0.75 | 38.7 KB | 0.017× |
| WebP | 0.50 | 29.6 KB | 0.013× |
Format choice moves the file size by an order of magnitude; quality tuning moves it by a factor of two.
What these numbers do not tell you
- They describe one machine, one runtime version, and one input shape each. Absolute values will differ on your hardware; the structural ratios (Base64 4/3, gzip on JSON, PNG vs WebP) will hold.
- End-to-end user experience also includes transfer time, decompression, and rendering. A 40 ms parse can still feel slow if it blocks the first paint.
- Browser-side tools are bounded by memory as much as by speed. A 2 GB file will fail from an out-of-memory crash long before any of these timings become relevant.
- Re-run the script to refresh:
node scripts/measure-benchmarks.mjs. The date above is the day the data in this page was generated.
Method: each timing is the median of 3–5 runs on an otherwise idle machine; Node crypto over a single 64 MB random buffer, digest included; random bytes encoded with Buffer.toString("base64"); zlib.gzipSync at level 6; synthetic record arrays, JSON.parse and JSON.stringify timed separately; pdf-lib creating pages with one text run each, then copyPages + save; canvas.toBlob inside headless Chromium; a 1600×1200 gradient with 4000 noise specks.