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).

Measured Apple M5 · macOS 27.0.1 · Node v20.20.2

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.

ItemValue
CPUApple M5
Cores / memory10 / 32 GB
Operating systemmacOS 27.0.1
RuntimeNode v20.20.2
Browser (image test)Chromium 151.0.7922.34
Data generated2026-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.

AlgorithmDataTimeThroughput
MD564 MB82 ms778 MB/s
SHA164 MB22 ms2,922 MB/s
SHA25664 MB22 ms2,910 MB/s
SHA51264 MB39 ms1,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.

InputEncoded outputRatioPadding
1.0 KB1.3 KB1.33592 × "="
64.0 KB85.3 KB1.33342 × "="
1.00 MB1.33 MB1.33332 × "="
10.00 MB13.33 MB1.33332 × "="

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.

SampleRaw sizegzip sizeRatio
JSON array (5000 records)295.1 KB40.4 KB13.7%
Repeated text87.9 KB342 B0.4%
Random bytes256.0 KB256.1 KB100.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.

RecordsBytesJSON.parseJSON.stringify
1,00095.9 KB0.3 ms0.2 ms
10,000988.0 KB2.6 ms1.9 ms
100,0009.93 MB33.3 ms17.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.

InputMerged pagesTimeOutput size
2 × 5 pages106 ms2.5 KB
5 × 20 pages10016 ms19.7 KB
10 × 20 pages20024 ms38.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.

FormatQualityFile sizevs PNG
PNGlossless2.25 MB1.000×
JPEG0.92159.2 KB0.069×
JPEG0.7578.3 KB0.034×
WebP0.9075.8 KB0.033×
WebP0.7538.7 KB0.017×
WebP0.5029.6 KB0.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

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.