ArrayBuffer.resize 怎么扩容网络分片缓冲区而不频繁拷贝
来源:17golang原创
时间:2026-11-11 09:05:09 472浏览 收藏
网络响应、WebSocket 二进制消息或文件分片到达时,如果每来一块数据就新建一个更大的 ArrayBuffer 再复制旧内容,业务层会反复付出分配和拷贝成本。更合适的做法是创建带 maxByteLength 的可调整缓冲区,只有当前容量不够时才调用 resize(),并用长度跟踪的 Uint8Array 继续写入。
参考文档:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/resize
先划清边界:resize() 不能把普通的固定长度 ArrayBuffer 变成可调整对象;它也不承诺 JavaScript 引擎底层永远不会搬移内存。本文解决的是“业务代码不再为每个分片手工创建新缓冲区并复制整段数据”,不是对引擎内部实现作零拷贝保证。

先创建真正可调整的 ArrayBuffer
可调整性在构造时确定。初始长度是当前可写容量,maxByteLength 是整个对象允许达到的上限;读取 resizable 可以把能力检查写进初始化路径。旧浏览器或旧运行时没有 resize 时,应在进入流式缓冲逻辑前明确降级,而不是等到追加分片时才报错。
function createChunkBuffer(initialSize, maxSize) {
// 先做能力检查,避免旧运行时把问题拖到网络接收阶段
if (typeof ArrayBuffer.prototype.resize !== "function") {
throw new Error("当前运行时不支持可调整 ArrayBuffer");
}
// maxByteLength 只能在构造时确定,不能事后修改
const buffer = new ArrayBuffer(initialSize, {
maxByteLength: maxSize,
});
// 初始化阶段就确认配置没有把普通缓冲区误当成可调整缓冲区
if (!buffer.resizable || buffer.maxByteLength !== maxSize) {
throw new Error("缓冲区没有按预期启用 resize");
}
return buffer;
}
例如初始容量可以按常见分片大小设置,上限则按单条消息或单个文件的业务限制设置。不要为了“以后可能用到”把上限无限放大,过大的上限会增加内存风险;真正需要无限增长的场景也不适合把一个消息全部留在内存中。
只在容量不足时计算并扩容
追加分片前先计算 used + chunk.byteLength。只要结果不超过当前 byteLength,直接写入;超过时再计算下一容量。下面使用常见的倍增策略,让扩容次数随数据量增长,而不是每个分片都触发一次。
function ensureCapacity(state, additionalBytes) {
// used 表示有效数据长度,additionalBytes 表示待追加分片大小
const required = state.used + additionalBytes;
if (required state.buffer.maxByteLength) {
// 超过上限时不要静默截断网络数据
throw new RangeError("消息大小超过 ArrayBuffer 的 maxByteLength");
}
// resize 返回 undefined,容量变化通过 byteLength 体现
state.buffer.resize(nextSize);
}
这里的 Math.max(required, doubled) 很重要:如果单个网络分片本身大于两倍容量,直接按倍增值扩容仍然不够写入,必须优先满足本次所需长度。若下一容量超过上限,应让调用方结束当前消息或改用流式消费,不能把数据截短后继续解析。
用长度跟踪 Uint8Array 接收网络分片
创建 Uint8Array(buffer) 时不传第三个长度参数,它会跟踪底层可调整缓冲区的当前范围。缓冲区扩容后,视图的 byteLength 和 length 会随之增加;维护一个单独的 used 字段,则能区分“已经写入的数据”和“尚未使用的容量”。
function createChunkAccumulator(initialSize = 64 * 1024, maxSize = 8 * 1024 * 1024) {
const buffer = createChunkBuffer(initialSize, maxSize);
return {
buffer,
// 不传 length,保持为长度跟踪视图,扩容后可继续使用
view: new Uint8Array(buffer),
used: 0,
};
}
function appendChunk(state, chunk) {
// 追加前扩容,避免 set 写入超出当前视图范围
ensureCapacity(state, chunk.byteLength);
state.view.set(chunk, state.used);
state.used += chunk.byteLength;
}
function takeBytes(state) {
// subarray 只创建消费窗口,不复制底层字节
return state.view.subarray(0, state.used);
}
把 view 创建成长度跟踪视图后,resize() 扩容不需要重新执行 new Uint8Array(buffer)。消费时用 subarray() 截出有效区间即可;如果后续 API 需要独立所有权,再明确调用 slice() 或复制到新的缓冲区。

收缩、固定长度视图和兼容边界
resize() 既能扩容也能收缩。收缩会保留前面的字节并丢弃超出新长度的部分,因此只能在确认尾部数据已经消费后执行。对网络接收缓冲区来说,通常在一条消息解析完成后直接丢弃整个状态,比频繁收缩更简单;如果要复用对象,也应先把 used 设回零,再决定是否缩小。
function resetAccumulator(state, keepSize = 64 * 1024) {
// 只在确认旧数据已被消费后清空有效长度
state.used = 0;
// 收缩会丢弃超出 keepSize 的尾部字节,不能用于保留未消费数据
if (state.buffer.byteLength > keepSize) {
state.buffer.resize(keepSize);
}
}
function supportsResizableBuffer() {
// 用原型能力和构造结果双重判断,给兼容分支留下明确入口
if (typeof ArrayBuffer.prototype.resize !== "function") {
return false;
}
return new ArrayBuffer(1, { maxByteLength: 2 }).resizable === true;
}
还有一个容易忽略的视图边界:不传第三个参数的 TypedArray 是长度跟踪视图,而显式传入长度的视图是固定长度视图。固定长度视图不会因为底层扩容而自动覆盖新增区域;如果底层收缩到它越界,观察到的长度还可能变成零。需要跟随网络缓冲区增长时,优先使用长度跟踪视图。
可以把整套方案压缩成一句话:构造时设置 maxByteLength → 维护 used → 只有容量不足才按需 resize → 用长度跟踪 Uint8Array 写入 → 消费完成后再决定是否收缩。这样减少的是业务层重复分配和整段复制,仍然要用上限、消息大小和兼容分支控制内存风险。
相关问题
普通 ArrayBuffer 能直接调用 resize 吗?
不能。只有构造时设置了 maxByteLength 的可调整缓冲区才具备这项能力;普通缓冲区的 resizable 为 false,应重新设计初始化路径或使用兼容实现。
resize 是不是一定完全零拷贝?
不是。它避免了业务代码在每个分片到达时手工创建新缓冲区并复制旧内容,但底层引擎如何管理内存属于实现细节,文章不能把这个 API 说成绝对零拷贝。
为什么我的 TypedArray 扩容后长度没变?
检查构造时是否传入了第三个长度参数。传入长度得到固定长度视图;省略长度或传入 undefined 才是长度跟踪视图。还要确认底层缓冲区本身确实是可调整的。
-
306 收藏
-
194 收藏
-
427 收藏
-
480 收藏
-
486 收藏
-
文章 · 前端 | 24分钟前 | 前端 · csv · 编码 · javascript · 数据导入 · csv ReadableStream TextDecoderStream UTF-8 BOM IgnoreBOM 前端流式读取283 收藏
-
240 收藏
-
355 收藏
-
132 收藏
-
390 收藏
-
文章 · 前端 | 35分钟前 | 前端 · javascript · 浏览器API · ArrayBuffer TypedArray ArrayBuffer.transfer detached JavaScript二进制缓冲区500 收藏
-
332 收藏
-
453 收藏
-
239 收藏
-
272 收藏
-
288 收藏
-
468 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习