乐趣区

关于javascript:try-catch引发的性能优化深度思考

要害代码拆解成如下图所示(无关局部已省略):

起初我认为可能是这个 getRowDataItemNumberFormat 函数外面某些办法执行太慢,从 formatData.replaceunescape(已废除,官网倡议应用 decodeURI 或者 decodeURIComponent 代替)办法都狐疑了一遍,发现这些办法都不是该函数运行慢的起因。为了深究起因,我给 style.formatData 传入了不同的值,发现这个函数的运行效率呈现不同的体现。开始有点纳闷为什么 style.formatData 的值导致这个函数的运行效率差异如此之大。

进一步最终定位发现如果 style.formatData 为 undefined 的时候,效率骤降,如果 style.formatData 为非法的字符串的时候,效率是正常值。我开始意识到这个问题的起因在那里了,把眼光转向了 try catch 代码块,这是一个很可疑的中央,在很早之前已经据说过不合理的 try catch 是会影响性能的,然而之前从没遇到过,联合了一些材料,我发现比拟少案例去探索这类代码片段的性能,我决定写代码去验证下:

window.a = 'a';
window.c = undefined;
function getRowDataItemNumberFormatTryCatch() {console.time('getRowDataItemNumberFormatTryCatch');
    for (let i = 0; i < 3000; i++) {
        try {a.replace(/%022/g, '"');
        }
        catch (error) {}}
    console.timeEnd('getRowDataItemNumberFormatTryCatch');
}

我尝试把 try catch 放入一个 for 循环中,让它运行 3000 次,看看它的耗时为多少,我的电脑执行该代码的工夫大略是 0.2 ms 左右,这是一个比拟快的值,然而这里 a.replace 是失常运行的,也就是 a 是一个字符串能失常运行 replace 办法,所以这里的耗时是失常的。我对他略微做了一下扭转,如下:

function getRowDataItemNumberFormatTryCatch2() {console.time('getRowDataItemNumberFormatTryCatch');
    for (let i = 0; i < 3000; i++) {
        try {c.replace(/%022/g, '"');
        }
        catch (error) {}}
    console.timeEnd('getRowDataItemNumberFormatTryCatch');
}

这段代码跟下面代码惟一的区别是,c.replace 此时应该是会报错的,因为 cundefined,这个谬误会被 try catch 捕捉到,而下面的代码耗时呈现了微小的变动,回升到 40 ms,相差了将近 200 倍!并且上述代码和首图的 getRowDataItemNumberFormat 函数代码均呈现了 Minor GC,留神这个 Minor GC 也是会耗时的。

这能够解释一部分起因了,咱们下面运行的代码是一个性能比拟要害的局部,不应该应用 try catch 构造,因为该构造是相当独特的。与其余结构不同,它运行时会在以后作用域中创立一个新变量。每次 catch 执行该子句都会产生这种状况,将捕捉的异样对象调配给一个变量。

即便在同一作用域内,此变量也不存在于脚本的其余局部中。它在 catch 子句的结尾创立,而后在子句开端销毁。因为此变量是在运行时创立和销毁的(这些都须要额定的耗时!),并且这是 JavaScript 语言的一种非凡状况,所以某些浏览器不能十分无效地解决它,并且在捕捉异样的状况下,将捕捉处理程序放在性能要害的循环中可能会导致性能问题,这是咱们为什么下面会呈现 Minor GC 并且会有重大耗时的起因。

如果可能,应在代码中的较高级别上进行异样解决,在这种状况下,异样解决可能不会那么频繁产生,或者能够通过首先查看是否容许所需的操作来防止。下面的 getRowDataItemNumberFormatTryCatch2 函数示例显示的循环,如果外面所需的属性不存在,则该循环可能引发多个异样,为此性能更优的写法应该如下:

function getRowDataItemNumberFormatIf() {console.time('getRowDataItemNumberFormatIf');
    for (let i = 0; i < 3000; i++) {if (c) {c.replace(/%022/g, '"');
        }
    }
    console.timeEnd('getRowDataItemNumberFormatIf')
}

下面的这段代码语义上跟 try catch 其实是类似的,但运行效率迅速降落至 0.04ms,所以 try catch 应该通过查看属性或应用其余适当的单元测试来完全避免应用此结构,因为这些结构会极大地影响性能,因而应尽量减少应用它们。

如果一个函数被反复调用,或者一个循环被反复求值,那么最好防止其中蕴含这些结构。它们最适宜仅执行一次或仅执行几次且不在性能要害代码内执行的代码。尽可能将它们与其余代码隔离,免得影响其性能。

例如,能够将它们放在顶级函数中,或者运行它们一次并存储后果,这样你当前就能够再次应用后果而不用从新运行代码。

getRowDataItemNumberFormat 在通过上述思路革新后,运行效率失去了质的晋升,在实测 300 屡次循环中缩小的工夫如下图,足足优化了将近 2s 多的工夫,如果是 3000 次的循环,那么它的优化比例会更高:


