将 Base64 编码的数据快速转换为 Uint8Array
为了方便节省请求数,有时我们会将二进制数据以 Base64 编码的形式嵌入 HTML 或 JavaScript 中,再于运行时解码成 Uint8Array,进行后续运算。然而浏览器没有提供直接的 API 来完成这种解码转换,需要我们自己实现。本文介绍两种快速的解码方法。
数据准备
const data = Array.from({ length: 100 * 1024 }, () => Math.floor(Math.random() * 256))
const raw = String.fromCharCode(...data)
const b64data = btoa(raw)
为测试不同解码方法的性能,我们生成了一组 100KB 的随机数据,并将其转换为 Base64 编码的字符串 b64data
。b64data
将作为解码函数的输入,以测试不同解码方法的性能。
方法一:循环拷贝
function decode1(b64data) {
const raw = atob(b64data)
const buf = new Uint8Array(raw.length)
for (let i = 0; i < raw.length; i++) buf[i] = raw.charCodeAt(i)
return buf
}
方法一最为直接。首先使用 atob
函数将 b64data
解码为包含原始数据的字符串 raw
,再通过一个循环拷贝 raw
的各个字符点位到字节数组 buf
中。这种方式的计算密集部分都是由 JS 完成的,尽管 V8 引擎有 JIT,运行性能仍是不理想:
console.time("decode1")
decode1(b64data)
console.timeEnd("decode1") // decode1: 1.2431640625 ms
方法二:使用 TextEncoder
注意 该方法解码的结果和原始数据是不等价的,请阅读下面分析后再决定是否使用。
function decode2(b64data) {
const raw = atob(b64data)
const buf = new Uint8Array(raw.length * 2)
const { written } = new TextEncoder().encodeInto(raw, buf)
return buf.subarray(0, written)
}
有没有其他捷径能取代方法一中的热循环呢?答案是使用 TextEncoder.encodeInto(str, buf)
函数。这个函数会将字符串 str
编码为 UTF-8 并写入到字节数组 buf
中。这一切都是在底层的 C++ 代码中完成的,因此性能会更好。以相同方式测试,可以观察到 decode2
的耗时仅有 decode1
的 60%。
console.time("decode2")
decode2(b64data)
console.timeEnd("decode2") // decode2: 0.7060546875 ms
decode2 的弊端和应用场景
读者可能会奇怪,为什么 decode2
中将 buf
的长度初始化为 raw.length * 2
而非 raw.length
呢?这是因为 TextEncoder.encodeInto
会将字符串编码为 UTF-8,我们得到的其实是原始数据的 UTF-8 表示。由于 b64data
编码自字符串 raw
,而 raw
的每个字符范围为 [0, 255],在 UTF-8 编码后,字符范围为 [128, 255] 的部分会变成两个字节。因此,我们需要将 buf
的长度初始化为 raw.length * 2
作为最大字节数上限。TextEncoder.encodeInto
返回的 written
字段会告诉我们实际写入的字节数,我们可以通过 buf.subarray(0, written)
来截取有效部分。
可惜,TextEncoder
默认只能编码 UTF-8。如果它支持 Latin-1 编码,decode2
的解码结果便会和原始数据完全等价。那既然 decode2
的结果和原始数据不等价,那它岂不是无用?也不尽然。
如果原始数据的每个字节范围都是 [0, 127],那么
decode2
的结果和原始数据是等价的。这是因为 ASCII 字符在 UTF-8 编码中仍然是单字节的。因此,如果你的数据确保了这一点,那么decode2
会是一个更快的解码方法。如果数据的下游能接受这种“异化”后的解码结果,
decode2
也可被使用。一个例子是解码后的数据将作为 WebAssembly 模块的输入。我们可以在编写 WASM 代码时提前考虑到这一点,实现一个简单的“双字节 UTF-8 -> 单字节”的转换函数,即可享受decode2
的性能,并同时兼顾数据的正确性。
作者:hsfzxjy
链接:
许可:CC BY-NC-ND 4.0.
著作权归作者所有。本文不允许被用作商业用途,非商业转载请注明出处。
OOPS!
A comment box should be right here...But it was gone due to network issues :-(If you want to leave comments, make sure you have access to disqus.com.