Your Base64 Just Got 3x Faster. Here’s What’s Still Slow (And When Not to Use It).
Published on May 24, 2026 by The Kestrel Tools Team • 9 min read
A Deno 2.8 release note hit Hacker News this week with a single line that made a lot of teams quietly reopen their upload code: “switching base64 encode/decode to simdutf delivers 3.07x faster performance for node:buffer base64, plus equivalent speedups for atob/btoa and the Web API base64 paths.” Suddenly the ten-year-old advice that “base64 is slow, avoid it for big payloads” needed an asterisk.
Here’s the catch: a faster wrong tool is still the wrong tool. The 33% size overhead that base64 has carried since RFC 3548 didn’t go anywhere. HTTP/2 binary framing, multipart/form-data, streaming Blob reads, and binary columns in your database all still beat base64 on bytes-on-the-wire and memory pressure — sometimes by a wide margin. The interesting question for base64 performance in JavaScript in 2026 isn’t “is it fast enough?” It’s “when is it still the right call, and when does the new speed just make a bad architecture finish faster?”
This post is a decision frame. We’ll walk through the new performance numbers, then a fitness matrix for picking base64 vs the alternatives, with concrete cases on each side.
How much faster is base64 in JavaScript in 2026?
About 3x faster on modern V8/Deno runtimes that ship with simdutf-backed base64. Deno 2.8’s release notes report a 3.07x speedup for node:buffer base64 encode/decode, with equivalent gains for atob, btoa, and the Web API Uint8Array.toBase64 / Uint8Array.fromBase64 paths. The work happens in simdutf, a SIMD-accelerated library that processes 16, 32, or 64 bytes at a time using AVX-512, AVX2, NEON, or RVV instructions, depending on the CPU.
A few specifics worth knowing before you assume the speedup applies to your code:
- The win is in the runtime, not in your application code. You don’t need to change a single line — upgrade Deno (or the underlying V8/Node) and the speedup is automatic.
- It applies to all four base64 paths:
node:bufferBuffer.from / toString, the globalatob/btoa, the newUint8Array.prototype.toBase64()(TC39 Stage 3 in 2024, shipping in V8 12.7+), and the matchingfromBase64. - It’s strictly a CPU win. Network transit, disk I/O, and the 33% size overhead are unchanged. If your bottleneck wasn’t the encode step, the speedup won’t move your p99.
- Older runtimes don’t get it for free. Node 20 LTS, browsers without simdutf-backed base64, and edge runtimes that pin older V8 still run the old scalar implementation. Check before you assume.
If you want to see the encode/decode in action on your own input — including a unicode-safe path that side-steps the classic btoa failure mode — you can paste a sample into our Base64 Encoder. It runs entirely client-side, so the input never leaves your browser, and the output is the same RFC 4648 base64 your backend speaks.
What base64 still costs, even at 3x speed
The speedup is real, but base64’s two structural costs — size and round-tripping — are unchanged.
1. The 33% size overhead is permanent. Base64 represents 3 bytes of binary as 4 ASCII characters, so encoded output is always ~133% the size of the input (a hair more once you add padding = and any wrapping newlines). On a 10 MB upload, that’s 3.3 MB of extra bytes you push through the request body, the load balancer, the CDN, the application server, and any proxy in between. A faster encoder doesn’t shrink that.
2. Memory pressure scales with payload. To base64-encode a 100 MB file in JavaScript you need, very roughly, the source bytes (100 MB) plus the encoded string (~133 MB) plus whatever JSON or HTTP scaffolding wraps it — and at least the encoded string has to be a single contiguous string in memory before you can JSON.stringify it. Streaming a multipart/form-data upload, by contrast, never holds the whole file in memory.
3. JSON is text, but JSON parsers are not free. When you embed base64 inside a JSON "image": "..." field, the parser has to copy that ~133 MB string out of the parser buffer and into a JS heap string before your handler ever sees it. We covered the broader “JSON is rougher than it looks at scale” problem in JSON number precision and IEEE 754; the same lesson applies here.
4. The new ergonomics don’t help old code. The Uint8Array.prototype.toBase64() API is clean and fast, but if your code is still piping through a backwards-compatible btoa(String.fromCharCode(...new Uint8Array(buf))) chain, you’re not seeing the full speedup. Migrate first; benchmark second.
The 2026 fitness matrix: when base64 wins, when it loses
This is the section worth bookmarking. Same input bytes, different transport choices, very different outcomes.
When base64 is still the right call
| Scenario | Why base64 wins |
|---|---|
| Small inline images in CSS or HTML (under ~10 KB) | Saves an HTTP round trip; the 33% size cost is under the round-trip cost on first paint. |
| JWT payloads | The JWT spec mandates base64url. There is no other choice; pick a fast implementation and move on. |
| JSON-safe transit of small binary blobs | Pasting a thumbnail, a signature, or a cryptographic nonce into a JSON body is fine when the blob is small and JSON is already the wire format. |
data: URLs | The format is defined as base64; this is what it’s for. |
HTTP Authorization: Basic headers | The spec says base64. Same answer as JWT. |
| Email attachments (MIME) | SMTP is a 7-bit-ASCII protocol by history; base64 is the standard binary-in-text encoding. Not your call. |
| Embedding signed payloads in URLs (under ~2 KB) | URL-safe base64 (RFC 4648 §5) is the conventional choice; URLSearchParams handles it cleanly. See encodeURI vs encodeURIComponent vs URLSearchParams. |
The pattern: when the protocol or spec already requires base64, or when the payload is small and JSON/text is the natural transport, base64 wins by being the path of least resistance — and now it’s also fast.
When something else beats base64
| Scenario | What to use instead | Why |
|---|---|---|
| File uploads over ~100 KB | multipart/form-data with a FormData + fetch | Streams the body; no 33% bloat; servers parse it natively. |
| HTTP/2 or HTTP/3 binary APIs | Raw ArrayBuffer body with Content-Type: application/octet-stream (or protobuf, msgpack, CBOR) | Binary framing in HTTP/2 means there’s no reason to re-encode binary as text. |
| Streaming reads of large files | Blob.stream() + ReadableStream piped to fetch | Constant memory, no full-file string allocation. |
| Database storage of binary blobs | BYTEA (Postgres), BLOB (MySQL/SQLite), or object storage with a row pointer | 33% smaller on disk, no encode/decode in the hot path. |
| Image delivery to browsers | A real image URL (CDN-served .webp / .avif) | The browser already has fast, cached, range-requestable image loading. Inlining defeats all of that. See WebP vs AVIF vs PNG. |
| Server-to-server message queues | msgpack, protobuf, CBOR, or raw binary on a binary-safe broker | Smaller, faster to parse, schema-aware. |
| Real-time WebSocket binary frames | ws.send(arrayBuffer) with binaryType = 'arraybuffer' | The WebSocket spec supports binary frames natively. Base64 in a text frame is pure overhead. |
The pattern on this side: when you control both endpoints, when the payload is large, or when a binary-aware transport already exists, base64 is paying a 33% size tax to solve a problem the transport already solved for you.
A 30-second decision rule
if (protocol_requires_base64) {
use_base64(); // JWT, Basic auth, data:, MIME, etc.
} else if (payload < 10_KB && transport === 'JSON_or_text') {
use_base64(); // ergonomics win, perf cost is negligible
} else if (payload > 100_KB || transport_is_binary_safe) {
use_streaming_or_binary(); // multipart, ArrayBuffer, protobuf, blob storage
} else {
benchmark_both(); // the 10–100 KB middle is genuinely case-by-case
}
This is the rule we keep on a sticky note for our own Base64 Encoder, and it has held up across audits of upload code, JWT pipelines, and data-URL CSS sprites.
The corner cases the 2018 posts skipped
Most of the “base64 is slow” content that ranks for this query is from the pre-simdutf, pre-HTTP/2 era. Three corner cases are worth flagging because they’re now the actual edges, not the old ones.
Unicode is no longer the slow part — it’s the wrong-output part
If you’re still wrapping btoa with unescape(encodeURIComponent(str)), you’re shipping deprecated behavior to keep emoji and accented characters from throwing. The modern fix is TextEncoder + Uint8Array.prototype.toBase64(), which is both correct and fast in 2026. We walked through the whole pattern in Why btoa(“café”) throws and how to base64-encode unicode the right way — start there if you’ve ever seen InvalidCharacterError in production logs.
URL-safe base64 is not the default
Standard base64 uses +, /, and =. All three break in URLs unless you percent-encode them. URL-safe base64 (RFC 4648 §5) substitutes - and _ and drops the = padding. The Uint8Array.prototype.toBase64({ alphabet: 'base64url' }) option ships with the new TC39 API. If you’re hand-rolling replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') — that still works, but it’s no longer necessary.
The encode/decode asymmetry is gone
Older benchmarks showed atob significantly slower than btoa. Under simdutf the two paths are within a few percent of each other on every architecture we’ve seen. If your code has historically batched encodes but not decodes (or vice versa) for performance reasons, that asymmetry no longer justifies the complexity.
So what should you actually do this week?
Four concrete moves, in priority order:
- Audit your large-payload upload paths. If anything over 100 KB is being base64-encoded into JSON, that’s the most likely place 3.07x faster encoding masks a problem
multipart/form-datawould solve outright. Faster doesn’t mean cheaper at the network layer. - Migrate to
Uint8Array.prototype.toBase64/fromBase64where supported. The simdutf path is wired up to these APIs first. Olderbtoa(String.fromCharCode(...))chains may not get the full speedup. - Bump your runtime. Deno 2.8, Node 22+ (with V8 12.7+), and recent Bun all carry the simdutf base64 path. If you’re on an LTS one major version back, you’re leaving the win on the table.
- Stop reading 2018 base64 perf advice. The benchmark numbers in those posts predate the entire SIMD-accelerated era, both for the encoder and for the alternatives (HTTP/2 binary framing, streaming
fetchbodies, the modernBlobAPI).
If you want a sandbox to play with the encode/decode behavior, including the unicode-safe path and URL-safe variant, our Base64 Encoder runs entirely in your browser — paste in a string, switch alphabets, and see the bytes. We use it ourselves to sanity-check JWT payloads, signed cookie values, and the occasional inline SVG before we commit it to a stylesheet.
The headline of this post — base64 just got 3x faster — is true. The footnote — which doesn’t change when you should use it — is the part worth keeping. Pick the right transport for the payload, and now your runtime will be ready when base64 is the right answer.