因为下面的代码是从我的项目中革新进去演示的,可能并不够直观,所以我从新写了另外一个类似的例子,代码如下,这外面的逻辑和下面的 getRowDataItemNumberFormat 函数讲道理是统一的,然而我让其产生谬误的时候进入 catch 逻辑执行工作。

事实上 plus1plus2 函数的代码逻辑是统一的,只有代码语义是不雷同,一个是返回 1,另一个是谬误抛出 1,一个求和办法在 try 片段实现,另一个求和办法再 catch 实现,咱们能够粘贴这段代码在浏览器别离去掉不同的正文察看后果。

咱们发现 try 片段中的代码运行大概应用了 0.1 ms,而 catch 实现同一个求和逻辑却执行了大概 6 ms,这合乎咱们下面代码察看的预期,如果把计算范畴持续加大,那么这个差距将会更加显著,实测如果计算 300000 次,那么将会由原来的 60 倍差距扩充到 500 倍,那就是说咱们执行的 catch 次数越少折损效率越少,而如果咱们执行的 catch 次数越多那么折损的效率也会越多。

所以在不得已的状况下应用 try catch 代码块,也要尽量保障少进入到 catch 控制流分支中。

const plus1 = () => 1;
const plus2 = () => { throw 1};
console.time('sum');
let sum = 0;
for (let i = 0; i < 3000; i++) {
    try {// sum += plus1(); // 正确时候 约 0.1ms
        sum += plus2(); // 谬误时候 约 6ms} catch (error) {sum += error;}
}
console.timeEnd('sum');

下面的种种体现进一步引发了我对我的项目性能的一些思考,我搜了下咱们这个我的项目至多存在 800 多个 try catch,蹩脚的是咱们无奈保障所有的 try catch 是不侵害代码性能并且有意义的,这外面必定会暗藏着很多上述类的 try catch 代码块。

从性能的角度来看,目前 V8 引擎的确在踊跃的通过 try catch 来优化这类代码片段,在以前浏览器版本中下面整个循环即便产生在 try catch 代码块内,它的速度也会变慢,因为以前浏览器版本会默认禁用 try catch 内代码的优化来不便咱们调试异样。

try catch 须要遍历某种构造来查找 catch 解决代码,并且通常以某种形式调配异样(例如:须要查看堆栈,查看堆信息,执行分支和回收堆栈)。只管当初大部分浏览器曾经优化了,咱们也尽量要防止去写出下面类似的代码,比方以下代码:

try {container.innerHTML = "I'm alloyteam";}
catch (error) {// todo}

下面这类代码我集体更倡议写成如下模式,如果你实际上抛出并捕捉了一个异样,它可能会变慢,然而因为在大多数状况下下面的代码是没有异样的,因而整体后果会比异样更快。

这是因为代码控制流中没有分支会升高运行速度,换句话说就是这个代码执行没谬误的时候,没有在 catch 中节约你的代码执行工夫,咱们不应该编写过多的 try catch 这会在咱们保护和查看代码的时候晋升不必要的老本,有可能扩散并节约咱们的注意力。

当咱们预感代码片段有可能出错,更应该是集中注意力去解决 successerror 的场景,而非应用 try catch 来爱护咱们的代码,更多时候 try catch 反而会让咱们疏忽了代码存在的致命问题。

if (container) container.innerHTML = "I'm alloyteam";
else // todo

在简略代码中该当缩小甚至不必 try catch,咱们能够优先思考 if else 代替,在某些简单不可测的代码中也应该缩小 try catch(比方异步代码),咱们看过很多 asyncawait 的示例代码都是联合 try catch 的,在很多性能场景下我认为它并不合理,集体感觉上面的写法应该是更洁净,整洁和高效的。

因为 JavaScript 是事件驱动的,尽管一个谬误不会进行整个脚本,但如果产生任何谬误,它都会出错,捕捉和解决该谬误简直没有任何益处,代码次要局部中的 try catch 代码块是无奈捕捉事件回调中产生的谬误。

通常更正当的做法是在回调办法通过第一个参数传递错误信息,或者思考应用 Promisereject() 来进行解决,也能够参考 node 中的常见写法如下:

;(async () => {const [err, data] = await readFile();
    if (err) {// todo};
})()

fs.readFile('<directory>', (err, data) => {if (err) {// todo}
});

联合了下面的一些剖析,我本人做出一些通俗的总结:

    1. 如果咱们通过欠缺一些测试,尽量确保不产生异样,则无需尝试应用 try catch 来捕捉异样。
    1. 非异样门路不须要额定的 try catch,确保异样门路在须要思考性能状况下优先思考 if else,不思考性能状况请君随便,而异步能够思考回调函数返回 error 信息对其解决或者应用 Promse.reject()
    1. 该当适当缩小 try catch 应用,也不要用它来爱护咱们的代码,其可读性和可维护性都不高,当你冀望代码是异样时候,不满足上述 1,2 的情景时候可思考应用。

最初,笔者心愿这篇文章能给到你我一些方向和启发吧,如有疏漏不妥之处,还请不吝赐教!

附笔记链接,浏览往期更多优质文章可移步查看,心愿对你有些许的帮忙,你的点赞是对我最大的激励:

  • https://github.com/Wscats/articles
退出移动版