Skip to content

Commit 304497e

Browse files
committed
add base64 benchmarks and switch imlementations based on result
1 parent 2710134 commit 304497e

3 files changed

Lines changed: 106 additions & 6 deletions

File tree

benchmarks/base64.bench.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { bench, describe } from 'vitest';
2+
3+
type Uint8ArrayWithBase64 = typeof Uint8Array & {
4+
fromBase64?: (str: string) => Uint8Array;
5+
};
6+
7+
type BufferLike = {
8+
from(
9+
input: string,
10+
encoding: 'base64' | 'utf-8',
11+
): { toString(encoding: 'utf-8' | 'base64'): string };
12+
};
13+
14+
const NodeBuffer = (globalThis as any).Buffer as BufferLike | undefined;
15+
16+
const textDecoder = new TextDecoder('utf-8');
17+
18+
function decodeBase64WithUint8Array(str: string): string {
19+
return textDecoder.decode(
20+
(Uint8Array as Uint8ArrayWithBase64).fromBase64!(str),
21+
);
22+
}
23+
24+
function decodeBase64WithNodeBuffer(str: string): string {
25+
return NodeBuffer!.from(str, 'base64').toString('utf-8');
26+
}
27+
28+
function decodeBase64WithAtob(str: string): string {
29+
const binary = atob(str);
30+
return textDecoder.decode(
31+
Uint8Array.from(binary, (char) => char.charCodeAt(0)),
32+
);
33+
}
34+
35+
const hasUint8ArrayFromBase64 =
36+
typeof (Uint8Array as Uint8ArrayWithBase64).fromBase64 === 'function';
37+
const hasNodeBuffer = typeof NodeBuffer?.from === 'function';
38+
39+
const payloads = [
40+
{
41+
name: 'short username/password',
42+
decoded: 'user:password',
43+
},
44+
{
45+
name: 'common API style credential',
46+
decoded: 'api-client-42:sk_live_b36WzMj0n95wE1y8hHkR2iS4qT7vNuPx',
47+
},
48+
{
49+
name: 'long token-like password',
50+
decoded:
51+
'service-account:7f2d9c31f7f14131a65d5315f2dbdb34dc5ddacb4f57b74a04a066f53f8e92bf',
52+
},
53+
].map((payload) => ({
54+
...payload,
55+
encoded: NodeBuffer!.from(payload.decoded, 'utf-8').toString('base64'),
56+
}));
57+
58+
// Sanity check that all decoding methods produce the same results before benchmarking
59+
for (const payload of payloads) {
60+
if (decodeBase64WithAtob(payload.encoded) !== payload.decoded) {
61+
throw new Error(`atob decode failed for ${payload.name}`);
62+
}
63+
64+
if (
65+
hasUint8ArrayFromBase64 &&
66+
decodeBase64WithUint8Array(payload.encoded) !== payload.decoded
67+
) {
68+
throw new Error(`Uint8Array.fromBase64 decode failed for ${payload.name}`);
69+
}
70+
71+
if (
72+
hasNodeBuffer &&
73+
decodeBase64WithNodeBuffer(payload.encoded) !== payload.decoded
74+
) {
75+
throw new Error(`Buffer decode failed for ${payload.name}`);
76+
}
77+
}
78+
79+
describe('decode base64 for basic-auth payloads', () => {
80+
for (const payload of payloads) {
81+
describe(payload.name, () => {
82+
if (hasUint8ArrayFromBase64) {
83+
bench('Uint8Array.fromBase64 + TextDecoder', () => {
84+
decodeBase64WithUint8Array(payload.encoded);
85+
});
86+
}
87+
88+
if (hasNodeBuffer) {
89+
bench('Buffer.from(base64).toString(utf-8)', () => {
90+
decodeBase64WithNodeBuffer(payload.encoded);
91+
});
92+
}
93+
94+
bench('atob + Uint8Array.from + TextDecoder', () => {
95+
decodeBase64WithAtob(payload.encoded);
96+
});
97+
});
98+
}
99+
});

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"dist/"
1919
],
2020
"scripts": {
21+
"bench": "vitest bench",
2122
"build": "ts-scripts build",
2223
"format": "ts-scripts format",
2324
"lint": "ts-scripts lint",

src/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,17 @@ const textDecoder = new TextDecoder('utf-8');
8888
* @private
8989
*/
9090
const decodeBase64: (str: string) => string = (() => {
91-
// 1) Modern Web / some runtimes
91+
// 1) Node.js (fast path)
92+
if (typeof NodeBuffer?.from === 'function') {
93+
return (str: string) => NodeBuffer.from(str, 'base64').toString('utf-8');
94+
}
95+
96+
// 2) Modern Web / some runtimes
9297
if (typeof (Uint8Array as Uint8ArrayWithBase64).fromBase64 === 'function') {
9398
return (str: string) =>
9499
textDecoder.decode((Uint8Array as Uint8ArrayWithBase64).fromBase64!(str));
95100
}
96101

97-
// 2) Node.js (fast path)
98-
if (typeof NodeBuffer?.from === 'function') {
99-
return (str: string) => NodeBuffer.from(str, 'base64').toString('utf-8');
100-
}
101-
102102
// 3) Browser fallback
103103
return (str: string) => {
104104
const binary = atob(str);

0 commit comments

Comments
 (0)