要写出高效的JavaScript,关键之一就是理解它的工作原理。编写高效代码的方法有无数种,例如,您可以编写对编译器友好的JavaScript代码,以避免让一行简单的代码速度降低7倍。在本文中,我们将重点介绍可以最大限度地减少Javascript代码解析时间的优化方法。让我们进一步缩小范围,只讨论V8,这是为Electron、Node.js和GoogleChrome提供支持的JS引擎。为了理解这些解析友好的优化方法,我们不得不先讨论一下JavaScript的解析过程。基于对代码解析过程的深入理解,我们将概述三种编写更快JavaScript的技术。让我们简单回顾一下JavaScript执行的三个阶段。从源代码到语法树——解析器从源代码生成抽象语法树。Fromsyntaxtreetobytecode-V8的解释器Ignition从语法树生成字节码(这一步在2017年之前不存在,详见本文)。从字节码到机器码——V8的编译器TurboFan从字节码生成图形,用高度优化的机器码替换部分字节码。上面的第二和第三阶段涉及JavaScript编译。在这篇文章中,我们将关注第一阶段并解释它对编写高效JavaScript的影响。我们描述了解析管道,它按照从左到右、从上到下的顺序获取源代码并生成语法树。抽象语法树(AST)。它是在解析器中创建的(图中的蓝色)。扫描器源代码首先被分解成块,每个块可能采用不同的编码,然后字符流将所有块的编码统一为UTF-16。在解析之前,扫描器将UTF-16字符流分解为标记。标记是脚本中具有语义的最小单元。有不同类型的标记,包括空格(用于自动插入分号)、标识符、关键字和代理对(只有当代理对不能被识别为其他东西时才组合成标识符)。然后将这些标记发送到预解析器,然后再发送到解析器。Preparser解析器做的工作量最少,只够跳过传入的源代码并进行惰性解析(而不是完全解析)。预解析器确保输入源代码包含有效语法并生成足够的信息以正确编译外部函数。这个准备好的函数稍后将按需编译。解析在接收到扫描器生成的令牌后,解析器现在需要生成一个中间表示供编译器使用。首先让我们谈谈解析树。分析树或具体语法树(CST)将源语法表示为树。每个叶节点是一个token,每个中间节点代表一个文法规则。在英语中,语法规则是指名词、主语等,而在编程中,语法规则是指表达式。然而,解析树的大小随着程序的大小而迅速增长。相比之下,抽象语法树更加简洁。每个中间节点代表一个结构,例如减法运算(-),并且树不会揭示源代码的所有细节。例如,由括号定义的分组隐含在树结构中。此外,省略了标点符号、分隔符和空格。您可以在此处详细了解AST和CST之间的区别。接下来我们关注AST。下面以JavaScript编写的Fibonacci程序为例:functionfib(n){if(n<=1)returnn;returnfib(n-1)+fib(n-2);}下面的JSON文件就是对应的抽象语法.这是使用ASTExplorer生成的。(如果您对此不熟悉,可以在此处阅读有关如何阅读JSON格式的AST的更多信息)。{"type":"Program","start":0,"end":73,"body":[{"type":"FunctionDeclaration","start":0,"end":73,"id":{"type":"Identifier","start":9,"end":12,"name":"fib"},"expression":false,"generator":false,"async":false,"params":[{"type":"Identifier","start":13,"end":14,"name":"n"}],"body":{"type":"BlockStatement","start":16,"end":73,"body":[{"type":"IfStatement","start":20,"end":41,"test":{"type":"BinaryExpression","start":24,"end":30,"left":{"type":"Identifier","start":24,"end":25,"name":"n"},"operator":"<=","right":{"type":"Literal","start":29,"end":30,"value":1,"raw":"1"}},"结果":{"type":"ReturnStatement","start":32,"end":41,"argument":{"type":"Identifier","start":39,"end":40,"name":"n"}},"alternate":null},{"type":"ReturnStatement","start":44,"end":71,"argument":{"type":"BinaryExpression","start":51,"end":70,"left":{"type":"CallExpression","start":51,"end":59,"callee":{"type":"Identifier","start":51,"end":54,"name":"fib"},"arguments":[{"type":"BinaryExpression","start":55,"end":58,"left":{"type":"Identifier","start":55,"end":56,"name":"n"},"operator":"-","right":{"type":"Literal","start":57,"end":58,"value":1,"raw":"1"}}]},"operator":"+","right":{"type":"CallExpression","start":62,"end":70,"callee":{"type":"Identifier","start":62,"end":65,"name":"fib"},"arguments":[{"type":"BinaryExpression","start":66,"end":69,"left":{"type":"Identifier","start":66,"end":67,"name":"n"},"operator":"-","right":{"type":"Literal""start":68,"end":69,"value":2,"raw":"2"}}]}}}]}}],"sourceType":"module"}(来源:GitHub)上面代码的要点是每个非叶子节点是一个运算符,每个叶子节点是一个操作数。此语法树稍后将作为输入传递给JavaScript,以便接下来的两个阶段执行。优化JavaScript的三个技巧在下面的技巧列表中,我将省略那些已经被广泛使用的技巧,例如缩小代码以最大化信息密度,从而使扫描器更省时。此外,我将跳过范围不大的建议,例如避免使用非ASCII字符。提高解析性能的方法有无数种,所以让我们关注其中应用最广泛的一种。1.尽可能跟随工作线程。阻塞主线程会导致用户交互的延迟,所以主线程的工作应该尽可能的减少。关键是要识别并避免导致主线程中的某些任务长时间运行的解析行为。这种启发式超出了解析器优化的范围。例如,用户控制的JavaScript片段可以使用网络工作者来实现相同的效果。您可以阅读更多有关实时处理应用程序和使用angular中的网络工作者的信息。避免大量使用内联脚本内联脚本是在主线程上处理的,根据前面的说法应该尽量避免。事实上,除了异步和延迟加载之外,任何JavaScript的加载都会阻塞主线程。避免嵌套外部函数的延迟编译也发生在主线程上。但是,如果处理得当,惰性解析可以加快启动速度。如果想强制全量解析,可以使用optimize.js(不再维护)等工具来决定是全量解析还是惰性解析。分解超过100kB的文件将大文件分解成较小的文件以最大化并行脚本加载速度。《2019年的JavaScript性能开销》一文对比了Facebook站点和Reddit站点的文件大小。前者通过在300多个请求中拆分大约6MB的JavaScript,成功地将主线程上的解析和编译工作的比例控制在30%;相反,Reddit主线程上的解析和编译工作达到了近80%。%。2.使用JSON而不是对象字面量——有时在JavaScript中,解析JSON比解析对象字面量更高效。解析基准已经证实了这一点。在不同的主流JavaScript执行引擎中解析一个8MB的文件,前者的解析速度最多可以提升2倍。2019年谷歌开发者大会也讨论了JSON解析如此高效的两个原因:JSON是单个字符串标记,对象字面量可能包含大量嵌套对象和标记;语法是上下文相关的。解析器逐字检查源代码,并不知道代码块是对象文字。左花括号不仅可以表示它是一个对象字面量,还可以表示它是一个解构对象或者箭头函数。但是,值得注意的是JSON.parse也会阻塞主线程。对于大于1MB的文件,可以使用FlatBuffers来提高解析效率。3.最大化代码缓存最后,您可以通过完全避免解析来提高解析效率。对于服务器端编译,WebAssembly(WASM)是一个不错的选择。但是,它不能替代JavaScript。对于JS,更合适的做法是最大化代码缓存。值得注意的是缓存并不是一直生效的。在执行结束之前编译的任何代码都会被缓存——这意味着处理程序、侦听器等不会被缓存。要最大化代码缓存,您必须最大化执行结束前编译的代码量。一种方法是使用立即可执行函数(IIFE)试探法:解析器将试探性地识别这些IIFE函数,稍后将立即编译它们。因此,使用试探法确保在脚本完成执行之前编译函数。此外,缓存是在单个脚本的基础上执行的。这意味着更新脚本将使缓存失效。V8团队建议可以拆分或合并脚本来实现代码缓存。然而,这两个提议是相互矛盾的。你可以阅读《JavaScript开发中的代码缓存》来了解更多关于代码缓存的知识。结论解析时间优化涉及工作线程的惰性解析和通过最大化缓存避免完全解析。了解了V8的解析机制后,我们还可以推导出上面没有提到的其他优化方式。下面是更多了解解析机制的资源,通常适用于V8和JavaScript解析。V8文档V8博客V8-perf额外提示:了解JavaScript错误和性能如何影响您的用户。跟踪生产中的JavaScript异常或错误既费时又伤脑筋。如果您有兴趣监控JavaScript错误和应用程序性能如何影响用户,请尝试LogRocket。LogRocket就像是为网络应用量身定做的DVR(录像机),它可以准确记录您网站上发生的一切。LogRocket可以帮助您统计和报告错误,以查看错误发生的频率以及它们如何影响您的用户群。您可以轻松地重现错误发生时的特定用户会话,以查看导致错误的用户操作。LogRocket可以在您的应用程序上记录请求和响应(包括标头和正文)以及与用户相关的上下文信息,以全面了解问题。它还可以记录页面的HTML和CSS,甚至可以将最复杂的单页应用程序重建为像素完美的视频。如果你想提高你的JavaScript错误监控能力,LogRocket是一个不错的选择。
