背景
默默干鸿蒙的某一天,我们突然接到客户报告的一个缺陷:鸿蒙页面在操作过程中出现卡顿。作为跨端框架的提供者,我们负责鸿蒙底座的构建,而业务代码则由客户自行开发。经排查,问题源于鸿蒙的JSVM模块------一个提供完整JS引擎能力的核心模块,负责创建和销毁引擎、执行JS代码以及JS与C++的交互等功能。难以想象,一段业务传递的JS代码在该模块中运行时间竟长达1.6秒?经过抽丝剥茧,我们发现客户将一段对500万字符进行编码的JS脚本放入了鸿蒙的JSVM中运行。
最后发现,客户将一串500万字符的字符串进行了两次编码拼接操作,全程使用 +
运算符进行字符串连接。不了解业务为什么这么做,但需要跟他们说明原因并协助其修改。
目前,虽然这段 500w 字符编码的 JS 脚本,在 chrome 上运行不过500多ms,但在鸿蒙的 jsvm 中运行将是1.6s左右。针对客户对鸿蒙性能的质疑,也只能后续再进行更深入的技术分析了。
优化目标
既然要进行优化,那么要优化到什么程度才算合理呢?
在24年,Chrome 的性能面板有一项重大改版,INP 取代 FID 成为新的 Core Web Vitals
。

通常情况下,JavaScript是驱动页面交互的主要力量,尽管部分控件(如复选框、单选按钮以及纯CSS控件)可以不依赖于JavaScript。因此,JavaScript的执行效率会直接影响INP指标。
按照上述互动性响应能力评估,js 脚本的运行时间应控制在200毫秒以内,以提升用户体验。因此,我们可以将200毫秒作为本次性能优化的目标。下面就是分析有哪些可优化的手段了。
字符拼接方式
字符编码过程中必不可少的就是字符拼接。字符拼接有以下几种方式
+
运算符- 使用
concat()
方法 - 存放数组,最后使用数组
join()
方法
根据以上三种方式,分别对1千、1万、10万、100万、500万的字符在chrome中独立进行拼接耗时检测
javascript
const str01 = new Array(1000).fill('e').join('');
const str1 = new Array(10000).fill('e').join('');
const str10 = new Array(100000).fill('e').join('');
const str100 = new Array(1000000).fill('e').join('');
const str500 = new Array(5000000).fill('e').join('');
function addStr(str,num) {
console.time('+ ' + num);
let res = '';
for (let i = 0; i < str.length; i++) {
res += str.charCodeAt(i);
}
console.timeEnd('+ ' + num);
}
function concatStr(str, num) {
console.time('concat' + num);
let res = '';
for (let i = 0; i < str.length; i++) {
res = res.concat(str.charCodeAt(i));
}
console.timeEnd('concat' + num);
}
function arrStr(str, num) {
console.time('arr' + num);
let res = '';
let arr = []
for (let i = 0; i < str.length; i++) {
arr.push(str.charCodeAt(i));
}
res = arr.join('');
console.timeEnd('arr' + num);
}

以上的数据受设备性能、浏览器及其当下的状态影响。10w字符以下的处理三者差异不大,10w以上差异明显
客户现有的编码代码(两种类型编码:base64encode(utf16to8(p)) )中使用了 += 的方式对编码后的字符进行了字符的拼接,导致当对500w字符进行编码时,总耗时有500ms左右。将拼接方式换成Array.push()方式后,总编码耗时平均在200ms 左右。
数组转字符方式
数组转字符也不用数据测试,join() 是在无特殊情况下,全量数组元素转字符最高效的方式。但是,当独立对一个500w个字符类型元素的数组进行 join 时,平均耗时仍需40ms 左右。
以当前编码为例,需要将字符串先转换成utf-8,然后再进行编码,此处会对500w的字符进行2次遍历

在上述流程中,可以去除第一步utf16to8功能 的数组转换字符串 步骤 , 能够再降低一部分的耗时,由最初的500ms 左右优化到150ms 左右。
字符取值方式
做完以上的处理其实已经优化的差不多了,但是500w字符120ms 的耗时,还是想着会不会有更优解?那么从字符取值方式上验证下看看?
字符取值有以下几种方式
- 使用方括号(
[]
)语法 - 使用
charAt()
方法 - 使用
charCodeAt()
方法(然后转换为字符) - 使用
codePointAt()
方法(然后转换为字符) - 使用
at()
方法
简单做了下500w字符的取值耗时运行代码
javascript
const str = new Array(5000000).fill('e').join('');
console.time('index access');
for (let i = 0; i < str.length; i++) {
const char = str[i];
}
console.timeEnd('index access');
console.time('charAt method');
for (let i = 0; i < str.length; i++) {
const char = str.charAt(i);
}
console.timeEnd('charAt method');
console.time('charCodeAt method');
for (let i = 0; i < str.length; i++) {
const char = String.fromCharCode(str.charCodeAt(i));
}
console.timeEnd('charCodeAt method');
console.time('codePointAt method');
for (let i = 0; i < str.length; i++) {
const char = String.fromCharCode(str.codePointAt(i));
}
console.timeEnd('codePointAt method');
console.time('at method');
for (let i = 0; i < str.length; i++) {
const char = str.at(i);
}
console.timeEnd('at method');
主要在node和chrome 上运行了下数据,3次取平均值
node(ms) | chrome(ms) | |
---|---|---|
[] | 6.446 | 4.593 |
charAt | 6.076 | 6.596 |
charCodeAt | 6.558 | 2.701 |
codePointAt | 12.658 | 3.503 |
at | 27.402 | 22.994 |
根据数据表示,该项没有特别大的变化,一般也不会使用at的方式去取值,而其他的方式针对500w数据量字符的优化也有限,最多不过能提升4ms,没有特别大的必要。
结果
优化后的代码在Chrome上的运行时间约为150毫秒,在鸿蒙上约为500毫秒。客户后来发现问题是由于开发人员在适配鸿蒙时错误地将多余的字符一起编码,实际上每次编码的字符最多只有100万。
总结
尽管问题最终被证明是一个乌龙,但通过这个过程,代码性能得到了优化,对鸿蒙和Chrome都有所改进。在JavaScript中进行性能优化的重要性不言而喻,特别是在处理大量数据时,选择合适的字符串操作和编码方法对性能有显著影响。针对大量字符编码时:
- 避免在循环中使用字符串拼接,使用Array.push()和join()方法代替。
- 对于大量字符的编码,考虑使用更高效的编码算法和数据结构。