花了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 Pro,16GB内存:主力测试机
- i5-12400F + RTX 3060 + 32GB的Windows台式机:中高配对照组
- i5-8250U + 8GB内存的老办公本:低端对照组
三台机器都装最新版Chrome,关掉不必要的后台进程。测试数据放在NVMe固态硬盘上,顺序读取速度3000MB/s以上。这里专门说磁盘,是因为压测时要加载几百MB的测试数据,如果用了机械硬盘或SATA SSD,光读文件就会拖慢进度,形成假瓶颈。这不是画布本身的问题,是测试环境引入的噪音。所以跑压测前,先确认磁盘不是短板。
浏览器方面,我做了一组“关闭硬件加速”的对照测试。为什么做这个?因为真实用户里有人因GPU驱动问题被迫关闭硬件加速,这部分人的体验我们也要覆盖。
3.2 海量元素渲染测试:核心数据
这是整轮压测最重要的数据,我按节点数分档记录。
| 节点数 | 加载耗时 | 拖动平均FPS | 缩放平均FPS | 掉帧率 | 内存占用 |
|---|---|---|---|---|---|
| 5,000 | 0.4s | 60 | 60 | 0.5% | 480MB |
| 10,000 | 0.8s | 57 | 50 | 2.1% | 780MB |
| 20,000 | 1.8s | 48 | 35 | 8.7% | 1.2GB |
| 40,000 | 4.6s | 28 | 18 | 19.4% | 2.8GB |
| 80,000 | 11.2s | 12 | 6 | 42.3% | 4.9GB |
看到这张表我沉默了。让我沉默的原因不是“数量多了会卡”这种常识,而是性能衰减的形态:数据量翻倍,FPS不是线性下降,而是接近指数下滑;内存增长也不是线性的,从40000节点到80000节点,内存翻了近一倍。这说明算法或数据结构里有隐藏的O(n^2)级别开销。
我还做了一个相对少见的测量:缩放过程中统计函数调用分布。发现卡顿最严重的阶段是缩放到最小级别时,因为画布要把所有节点显示在视口里,空间索引查询范围变大,加上渲染层要遍历大量节点做简化绘制,主线程在短时间内处理了海量任务。
3.3 密集交互压力测试:操作延迟与CPU负载
静态渲染测完,我开始模拟真实用户。脚本每2秒做一次随机操作:框选、拖动、缩放、删除节点,顺序完全随机,持续10分钟。
结果,在20000节点时已经出现明显操作延迟。框选操作从按下到出现选区的平均耗时230ms,用户能感知的阈值一般是100ms,230ms已经属于“明显不跟手”。
更值得注意的是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%。这说明“渲染得更少”比“渲染得更快”更有效。
实验三:从DOM+Canvas混合改为纯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小时的收获,我会说:性能问题的本质是数据结构与渲染策略的设计问题,不是靠几个优化函数就能糊弄过去的。我最大的建议是,如果你正在做无限画布类产品,别等用户投诉卡顿再救火,把压力测试前置到架构选型阶段,先确定你这条产品线的“体验红线”是多少节点,再用测试数据反推技术方案。跑压测很枯燥,但沉淀下来的数据,会是你后续迭代里最硬的底气。