本文是一次优秀的反哺开源社区贡献实践,腾讯工程师在日常工作中基于对开源库 qs 的使用,发现了其在某些业务场景下存在的瓶颈问题。通过对测试数据的复现,debug 定位了真正的问题原因,并基于对问题的分析提出了一个稳定提升 5 倍性能的调优方案。 本次调优方案在他发起 pull request 后,仅耗时 34 小时便被开源库作者合入主线并发布新版本,成为截至目前唯一的性能优化更新。他是怎么做到的,一起来看看吧!
qs 是 JavaScript 领域最流行的解析和序列化 URL 查询字符串开源库。
GitHub 上依赖 qs 的代码库超过2760万,npm 上每周下载量超过7000万。
作者 Jordan Harband 自2014年以来一直是 TC39(JavaScript 标准委员会)成员,并在18-21年担任编辑。
2.1 发现问题
2.2 定位问题
2.3 原因分析
2.4 解决思路
1、通过以上分析,优化一个方向在于通过减少字符串拼接的次数而减少临时变量的产生以降低内存的消耗。
2、如何减少字符串的拼接呢?考虑换一种数据结构来存放这些被 encode 后的字符,最终再把这些字符一次性转成字符串。
3、首先尝试把 encode 后的字符放在一个数组中,这样就不会产生临时的字符串变量了,等 string 的每个字符都处理完成,再把数组转成最终结果的字符串。
// 简化代码示意
var out = [];
out.push(c);
out.join('');
然而,经过测试发现该方法:
耗时略降,内存暴增,负优化!初步探索失败告终!
4、直接把字符串改数组来存放临时变量,虽然失败了,但是会发现,改成数组存放,耗时和内存确实有所减少,只是大数组转字符串这一步又大幅消耗了内存。
5、那么是不是可以尝试分片:限制一定数量的字符放入到数组中,然后把数组转成字符串,再把这些片段字符串拼接成最终的结果,这样可以减少字符串拼接过程产生的临时变量,也会控制数组的大小和生命周期,避免内存占用过高。
2.5 方案优化
1、首先把string进行分片,每片 string 遍历进行 encode,encode 后的字符放入到 array 中存储,当一片 string encode 完成后,把 array 转字符串拼接到最终结果中去,这样这个临时存储的 array 就可以及时释放掉。
// 简化代码示意
var limit = 1024;
var out = ''
for (var i = 0; i < string.length; i += limit) {
var segment = string.slice(i, i + limit);
var arr = [];
for (var j = 0; j < segment.length; j++) {
var c = segment.charCodeAt(j);
arr.push(c)
}
out += arr.join('');
}
2、根据以上方案进行多项测试,最终对比之后发现分片大小为1024时性能最好。同样 30M 的数据测试,耗时1701ms,内存459M,性能提升约5倍!
3、为什么分片是1024呢?qs 的作者也问了这个问题。如果分片太小,那么字符串拼接的次数还是很多,效果不明显。如果分片太大,临时数组本身占用内存不能及时释放掉,并且大的数组转字符串性能也不佳。1024是考虑到减少字符串拼接次数和能让临时数组及时释放掉之间的平衡,综合测试得到的最好结果。
3.1 历程(仅 34 hours)
本以为给开源库提交代码到进入正式的版本会经过较长周期,但是本次贡献在和作者15个小时时差的情况下,从 GitHub 上发起 pull request 到 npm 新版本发布全程仅34小时!尤其是代码合入主线后一小时内就发布了新版本!
看 qs 历史发布记录一个新版需要几个月时间,如果是这样那在业务中还需要自己先单独维护一个包非常麻烦,好在作者的支持非常及时高效。
3.2 结果
// 测试脚本
const qs = require('qs');
const string = '好'.repeat(30 * 1024 * 1204);
const start = Date.now();
qs.stringify({ string });
console.log(`cost: ${ Date.now() - start }ms, ${JSON.stringify(process.memoryUsage())}`);
// 6.12.0版本测试结果
cost: 7855ms, {"rss":2544050176,"heapTotal":2508939264,"heapUsed":2447279864,"external":231929,"arrayBuffers":18614}
// 6.12.1版本测试结果
cost: 2090ms, {"rss":482816000,"heapTotal":461070336,"heapUsed":421214168,"external":231929,"arrayBuffers":18614}
-End-
原创作者 | 李鑫