当前位置:首页 > 文章列表 > 文章 > 前端 > ArrayBuffer.resize 怎么扩容网络分片缓冲区而不频繁拷贝

ArrayBuffer.resize 怎么扩容网络分片缓冲区而不频繁拷贝

来源:17golang原创 2026-11-11 09:05:09 0浏览 收藏

网络响应、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删除
Angular DestroyRef 怎么避免异步回调访问已销毁组件Angular DestroyRef 怎么避免异步回调访问已销毁组件
上一篇
Angular DestroyRef 怎么避免异步回调访问已销毁组件
Astro client:media 怎么按屏幕条件决定组件何时水合
下一篇
Astro client:media 怎么按屏幕条件决定组件何时水合
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    978次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    983次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1100次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1070次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    909次使用