登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

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 和长度跟踪 Uint8Array 的静态关系说明图
图1:静态结构说明图,查看网络分片、容量边界、可调整缓冲区和消费视图之间的关系。

先创建真正可调整的 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() 或复制到新的缓冲区。

ArrayBuffer 的初始长度、maxByteLength、resize 和长度跟踪视图边界静态说明图
图2:静态边界说明图,查看可调整上限、resize 调用、长度跟踪视图与固定长度视图的差异。

收缩、固定长度视图和兼容边界

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 才是长度跟踪视图。还要确认底层缓冲区本身确实是可调整的。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>