- 教程
- 文档
【免费下载链接】you-dont-know-js-ru
📚 Russian translation of "You Don't Know JS" book series
导读
本文围绕 "You Don't Know JS: Async & Performance" 系列(本仓库为俄语译本,见 async & performance/README.md)第六章《Benchmarking & Tuning》展开。前五章分别解决了异步/并发层面的"编码模式性能"(第 1~4 章)与程序架构层面的"宏观性能"(第 5 章,详见 async & performance/ch5.md),本章则将镜头对准微性能(micro performance):如何准确、可靠地测量单条表达式/语句的执行速度,判断哪些性能指标真正重要、哪些只是幻觉,以及如何区分二者。读完本文,你将掌握一套从"错误的Date.now()计时法"到"基于 Benchmark.js 的统计可靠测试"、再到"TCO 尾调用优化"的完整方法论。
一、为什么绝大多数人写错了基准测试
1.1 从一段"教科书式错误"的计时代码说起
大多数 JS 开发者如果被要求测量某段操作的执行时间,第一反应几乎都是这样写:
var start = (new Date()).getTime(); // 或 `Date.now()` // do some operation var end = (new Date()).getTime(); console.log( "Duration:", (end - start) );这段代码看似直观,实则充满了陷阱:
- 计时器精度不足:某些平台并没有毫秒级精度,而是以更大的粒度刷新计时器。例如老版本 Windows(以及对应的 IE)只有 15ms 的精度——这意味着被测操作至少要运行 15ms 才能得到一个非
0的读数。报告出0并不代表"不到 1ms",而可能只是"没到 15ms"。 - 单次运行无统计意义:无论报出什么时长,你唯一能确定的是"这一次恰好跑了这么久"。你几乎无法确认它每次都会这么快——引擎或系统在那个瞬间可能恰好没有干扰,也可能恰好有干扰。
- 读数不可信:报出
4也未必代表 4ms,取start/end两个时间戳之间本身就可能夹杂其他延迟。 - 测试过于理想化:JS 引擎可能为这个被隔离的测试用例专门做了优化,而在真实程序中这种优化会被稀释甚至完全失效,导致实际运行比测试慢得多。
结论是:用这种方式做出来的"基准测试"基本无用,而且危险——它不仅给你虚假的信心,也会误导那些不思考测试条件的人。
1.2 重复就能救回来吗?——循环与统计学
"那我在外面套一层循环,让整体测试时间变长不就行了?"
把操作重复 100 次,总共 137ms,除以 100 得单次平均 1.37ms?不对。
- 单纯的数学平均值不足以支撑你对整个应用性能做外推判断。100 次迭代中哪怕只有一两个离群值(高或低),就会扭曲平均值;再把这个结论反复应用到各处,偏差会被无限放大。
- 更合理的是"按时间跑"而不是"按次数跑":让测试循环持续到累计一定时间为止。但跑多久取决于计时器的精度——精度越差,需要跑得越久才能把误差百分比压下来。文中给出两个关键数据:15ms 计时器要把误差压到 1% 以下,每个测试周期需要跑 750ms;而 1ms 计时器只需要 50ms就能获得同样的置信度。
- 单一样本仍然不够:你需要大量样本做平均,还需要了解最慢样本多慢、最快样本多快、两者差距多大——不仅要一个"跑得多快"的数字,还要一个可量化的"这个数字有多可信"的度量。
这些只是起步的底线。换句话说:如果此前你对基准测试的态度不够严肃,那么"你还不了解:什么是正确的基准测试"。
二、Benchmark.js:把统计交给专业工具
标准偏差、方差、误差幅度(margin of error)——这些统计学概念如果你没有真正理解,就没有资格自己手写基准测试逻辑。幸运的是,John-David Dalton 与 Mathias Bynens 等研究者已经写好了统计可靠的基准测试工具Benchmark.js,作者在文中给出的建议非常直接:"就用那个工具。"
2.1 基本用法
function foo() { // operation(s) to test } var bench = new Benchmark( "foo test", // 测试名称 foo, // 被测函数(只测函数体内容) { // .. // 可选额外选项(详见官方文档) } ); bench.hz; // 每秒执行的操作次数(operations per second) bench.stats.moe; // 误差幅度(margin of error) bench.stats.variance; // 样本方差 // ..Benchmark.js 替你处理了搭建"公平、可靠、有效"基准测试所需的全部复杂细节。如果要对比方案 X 与方案 Y,只需把两个测试放进一个Suite(Benchmark.js 的组织单元),让它们头对头运行,再比较统计量得出结论。
2.2 超越浏览器:性能回归测试
Benchmark.js 既能在浏览器中运行(配合后文 jsPerf.com),也能在 Node.js 等非浏览器环境中运行。文中特别提到一个被低估的使用场景:在 Dev/QA 环境中针对应用关键路径(critical path)跑自动化的性能回归测试——就像部署前跑单元测试套件一样,把当前结果与历史基准对比,持续监控性能是改善了还是恶化了。
2.3 Setup/Teardown 的经典陷阱
前文代码中略过了{ .. }选项对象,但其中setup与teardown两个选项值得单独讨论——它们定义了测试用例运行前/后要调用的函数。
极其重要的理解:setup与teardown代码不会在每个测试迭代(iteration)前运行!正确的理解是:存在一个外层循环(重复周期 cycle)和一个内层循环(重复测试迭代)。setup/teardown只在每个外层周期的开始/结束运行一次,而不是在内层循环内部。
看这个例子:
a = a + "w"; b = a.charAt( 1 );你写的setup是:
var a = "x";直觉告诉你每次迭代开始时a都是"x"。不是!它只在每个周期开始时是"x",随后重复的+ "w"拼接会让a越变越长——虽然你只访问1位置的字符"w"。
这个陷阱最常见的翻车场景是 DOM 副作用:你以为父元素每次都是空的,实际它却被不断添加子元素,结果被严重污染。结论:side effect 类操作必须谨慎放进被测代码,或妥善放在 setup 之外。
三、上下文为王(Context Is King)
3.1 20% 的差异真的有意义吗?
假设测试显示 X 每秒执行 10,000,000 次操作,Y 每秒 8,000,000 次。数学上你可以说"Y 比 X 慢 20%"——数学没错,但结论的含金量远低于你的想象:
- 10,000,000 ops/s = 10,000 ops/ms = 10 ops/µs,即单次操作约 0.1µs =100ns。
- 人眼通常无法分辨低于100ms的差异——这比 100ns 慢了一百万倍。
- 即使采用更激进的最新研究结论(人脑最快约 13ms 处理一个事件),X 仍然比人脑可感知的速度快 125,000 倍。
- X 与 Y 之间 2,000,000 ops/s 的差距,折算下来约20ns,在最好情况下也只是人脑可感知间隔的 65 万分之一。
作者毫不客气地给出结论:这些性能差异根本无关紧要。除非该操作要在紧循环里连续执行 650,000 次甚至 5,000,000~10,000,000 次,才勉强接近"可能被感知"的量级。而像++xvsx++这种微型操作的绝大多数基准测试结果,对"应该选 X 还是选 Y"的结论而言基本是废纸。
3.2 引擎优化的黑箱:你测的并不是你写的
你无法可靠地外推"隔离测试里 X 比 Y 快 10µs,所以 X 永远更快"。现代引擎远比这复杂。文中用了一个纯假设的例子:
var twelve = "12"; var foo = "foo"; // test 1 var X1 = parseInt( twelve ); var X2 = parseInt( foo ); // test 2 var Y1 = Number( twelve ); var Y2 = Number( foo );即便你熟悉parseInt(..)与Number(..)的语义差异,结果仍可能出乎意料,因为引擎可能:
- 发现
twelve/foo只被使用一次,于是**内联(inline)**这些常量值,把Number("12")直接替换成12; - 触发死代码消除(dead-code elimination),发现
X、Y变量根本没人用,于是两个测试什么都没干; - 基于"固定输入"做优化——而真实程序中输入是变化的,优化决策可能完全不同甚至不生效;
- 因为基准工具让代码跑了成千上万次才触发优化,而真实程序只跑几百次,引擎判定"不值得优化"。
关键结论不是"不要测试",而是:测试不真实的代码,只能得到不真实的结果。在可行范围内,应该测试真实、非琐碎(non-trivial)的代码片段,并尽量贴近真实运行条件。像++xvsx++这类微基准,几乎可以默认其结论是假的。
四、jsPerf.com:跨环境众包测试
4.1 为什么必须跨环境
单靠 Benchmark.js 在自己的环境里测是不够的:高端台式机的 Chrome 与智能手机上的 Chrome 移动版性能天差地别;满电手机与只剩 2% 电量、正在关闭射频和处理器核心的手机又不一样。要做出"X 比 Y 快"这样跨环境的断言,就必须在尽可能多的真实环境中测试,并对照你的用户画像交叉验证。
jsPerf正是为此而生:它基于前面提到的 Benchmark.js,运行统计准确、可靠的测试,并把测试放在公开 URL 上供人分享;每次运行的结果都被收集并持久化,页面会累计绘制结果曲线供所有人查看。
4.2 创建测试的小技巧
创建测试时初始有两个测试用例框,你可以按需增删。一个实用技巧:如果只想测单一方案(而非头对头),可以在首次创建时给第二个输入框填占位文本,然后编辑测试并留空第二个框将其删除。页面级 setup(导入库、定义工具函数、声明变量等)可在页面初始化时配置;周期级 setup/teardown 的语义与上文 Benchmark.js 一节完全一致。
4.3 理智检验:常见的有缺陷测试
jsPerf 资源虽好,但上面大量已发布的测试在仔细分析后漏洞百出。文中给了三个典型案例:
案例 A:把循环写进被测代码
// Case 1 var x = []; for (var i=0; i<10; i++) { x[i] = "x"; } // Case 2 var x = []; for (var i=0; i<10; i++) { x[x.length] = "x"; } // Case 3 var x = []; for (var i=0; i<10; i++) { x.push( "x" ); }需要反思的点:
- 开发者习惯把自己的
for循环塞进测试,却忘了Benchmark.js 已经替你完成了所有重复——这些for循环极可能是完全多余的噪音。 x的声明与初始化被放进每个测试用例(可能不必要)。回忆前面的陷阱:如果x = []放在setup里,它不会在每个迭代前运行,而是每个周期开头运行一次——x会持续增长得很大,而不是for循环暗示的大小10。那么测试意图是"只测引擎在小数组(size 10)下的行为"吗?如果是,你是否又过度聚焦于内部实现细节?还是测试拥抱了"数组会实际增长得很大"的真实上下文?- 是想测
x.length或x.push(..)追加操作的额外开销吗?这或许是个合理的测试,但push(..)是函数调用,当然比[..]索引访问慢——案例 1、2 比案例 3 更公平。
案例 B:内联函数表达式带来的"苹果对橙子"
// Case 1 var x = ["John","Albert","Sue","Frank","Bob"]; x.sort(); // Case 2 var x = ["John","Albert","Sue","Frank","Bob"]; x.sort( function mySort(a,b){ if (a < b) return -1; if (a > b) return 1; return 0; } );表面意图是测自定义比较器比内置比较器慢多少,但第二个用例把mySort(..)写成内联函数表达式——它不仅测了自定义 JS 函数,还顺带测了每次迭代创建新函数表达式的开销!相关测试表明,仅隔离"创建内联函数表达式 vs 引用预先声明的函数"这一项,前者可能慢2%~20%。更公平的写法是把mySort声明放在页面 setup 里,然后在测试用例中按名称引用:x.sort(mySort)。
案例 C:不同结果直接宣告测试无效
// Case 1 var x = [12,-14,0,3,18,0,2.9]; x.sort(); // Case 2 var x = [12,-14,0,3,18,0,2.9]; x.sort( function mySort(a,b){ return a - b; } );内置sort(..)的比较器会把值强制转成字符串做字典序比较(即做了mySort没有的"额外工作"),导致两个用例输出不同结果:前者得到[-14, 0, 0, 12, 18, 2.9, 3],后者(更符合直觉意图)得到[-14, 0, 0, 2.9, 3, 12, 18]。两个用例结果不同,几乎必然使整个测试失效——你得到的任何数据都是假的。
案例 D:细微的不对称
// Case 1 var x = false; var y = x ? 1 : 2; // Case 2 var x; var y = x ? 1 : 2;意图或许是测? :对非布尔x做布尔强制的开销(参见本书系列 types & grammar 分册)。但微妙的问题在于:用例 1 执行了赋值,用例 2 没有——你实际上在两个用例中做了不对称的工作。为消除这潜在的(虽然微小)偏差:
// Case 1 var x = false; var y = x ? 1 : 2; // Case 2 var x = undefined; var y = x ? 1 : 2;现在两边都有赋值,真正想测的"是否发生强制转换"才被更准确地隔离出来。
五、如何写出好测试
好测试的创作要求对两个用例之间的差异做细致的分析思考:这些差异是有意的还是无意的?
- 有意的差异当然正常且没问题,但无意差异太容易制造并扭曲结果,必须极其小心。
- 你可能确实有意制造了差异,但对其他读者并不显而易见——他们会因此错误地怀疑(或轻信!)你的测试。解决办法是:写更好、更清晰的测试,并花时间用 jsPerf 的 "Description" 字段和代码注释明确记录测试意图,标注出有意差异,帮助他人和未来的自己发现可能扭曲结果的无意差异。
- 把与测试无关的东西在页面或测试 setup 中预先声明,让它们处于被测计时范围之外。
- 与其从真实代码中抠出一小段脱离上下文单独测,不如包含更大(但仍相关)的上下文来测试——这种测试通常更慢,但你在其中发现的任何差异在真实上下文中都更相关。
六、微性能(Microperformance)
6.1 引擎可能运行与你写的不一样的代码
面对性能基准测试,你需要接受的第一件事是:你写的代码并不总是引擎真正运行的代码。第 1 章讨论过编译器的语句重排(statement reordering),这里更进一步:编译器有时不只改变执行顺序,还会改变代码的实质内容。
看这个例子:
var foo = 41; (function(){ (function(){ (function(baz){ var bar = foo + baz; // .. })(1); })(); })();你可能觉得最内层函数对foo的引用需要做三层作用域查找——但《Scope & Closures》分册(见 scope & closures)讲过:编译器通常会缓存这类查找,跨作用域引用foo并不会真的"多花钱"。更深一层:如果编译器发现foo只在这一个地方被引用、且值永远就是41,它完全可以删除foo变量并内联这个值:
(function(){ (function(){ (function(baz){ var bar = 41 + baz; // .. })(1); })(); })();(同理,baz也可能被编译器做类似分析和重写。)当你把 JS 代码看作"给引擎的提示或建议"而非"字面要求"时,就会意识到:对琐碎语法细节的执念,多半是站不住脚的。
再看经典的阶乘递归:
function factorial(n) { if (n < 2) return 1; return n * factorial( n - 1 ); } factorial( 5 ); // 120同样的代码用 C 语言配合高级优化编译,编译器会发现factorial(5)可以直接替换成常量120,把函数和调用整个消除。另外一些引擎有"递归展开(unrolling recursion)"实践,可能把递归重写成循环:
function factorial(n) { if (n < 2) return 1; var res = 1; for (var i=n; i>1; i--) { res *= i; } return res; } factorial( 5 ); // 120所以,如果你曾纠结于n * factorial(n-1)和n *= factorial(--n)谁更快、甚至为此做了基准测试,你可能忽略了:在更大上下文里,引擎可能根本不跑这两行代码——因为它把递归展开了。
6.2--nvsn--:典型的无效执念
--n常被引证为比n--"更快"(理论上在汇编层面少一点工作)。这在现代 JavaScript 中基本是无稽之谈——这类事应该交给引擎处理。对比下面三个for循环:
// Option 1 for (var i=0; i<10; i++) { console.log( i ); } // Option 2 for (var i=0; i<10; ++i) { console.log( i ); } // Option 3 for (var i=-1; ++i<10; ) { console.log( i ); }即便你有些理论认为选项 2 或 3 比选项 1 快一点点(这本身就很可疑),选项 3 因为要配合前置++i而从-1起步,明显更令人困惑;选项 1 与 2 的差异则完全无关紧要。引擎完全可能在看到i++的地方安全地替换成等价的++i——你花在选 A 还是选 B 上的时间纯属浪费。
6.3 缓存x.length:理论很美,实测无效
var x = [ .. ]; // Option 1 for (var i=0; i < x.length; i++) { // .. } // Option 2 for (var i=0, len = x.length; i < len; i++) { // .. }理论是:x.length既然不变,缓存到len可以省掉每次迭代查询的代价。但如果你真的跑基准测试就会发现:理论上听起来不错,实践中任何测量差异在统计上完全无关紧要。而且有证据表明,在 v8 等引擎里,预先缓存长度反而可能让事情稍微变糟。作者的建议很直白:别试图比你的 JS 引擎更聪明,在性能优化这件事上你大概率会输。
6.4 并非所有引擎都一样
不同浏览器里的 JS 引擎都可以"符合规范",但对代码的处理方式却可能截然不同——JS 规范几乎不要求任何性能相关的东西(唯一的例外就是本章后面要讲的 ES6 TCO)。引擎可以自由决定把优化资源倾注给某个操作,而牺牲另一个操作。
社区(尤其是 Node.js 使用者)中有一股潮流:深入分析 v8 的内部实现细节,写出专门适配 v8 的代码。这类努力确实可能获得相当可观的性能回报,但文中提醒我们注意:
- 你的代码真的只打算跑在一个引擎上吗?即便现在只面向 Node.js,能保证 v8 永远是那个引擎吗?几年后会不会选择另一个服务端 JS 平台?届时你当初的"优化"可能在新引擎上变成更慢的写法。
- 即使一直跑在 v8 上,v8 也可能在某个版本改变某些操作的实现,让过去的"快"变成"慢"。
文中举了两个真实的历史教训:
- 字符串拼接的
join("")神话:曾经把多个字符串放进数组再join("")拼接,比直接用+快——这与当时字符串在内存中的存储管理实现细节有关,"最佳实践"随之风靡业界。但后来引擎改变了内部管理方式,专门为+拼接做了优化("沿着牛道铺路"——按既有广泛用法去标准化/优化某种做法),于是一夜之间所有用join(..)拼字符串的存量代码都变成了次优方案。 - Opera 的 String 对象建议:曾有一段时间,Opera 在原始包装对象的装箱/拆箱处理上与其他浏览器不同,于是建议开发者用
String对象而非原始string值来访问length或charAt(..)。这个建议在当时对 Opera 或许正确,但对其他主流浏览器则完全相反——它们恰恰针对string原始值而非对象包装器做了优化。
作者的态度是:纯基于引擎实现细节(尤其是单一引擎的细节)做大规模性能优化要非常谨慎。反过来也要警惕:不要为了绕开某个引擎的性能短板而改代码——历史上 IE 常是这类妥协的牺牲品,但"为一个浏览器的问题改变写法"可能让代码在所有其他浏览器中都次优。更务实的做法是写正确的代码,同时把性能问题反馈给浏览器厂商(多数浏览器都有公开 bug tracker)。作者给出 Tip:只有当某个浏览器的性能问题是真正的"致命阻断器"(show-stopper)而非单纯烦人时才值得绕开,并且要仔细确认该 hack 不会在另一个浏览器中产生明显的负面副作用。毕竟,"没有什么比一个临时 hack 更永久"——你现在为绕过性能 bug 写的代码,很可能活得比浏览器里的那个 bug 还久。
6.5 大局观:关键路径(Critical Path)才是王道
与其纠结微性能细节,不如把注意力放在大局优化上。判断标准首先是你是否知道代码是否处于关键路径上——不在关键路径上的优化几乎不值钱。
著名的 Knuth 名言"过早优化是万恶之源"常被断章取义,文中给出了完整上下文:
程序员浪费了大量时间思考或担心程序中非关键部分的速度,而这些效率尝试在考虑调试和维护时会产生强烈的负面影响。我们应该忘记小的效率,大约 97% 的时间里都是如此:过早优化是万恶之源。然而我们不应该放弃那关键的 3%里的机会。(重点为作者所加)
合理的转述是:"非关键路径优化是万恶之源。"关键在于判断你的代码是否在关键路径上——在,就该优化;不在,就不该。作者甚至提出更绝对的说法:花在关键路径优化上的时间再多都不算浪费(哪怕省下的很少);而花在非关键路径上的优化无论如何都不合理(哪怕省下很多)。
关键路径的例子:会被反复运行的"热"代码,或用户能直接感知的 UX 关键位置——比如动画循环、CSS 样式更新。文中给出一个动画循环里把字符串转数字的例子:
var x = "42"; // 需要数字 `42` // Option 1: 让隐式强制转换自动发生 var y = x / 2; // Option 2: 使用 `parseInt(..)` var y = parseInt( x, 0 ) / 2; // Option 3: 使用 `Number(..)` var y = Number( x ) / 2; // Option 4: 使用 `+` 一元运算符 var y = +x / 2; // Option 5: 使用 `|` 一元运算符 var y = (x | 0) / 2;判断要点:
parseInt(..)能完成任务,但做的工作多得多——它是在"解析"字符串而非单纯"强制转换",因此几乎可以确定是较慢的选项,应尽量避免。但如果x可能是需要解析的值,比如来自 CSS 样式查询的"42px",那parseInt(..)就是唯一合适的选择。Number(..)是函数调用;行为上与+一元运算符等价,但可能需要更多机制来执行函数调用,实际可能略慢——当然引擎也可能识别出这种行为对称性,直接为你内联掉Number(..)(等价于+x)。- 但请记住:在大多数情况下纠结
+xvsx | 0很可能白费力气。这是典型的微性能问题,不应让它支配或降低程序的可读性——在若干性能大致相近的选项中,可读性应该是另一个重要考量。(更完整的类型强制转换机理见 types & grammar 分册。)
七、尾调用优化(Tail Call Optimization,TCO)
7.1 什么是尾调用
ES6 包含一项涉足性能领域的强制性要求:尾调用优化。所谓"尾调用",是指出现在另一个函数"尾部"的函数调用——该调用结束后,除了(可能)返回其结果值外,再无其他事情要做。
function foo(x) { return x; } function bar(y) { return foo( y + 1 ); // 尾调用 } function baz() { return 1 + bar( 40 ); // 不是尾调用 } baz(); // 42foo(y+1)在bar(..)中是尾调用,因为foo(..)结束后bar(..)也就结束了(只是返回foo(..)的结果)。而bar(40)不是尾调用——它结束后其结果还要先加1,baz()才能返回。
7.2 栈帧复用与递归解放
调用一个新函数需要预留一块管理调用栈的内存,称为"栈帧(stack frame)"。上面的片段通常需要同时为baz()、bar(..)、foo(..)各保留一个栈帧。但如果 TCO 能力的引擎识别出foo(y+1)处于尾位置(tail position)——bar(..)基本已经完成——那么调用foo(..)时就不必新建栈帧,而是复用bar(..)的现有栈帧。这不仅更快,而且更省内存。
这种优化在简单片段里无足轻重,但对递归却是天翻地覆的:没有 TCO 时,引擎必须对不同递归深度施加任意(且各不相同)的限制来防止内存耗尽;有了 TCO,处于尾位置的递归调用可以基本无界运行,因为始终只用一个栈帧!
7.3 重写阶乘为 TCO 友好形式
前面那个递归版factorial(..)并不是 TCO 友好的(return n * factorial(n-1)里乘法发生在尾调用之后)。把它改写成累积器(accumulator)风格:
function factorial(n) { function fact(n,res) { if (n < 2) return res; return fact( n - 1, n * res ); } return fact( n, 1 ); } factorial( 5 ); // 120这个版本仍然是递归,但内层两次fact(..)调用都处于尾位置,因此可被 TCO 优化。注意:TCO 只在真正存在尾调用时生效。如果你写的递归函数没有尾调用,性能仍会退化为常规的逐帧分配,引擎对递归深度的限制依然适用。许多递归函数可以像上面这样改写,但需要细心。
7.4 为什么 ES6 要把 TCO 变成"必须"
ES6 之所以要求引擎实现 TCO 而非放任自流,是因为"缺乏 TCO"往往会降低开发者用递归实现某些算法的意愿——他们害怕调用栈深度限制。如果缺乏 TCO 只是让所有情况优雅地退化为较慢性能,ES6 大概不会把它列为硬性要求;正因缺乏 TCO 可能让某些程序变得不可行,它才更像语言的重要特性,而非隐藏的实现细节。
ES6 自此保证:所有 ES6+ 合规浏览器中,开发者都可以依赖这项优化。这是 JS 性能的一次胜利。
延伸阅读:TCO 的完整语法形态(Proper Tail Calls, PTC)、手动重写技巧、运行时特性检测(通过递归 +try..catch探测引擎是否支持 TCO)以及如何据此做元编程取舍,见本系列 es6 & beyond/ch7.md 的 "Tail Call Optimization (TCO)" 一节。
八、总结
- 有效测量一段代码的性能(尤其是与同段代码的另一种写法对比)需要极其细致的关注。
- 不要自己手写统计可靠的基准逻辑——直接用 Benchmark.js;但要注意测试的编排方式,因为太容易构造出"看似有效实则漏洞百出"的测试,哪怕极小的差异也会把结果扭曲到完全不可信。
- 尽可能从尽可能多的不同环境收集测试结果,以消除硬件/设备偏差;jsPerf.com 是众包基准测试运行的绝佳平台。
- 许多常见性能测试不幸地执着于
x++vs++x这类无关紧要的微性能细节。写好测试意味着理解如何聚焦大局:优化关键路径,同时避开"不同引擎实现细节"这类陷阱。 - 尾调用优化(TCO)是 ES6 起的一项强制优化,让某些在 JS 中原本不可能的递归模式变得切实可行——尾位置的函数调用无需额外资源即可执行,引擎不再需要为递归算法人为限制调用栈深度。
本文基于仓库内 async & performance/ch6.md 编写;全系列目录见 async & performance/toc.md,章节导航见 async & performance/README.md。本分册第 5 章关于程序级性能(Web Workers、SIMD、asm.js)的内容可参考 async & performance/ch5.md。
- 教程
- 文档
【免费下载链接】you-dont-know-js-ru
📚 Russian translation of "You Don't Know JS" book series
相关推荐
深入理解JavaScript ES6及新特性——《You Don't Know JS》核心解读
深入理解JavaScript ES6及新特性——《You Don't Know JS》核心解读 JavaScript ES6是JavaScript语言的革命性升
文档教程浏览器内ADB调试:Tango如何革新Android开发体验
浏览器内ADB调试:Tango如何革新Android开发体验 你是否曾为Android开发调试的繁琐环境配置而烦恼?传统ADB客户端需要复杂的本地环境设置,限制
开发工具通信移动开发CLIJetifier-standalone 使用指南:JAR/AAR/ZIP 文件的 AndroidX 转换
Jetifier standalone 使用指南:JAR/AAR/ZIP 文件的 AndroidX 转换 Jetifier standalone 是一款强大的
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考