Skip to content

Commit 6ebf2f1

Browse files
matmenclaude
andcommitted
Use the runtime's base64, CRC-32 and deflate where they are faster
- the Unicode tables are decoded with Uint8Array.fromBase64 or atob instead of a JS base64 decoder - crc32 uses node:zlib's crc32 (Node, Bun, Deno) from 64 bytes on, 3-4x faster; shorter buffers stay with slice-by-16 - without node:zlib (browsers), compression levels 1-6 use CompressionStream (2.3x faster than the JS deflate); its level is fixed, so level 0 and 7-9 stay with the JS deflate Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ
1 parent ba86a52 commit 6ebf2f1

3 files changed

Lines changed: 36 additions & 18 deletions

File tree

‎src/codecs/crc.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* Copyright (c) 2021 Arjun Barrett, MIT License (see THIRD_PARTY_NOTICES.md)
44
*/
55

6+
import { nodeZlib } from './zlib.ts';
7+
68
// slice-by-16 tables: TABLE[k * 256 + n] is the CRC of byte n followed by k zero bytes
79
const TABLE = new Int32Array(16 * 256);
810
for (let n = 0; n < 256; n++) {
@@ -15,11 +17,16 @@ for (let n = 0; n < 256; n++) {
1517
for (let k = 256; k < TABLE.length; k += 256) c = TABLE[k + n] = TABLE[c & 0xff] ^ c >>> 8;
1618
}
1719

20+
// the runtime's native CRC-32 (node:zlib in Node, Bun and Deno) is faster except for a few bytes, where the call costs more
21+
const native = nodeZlib && nodeZlib.crc32 ? nodeZlib.crc32.bind(nodeZlib) : null;
22+
const NATIVE_MIN = 64;
23+
1824
/**
19-
* CRC-32 (ISO 3309), as used by PNG and zlib. Processes 16 bytes per step (slice-by-16, see #12), byte by byte so
20-
* it doesn't depend on alignment or endianness
25+
* CRC-32 (ISO 3309), as used by PNG and zlib. Natively where the runtime has it, else 16 bytes per step (slice-by-16,
26+
* see #12), byte by byte so it doesn't depend on alignment or endianness
2127
*/
2228
export function crc32(buffer: Uint8Array): number {
29+
if (native && buffer.length >= NATIVE_MIN) return native(buffer) >>> 0;
2330
let crc = -1, i = 0;
2431
for (const end = buffer.length - 16; i <= end; i += 16) {
2532
crc = TABLE[3840 + ((buffer[i] ^ crc) & 0xff)] ^ TABLE[3584 + ((buffer[i + 1] ^ crc >> 8) & 0xff)]

‎src/codecs/zlib.ts‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,7 @@ export function decompress(data: Uint8Array, limit: number): Uint8Array {
639639

640640
interface NodeZlib {
641641
deflateSync(data: Uint8Array, options: { level: number }): Uint8Array;
642+
crc32?(data: Uint8Array): number;
642643
inflateSync(data: Uint8Array, options: { maxOutputLength: number, finishFlush: number }): Uint8Array;
643644
constants: { Z_SYNC_FLUSH: number };
644645
}
@@ -650,13 +651,14 @@ interface StreamReader {
650651

651652
interface Globals {
652653
process?: { getBuiltinModule?: (id: string) => unknown };
654+
CompressionStream?: new (format: 'deflate') => unknown;
653655
DecompressionStream?: new (format: 'deflate') => unknown;
654656
Blob?: new (parts: Uint8Array[]) => { stream(): { pipeThrough(transform: unknown): { getReader(): StreamReader } } };
655657
}
656658

657-
const { process: runtime, DecompressionStream: NativeDecompression, Blob: NativeBlob } = globalThis as unknown as Globals;
659+
const { process: runtime, CompressionStream: NativeCompression, DecompressionStream: NativeDecompression, Blob: NativeBlob } = globalThis as unknown as Globals;
658660

659-
const nodeZlib = ((): NodeZlib | null => {
661+
export const nodeZlib = ((): NodeZlib | null => {
660662
try {
661663
return (runtime && runtime.getBuiltinModule && runtime.getBuiltinModule('node:zlib') as NodeZlib) || null;
662664
} catch {
@@ -674,6 +676,22 @@ const plain = (buffer: Uint8Array): Uint8Array => new Uint8Array(buffer.buffer,
674676
export async function compressAsync(data: Uint8Array, level: number): Promise<Uint8Array> {
675677
level = Math.min(9, Math.max(0, level | 0));
676678
if (nodeZlib) return plain(nodeZlib.deflateSync(data, { level }));
679+
// CompressionStream has a fixed level (zlib's default 6): used for levels 1-6, where it is faster than the JS
680+
// deflate; storing (0) and the higher levels stay with the JS one
681+
if (NativeCompression && NativeBlob && level >= 1 && level <= 6) {
682+
const reader = new NativeBlob([data]).stream().pipeThrough(new NativeCompression('deflate')).getReader();
683+
const parts: Uint8Array[] = [];
684+
let length = 0;
685+
for (; ;) {
686+
const { done, value } = await reader.read();
687+
if (done || !value) break;
688+
parts.push(value);
689+
length += value.length;
690+
}
691+
const out = new Uint8Array(length);
692+
for (let i = 0, offset = 0; i < parts.length; offset += parts[i++].length) out.set(parts[i], offset);
693+
return out;
694+
}
677695
return compress(data, level);
678696
}
679697

‎src/render/unicode.ts‎

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,15 @@ interface Ranges {
1111

1212
const decoded = new WeakMap<EncodedProperty, Ranges>();
1313

14-
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
14+
// base64 is decoded natively: Uint8Array.fromBase64 where available, atob everywhere else (browsers, Node, Deno, Bun)
15+
declare const atob: (data: string) => string;
16+
const fromBase64 = (Uint8Array as unknown as { fromBase64?: (text: string) => Uint8Array }).fromBase64;
1517

1618
function base64(text: string): Uint8Array {
17-
const digits = new Uint8Array(128);
18-
for (let i = 0; i < 64; i++) digits[ALPHABET.charCodeAt(i)] = i;
19-
const length = text.replace(/=+$/, '').length;
20-
const out = new Uint8Array(length * 3 >> 2);
21-
for (let i = 0, o = 0, bits = 0, value = 0; i < length; i++) {
22-
value = value << 6 | digits[text.charCodeAt(i)];
23-
bits += 6;
24-
if (bits >= 8) {
25-
bits -= 8;
26-
out[o++] = value >> bits & 255;
27-
}
28-
}
29-
return out;
19+
if (fromBase64) return fromBase64(text);
20+
const binary = atob(text), bytes = new Uint8Array(binary.length);
21+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
22+
return bytes;
3023
}
3124

3225
function decode(property: EncodedProperty): Ranges {

0 commit comments

Comments
 (0)