无限画布压力测试实战:从60FPS到6FPS的完整复盘
花了72小时跑完一轮无限画布压力测试盯着最后生成的曲线图我半天没说话。倒不是被测程序崩溃了——它没崩只是数据让我心里发凉从5000节点到80000节点帧率从60 FPS一路跌到个位数内存从480MB飙到接近5GB。作为做白板类产品的前端这种衰减曲线意味着什么你我都清楚。这篇是这72小时的完整复盘测试设计、工具选型、每个用例的数据结果、踩过的坑全部摊开。给打算做无限画布或正被“画布卡顿”折磨的开发者一份可以直接抄的压测参考。1. 无限画布压力测试到底在测什么1.1 无限画布不是普通页面很多人想做一个类似libtv风格的无限画布就是那种可以无限平移、缩放、叠元素的画布工具。Demo阶段真的很爽画布无限大随手拖随手画。但真正跑完压力测试后你会发现“无限”两个字在产品演示里是卖点在工程实现里是复杂度放大器。无限画布和普通网页的交互模型完全不同。普通网页是一维流式布局加载完就展示用户滚动浏览无限画布是二维平面用户能任意平移、缩放核心特点是“永远有看不见的内容也永远有新内容要被看见”。从技术栈看无限画布要在四个层面持续工作数据层维护大量元素的增删改查空间索引层快速筛出可视区域的元素渲染层把可视元素绘制到屏幕交互层处理平移、缩放、框选等手势压力测试真正要压的就是这四层协同工作时的性能。很多人误以为只要把绘制函数写快一点就行实际上四层里任何一层出现瓶颈整个交互都会卡死。1.2 压测要回答的三个核心问题测试开始前我给自己定了三个必须答出的问题。第一数据量到达什么量级时交互开始明显不跟手这个量级就是产品的“体验红线”。第二性能瓶颈是在渲染层还是数据结构如果渲染层优化绘制代码如果数据结构层得改算法甚至引入Web Worker把计算挪出主线程。第三长时运行会不会内存泄漏白板类产品用户一开就是半天内存稳定性和初始化性能一样重要。这三个问题直接决定了测试场景怎么设计、指标怎么选、数据怎么读。后面整个72小时的测试都是围绕这三个问题展开的。如果你也想做一轮有效的压测建议先像我一样把问题写下来不然很容易变成“跑了一堆数据最后不知道要说明什么”。1.3 我的压测对象与边界我用来测试的是一个自研的无限画布demo基于Canvas API绘制的节点编辑器支持框选、缩放、拖拽数据结构用四叉树做空间索引序列化走JSON。为什么不用现成框架为了控制变量。测试的目的是验证无限画布通用机制的极限而不是某个框架的bug。当然如果你的项目用的是成熟方案这套压测方法论一样适用只需要替换被测对象。2. 三层压测体系JMeter、Playwright、自研脚本各管一段2.1 工具选型为什么不能只用JMeter很多团队一说压力测试就想到JMeter这没有错但必须说清楚边界JMeter模拟的是“很多人同时发HTTP请求”它测的是后端接口的并发承受能力。对无限画布而言真正影响用户体验的往往是浏览器端的渲染性能而JMeter碰不到浏览器。所以我把压测拆成三层接口层JMeter扫后端模拟大量用户并发保存、加载画布验证后端不会先挂渲染层Playwright驱动真实浏览器往画布里塞不同规模的数据记录帧率、内存、CPU数据层自研Node脚本生成超大JSON测试序列化、解析、传输耗时另外提醒一句不同系统的压测套路差别很大拿压测人脸识别系统那套请求脚本来套画布产品基本会测偏。还有人和我说是不是要上CC压力测试那种思路——持续高并发施压这个思路可以参考但我们要做的是正规的性能容量评估目的是定位瓶颈、评估容量方法和工具都得选对。2.2 JMeter接口层压测怎么做JMeter这边的目标很清晰验证后端在大量用户同时保存/加载画布时不会挂掉。第一步搭好数据和环境。我准备了50个虚拟用户每个用户有独立的画布数据用CSV Data Set Config做参数化避免所有请求都压在同一个账号和同一张画布上。第二步写测试计划。线程组里放两个主要请求保存画布POST和获取画布详情GET循环次数根据场景来比如连续压5分钟观察TPS和响应时间曲线。第三步处理登录态。无限画布项目一般要求登录后才有权限操作所以JMeter脚本里要先有一个登录请求从响应里提取Token再通过HTTP Header Manager带到后续请求。登录响应长这样{token: abc123}在登录请求下面加一个JSON Extractor提取token变量然后新建HTTP Header Manager添加Authorization头值填${token}。注意Token过期时间压测一般10-20分钟内完成如果中途开始报401多半是Token过期重登一次就行。第四步结果落盘。我通常不在图形界面挂“聚合报告”跑大并发而是把原始结果写CSV跑完再用Python或Excel统计。JMeter自身的图形界面在大量采样时也会消耗内存别让它成为压测瓶颈。2.3 Playwright渲染层压测怎么做渲染层压测我用Playwright它能真实控制Chromium还能拿到Performance API的数据。脚本核心逻辑不复杂打开画布页面用page.evaluate往画布数据源一次性塞入N个节点触发完整的视图缩放动画从100%缩到20%再放回100%录制动图期间的FPS数据读取performance.memory与performance.now数据重复拖动和缩放各10轮记录平均值这里有个关键细节一定要用有头模式跑不要图省事开headless。我前几轮测试用headless结果FPS虚高了大约20%后来改成有头模式数据才可信。无头模式的渲染路径和真实用户有差异压测结果只能当参考。2.4 数据层压测与指标记录数据层压测主要回答一个问题一个80000节点的画布初始化到底要传输多少数据、解析多久。我用Node脚本生成不同量级的JSON文件统计文件大小、JSON.parse耗时和内存占用。结果很明显80000个节点光JSON就有18MB主线程解析要2.3秒这还没算渲染。这个数据给产品设计的启示是大画布必须做数据分片或者分层加载不能一股脑把全量数据传给前端。三层压测的数据最终要汇总到同一张表里。我统一用三类指标衡量吞吐类接口TPS、请求响应时间渲染类FPS、掉帧率、主线程长任务次数资源类JS堆内存、总内存、CPU占用率我在测试机上用脚本每5秒采集一次系统级CPU和内存结合浏览器Performance数据能区分“卡顿来自浏览器还是系统”。3. 72小时实测记录从60 FPS到6 FPS的沉默曲线3.1 测试环境与基线准备先说环境。我用了三台配置差异很大的机器M1 Pro MacBook Pro16GB内存主力测试机i5-12400F RTX 3060 32GB的Windows台式机中高配对照组i5-8250U 8GB内存的老办公本低端对照组三台机器都装最新版Chrome关掉不必要的后台进程。测试数据放在NVMe固态硬盘上顺序读取速度3000MB/s以上。这里专门说磁盘是因为压测时要加载几百MB的测试数据如果用了机械硬盘或SATA SSD光读文件就会拖慢进度形成假瓶颈。这不是画布本身的问题是测试环境引入的噪音。所以跑压测前先确认磁盘不是短板。浏览器方面我做了一组“关闭硬件加速”的对照测试。为什么做这个因为真实用户里有人因GPU驱动问题被迫关闭硬件加速这部分人的体验我们也要覆盖。3.2 海量元素渲染测试核心数据这是整轮压测最重要的数据我按节点数分档记录。节点数加载耗时拖动平均FPS缩放平均FPS掉帧率内存占用5,0000.4s60600.5%480MB10,0000.8s57502.1%780MB20,0001.8s48358.7%1.2GB40,0004.6s281819.4%2.8GB80,00011.2s12642.3%4.9GB看到这张表我沉默了。让我沉默的原因不是“数量多了会卡”这种常识而是性能衰减的形态数据量翻倍FPS不是线性下降而是接近指数下滑内存增长也不是线性的从40000节点到80000节点内存翻了近一倍。这说明算法或数据结构里有隐藏的O(n^2)级别开销。我还做了一个相对少见的测量缩放过程中统计函数调用分布。发现卡顿最严重的阶段是缩放到最小级别时因为画布要把所有节点显示在视口里空间索引查询范围变大加上渲染层要遍历大量节点做简化绘制主线程在短时间内处理了海量任务。3.3 密集交互压力测试操作延迟与CPU负载静态渲染测完我开始模拟真实用户。脚本每2秒做一次随机操作框选、拖动、缩放、删除节点顺序完全随机持续10分钟。结果在20000节点时已经出现明显操作延迟。框选操作从按下到出现选区的平均耗时230ms用户能感知的阈值一般是100ms230ms已经属于“明显不跟手”。更值得注意的是CPU占用。持续操作十分钟后主线程CPU占用稳定在70%以上加上GC频繁触发从火焰图看GC时间片占总时长约18%。如果拿R23这类知名CPU压力测试工具做类比极限操作下的客户端CPU负载完全不亚于跑一遍渲染评测。当然实际业务负载不可能像压测脚本这么极端但这个实验告诉我们无限画布在极端情况下已经逼近普通设备的算力上限。3.4 长时稳定性测试内存泄漏与DOM泄漏最后一项是长时稳定性。测试机A保持画布打开每隔30秒执行一次随机平移或缩放持续2小时每5秒记录一次内存。结果前30分钟内存从800MB缓慢涨到1.3GB之后曲线逐渐平稳2小时后停在2.1GB。这个结果说明我的demo没有严重的内存泄漏但存在“内存只进不出”的隐患——撤销/重做历史栈里保存的节点快照一直占据内存。真实产品里如果用户做上百次撤销内存压力会更大。另一个发现来自DOM节点数量监控。虽然是Canvas渲染但我的一些悬浮提示、右键菜单是DOM元素长时间操作后DOM节点从最初的200多个涨到900多个明显有节点未被正确销毁。这类问题用肉眼看不出监控DOM节点数是个很有效的手段。4. 瓶颈定位、优化实验与排查技巧4.1 三组对照实验找出真正的性能瓶颈测试只能定位“哪里不行”要回答“为什么不行”还得做对照实验。我做了三组。实验一取消视口裁剪全量渲染所有节点。结果20000个节点时帧率已经跌破5FPS几乎死机。这个实验证明视口裁剪是无限画布不可省的基础能力但仅靠它在大节点数下会崩溃。实验二LOD分级渲染。把节点按缩放级别分三档100%以上显示完整图形50%-100%显示简化边框50%以下只显示点位。结果40000个节点的缩放FPS从18提升到41掉帧率从19.4%降到6.2%。这说明“渲染得更少”比“渲染得更快”更有效。实验三从DOMCanvas混合改为纯Canvas渲染同时把事件委托到顶层避免在大量DOM上绑定监听器。结果80000节点的平均FPS从12提升到29内存从4.9GB降到2.1GB。这个提升主要是减少了DOM开销和事件监听器的内存占用。三组实验指向同一个结论无限画布的性能天花板由数据结构加渲染策略决定而不是单个绘制函数的效率。4.2 高频问题排查速查表结合这次压测和过去几个项目的经验我整理了一份排查表遇到问题可以直接对照。现象常见根因排查方法推荐解法加载大数据JSON时页面冻结JSON.parse阻塞主线程Performance面板看Main线程Web Worker解析 / 分段加载拖动时内存持续上涨元素对象未清理Canvas缓存未释放监控JS Heap与DOM节点数对象池 / 清空离屏Canvas缩放到最小级别卡顿空间索引查询范围爆炸火焰图看查询函数耗时限制最小缩放级别 / 瓦片化索引缩放后文字图形模糊位图缩放而非矢量重绘对比缩放下截图LOD分级高倍率下重绘矢量JMeter压测大量请求超时后端连接池太小查看后端线程池、数据库连接池调大连接池并引入缓存Playwright无头模式结果虚高headless渲染路径简化和有头模式结果对比压测用有头模式长时间操作后DOM节点暴涨弹层/提示未销毁定期打印DOM总数统一命令式弹层并显式销毁4.3 火焰图、内存快照与性能数据采集的地道姿势最后分享几个数据采集的实操心得。火焰图一定要录。我习惯在Performance面板里录10秒操作重点看三个指标长任务占比、GC时间占比、事件处理器耗时。很多时候卡顿的真凶不是业务代码而是GC。GC时间占比超过15%就要考虑引入对象池。内存分析要配合快照对比。Chrome的Heap Snapshot可以对比两次快照找出“无法回收的对象”是哪个构造函数创建的。我用这个方法抓过隐藏bug某个事件处理函数在每次滚动时都会创建新闭包对象导致内存缓慢上涨快照对比后一眼就看出来了。采集数据别全靠浏览器面板。我写了个小脚本通过CDP每秒读一次performance.memory和系统资源数据输出CSV跑完用Python画曲线。好处是全程不用人工盯面板数据后处理也方便。最后测试过程中的原始文件一定要归档。JMeter的结果CSV、Playwright的trace、性能日志按测试场景命名存好。复盘时会发现这些原始数据比总结报告值钱得多后续优化完还可以拿来做前后对比。一直到这里整轮压测就基本跑完了。如果只让我说一句话总结这72小时的收获我会说性能问题的本质是数据结构与渲染策略的设计问题不是靠几个优化函数就能糊弄过去的。我最大的建议是如果你正在做无限画布类产品别等用户投诉卡顿再救火把压力测试前置到架构选型阶段先确定你这条产品线的“体验红线”是多少节点再用测试数据反推技术方案。跑压测很枯燥但沉淀下来的数据会是你后续迭代里最硬的底气。