JavaScript 启动性能探究

本文讲的是JavaScript 启动性能探究,

作为 web 开发者,都知道 web 项目开发到最后,页面规模很容易变的很大。 但 加载 一个网页远不止从网线上传送字节码那么简单。浏览器下载了页面脚本之后,它还必须解析、解释和运行它们。这篇文章将深入 JavaScript 的这一部分,研究 为什么这一过程会拖慢应用程序的启动,以及 如何 解决。

过去,人们并没有花很多时间优化 JavaScript 的解析、编译步骤。我们总是期望解析器在遇到 script 标签时立即解析和执行代码,但是情况并非如此。 以下是对 V8 引擎工作原理的简要分析:

上图是描述 V8 工作流程的简图,这是我们正在努力达到的理想化流程。

我们来着重分析几个主要阶段。

启动时拖慢应用的是什么?

在启动期间,JavaScript 引擎花费 显著 的时间来解析、编译和执行脚本。这一阶段很关键,因为如果它用时太多,将会 延后用户可以与我们的网站 互动 的时间点。想象一下,如果用户可以看到一个按钮,但很多秒之后才能点击或触摸,这将会 降低用户体验。

V8 的 Chrome Canary 中的 Runtime Call Stats 分析出的流行网站的解析和编译时间。注意,桌面端本就缓慢的解析、编译过程,在一般手机上则需要更长的时间。

启动时间对 性能敏感的 代码很重要。事实上,V8 —— Chrome 的 JavaScript 引擎,在 Facebook,Wikipedia 和 Reddit 等顶级网站上都会花费大量时间来解析和编译脚本:

粉红色区域(JavaScript)表示在 V8 和 Blink 的 C++ 中花费的时间,而橙色和黄色则表示解析和编译所用时间。

在你可能正在使用的 大量 大型网站和框架中,解析和编译也被视为一个性能瓶颈。以下是来自 Facebook 的 Sebastian Markbage 和 Google 的 Rob Wormald 的推文:

Sam Saccone 在 Planning for Performance 中提到了 JS 解析的成本。

随着我们进入一个逐渐移动化的世界,我们有必要知道 解析、编译在手机上花费的时间通常是在桌面上的 2 - 5 倍。高端手机(例如 iPhone 或 Pixel)的表现与 Moto G4 非常不同。这更说明了测试代表性硬件(不仅仅是高端硬件!)的重要性,只有这样,用户体验才不会受到影响。

1 MB 的 JavaScript 文件在不同类别的桌面设备和移动设备上的 解析时间。可以注意到,像 iPhone 7 这样的高端手机的性能和 Macbook Pro 是多么接近,而沿着图表向下看,普通的移动硬件性能则不一样了。

如果应用程序需要传输的文件很大,那么被广泛采用的现代打包技术,如 code-splitting、tree-shaking 和 Service Worker caching 可以产生巨大作用。但是,即使文件很小,如果代码写得不好或者用了很差的第三方库,也会导致主线程在编译或函数调用时被长时间阻塞。 整体衡量和理解真正的瓶颈在哪里是很重要的。

JavaScript 解析、编译是普通网站的瓶颈吗?

『(你说的都对……)但是,我的网站又不是Facebook』,你可能会这样说。 『外面的一般网站的解析和编译时间所占比例有多大?』,你可能会这样问。现在我们来研究一下!

我花了两个月时间测试了一系列(6000+)使用了不同库和框架(如 React、Angular、Ember 和 Vue)构建的大型生产站点的性能。其中大多数测试最近可以在 WebPageTest 重做。所以如果你愿意的话,很容易重做这些测试,或者深入研究这些数据。下面是一些分析结果:

应用在桌面端(使用网线)用 8 秒可以变得可交互,在移动端( 3G 网络下的 Moto G4 )则需要 16 秒。

是什么造成了这一结果?大多数网页应用在桌面端的启动(解析、编译、执行)平均花费了 4 秒。

在移动设备上,解析时间比在桌面设备上多出 36%。

大家都传输了巨大的 JS 打包文件吗?没有我猜到的那么大,但还有改进的余地。 410KB,这是开发者传输的 gzip 压缩后的 JS 文件大小的中位数。这与 HTTPArchive 报告的『平均每网页 JS 大小』420KB 比较符合。最差劲的网站会向任何网页请求者发送高达 10MB 的脚本文件。

脚本大小很重要,但它不是一切。解析和编译时间不一定随着脚本大小的增加线性增加。 较小的 JavaScript 打包文件通常会减少 加载 时间(忽略浏览器,设备和网络连接的影响),但是你的 200KB 的JS文件不等于别人的 200KB,同样大小的文件可以有差别很大的的解析和编译时间。

当前如何测量 JavaScript 的解析和编译

Chrome DevTools

Timeline (Performance panel) > Bottom-Up/Call Tree/Event Log 可以帮助我们深入了解解析、编译花费的时间。为了得到更完整的分析(比如在解析、编译或惰性编译中花费的时间),我们可以打开 V8 的 Runtime Call Stats 。 在 Canary 中,你可以在 Timeline 中的 Experiments > V8 Runtime Call Stats 找到它。

Chrome Tracing

在 Chrome 地址栏输入 about:tracing 之后,这个 Chrome 的底层跟踪工具允许我们使用 disabled-by-default-v8.runtime_stats 类别来深入了解 V8 的时间消耗详情。V8 几天前发布了一个 循序渐进的指导文档 可以帮助你了解它的用法。

WebPageTest

当我们使用 Chrome 的 Capture Dev Tools Timeline 进行跟踪分析时,WebPageTest 的 『Processing Breakdown』 页面包含了对 V8 编译、EvaluateScript 和 FunctionCall 的时间的深入分析。

现在我们还可以通过指定 disabled-by-default-v8.runtime_stats 为自定义跟踪类别来获得 Runtime Call Stats(WPT 的 Pat Meenan 现在默认这样做!)。

如果你想知道如何充分利用这一工具,可以参阅我写的 这篇文档 。

User Timing

也可以像 Nolan Lawson 下面指出的这样,通过 User Timing API 测量解析时间:

第 3 个脚本不重要,第 1 个脚本与第 2 个脚本分开( performance.mark() 在要测试的 script 标签之前执行)是重要的。

使用此方法时,V8 的 preparser 可能会影响后续重新加载。这可以通过在脚本的结尾处附加一个随机字符串来解决,Nolan 在他的 optimize-js benchmarks 中就是这样做的。

我使用类似的方法用 Google Analytics 来测量 JavaScript 解析时间的影响:

自定义的 Google Analytics 维度 『parse』 可让我测量开放环境中访问我的网页的真实用户和设备的 JavaScript 解析时间。

DeviceTiming

Etsy的 DeviceTiming 工具可以帮助测量受控环境中脚本的解析和执行时间。它的工作原理是用测试代码封装本地脚本,以便每次页面被不同的设备(例如笔记本电脑、手机、平板电脑)访问时,我们可以本地比较解析、执行时间。Daniel Espeset的文章Benchmarking JS Parsing and Execution on Mobile Devices 详细介绍了这个工具。

当前可以做什么来减少 JavaScript 解析时间?

  • 传输更少的 JavaScript。 需要解析的脚本越少,我们在解析和编译阶段用的时间就越少。
  • 使用 code-splitting 技术,只发送用户当前路由需要的代码,延迟加载其余代码。 想要避免解析太多的 JS,这可能是最有帮助的方法。类似 PRPL 的模式鼓励这种基于路由的文件分块,现在已经被 Flipkart、Housing.com 和 Twitter 采用。
  • Script streaming: 过去,V8 已经告诉开发者通过 async/defer 选择使用 [Script streaming](https://blog.chromium.org/2015/03/new-javascript-techniques -for-rapid.html) 模式,可以使得解析时间减少 10 - 20%。这允许 HTML 解析器能够至少先检测到资源,将(解析)工作分配给 script streaming 线程,从而不阻塞文档解析。现在,解析器阻塞脚本也有了个模式,不需要做什么额外操作。V8 建议 先加载较大的打包文件,因只有一个脚本流线程(之后会说到这点)
  • 测量依赖的解析成本 ,比如各种库和框架。在可能的情况下,将它们切换为拥有更快解析速度的依赖(例如,把 React 切换为 Preact 或 Inferno,后两者启动时需要更少的字节码,更少的解析、编译时间)。Paul Lewis 在最近的一篇文章中介绍了 framework bootup 成本。Sebastian Markbage 也在推文中 提到,一个测量框架的启动成本的好方法是首先渲染一遍视图,然后删除它,然后再次渲染,这可以告诉你它的启动成本。因为第一次渲染会(解析、编译)唤起一堆懒编译的代码,之后重复渲染一个更大的代码树时可以不用重复进行。

如果我们选择的 JavaScript 框架支持提前编译模式(AoT),也有助于大大减少在解析、编译中花费的时间。Angular 应用就受益于这种模式,看这个例子:

Nolan Lawson 的 『解决 Web 性能危机』

当前 浏览器 为了减少解析和编译时间在做什么?

并不是只有开发者才认为生产环境的应用启动时间是一个需要改进的领域。V8 发现 Octane 作为有历史的测试平台之一,对我们通常测试的 25 个热门网站的真实性能的测试效果不佳。Octane 对于 1)JavaScript 框架(通常代码不是单/多态性)和 2)实际页面应用程序启动(大多数是冷启动代码)来说不是一个好的工具。这两个用例对于web 来说非常重要。也就是说,Octane 不是对所有种类的工作负载都是不合理的。

V8 团队一直努力改善启动时间,并且已经在这些地方取得了一些胜利:

在查看我们的 Octane-Codeload 数据之后,我们估计在许多页面上,V8 解析时间提高了25%:

我们也看到了 Pinterest 网站在这方面的有所进展。在过去几年中,V8 开展了许多其他探索,以减少解析和编译时间。

Code caching

来自文章 使用 V8 的代码缓存

Chrome 42 引入了Code caching —— 它通过在本地存储编译后的代码,使得在用户返回页面时,脚本请求、解析和编译过程都可以跳过。我们注意到,这项变更可让 Chrome 在处理页面后续访问时减少约 40% 的编译时间,但现在我想对这项功能进一步说明:

  • 在 72 小时内执行两次 的脚本才会触发 Code caching。
  • 对于 Service Worker 的脚本:对于在 72 小时内执行两次的脚本触发 Code caching。
  • 对于通过 Service Worker 存储在 Cache Storage 中的脚本:首次执行 脚本就会触发 Code caching。

所以,结论是, 如果我们的代码是在缓存中,V8 会在第 3 次加载时跳过解析和编译。 我们可以在 chrome://flags/#v8-cache-strategies-for-cache-storage 中查看这些差异。 我们还可以设置 js-flags=profile-deserialization 后运行Chrome,看看项目是否从代码缓存中加载(在日志中显示为反序列化事件)。

使用 Code caching 需要注意的一个点是,它只缓存可预编译的代码。通常只是运行一次以设置全局变量的顶层代码。 函数定义通常是惰性编译的,不总是被缓存。 IIFEs(optimize-js 用户)也被在 V8 Code caching 缓存,因为它们也可预编译。

Script Streaming

Script streaming 允许脚本开始下载后在 单独的后台线程 上解析异步或延迟脚本,从而将页面加载速度提高了多达 10%。如前所述,这同样适用于 同步 脚本。

自从该功能首次引入以来,V8 已经切换到允许 所有脚本、甚至 是 src="" 的解析器阻塞脚本在后台线程上解析,所以所有人都应该在这里看到一些成果。唯一需要注意的是,这里只有一个后台线程,所以把大的、关键的脚本先分配给它很重要。在这里思考任何潜在的优化都很重要。

经验之谈是,把 defer 脚本放在 里,这样 V8 就可以早发现资源,然后在用台线程解析它。

可以使用 DevTools Timeline 检查是否选择了正确的脚本被流式传输 —— 如果有一个大脚本占用了解析时间,那么(通常)确保它被流式传输接收是有意义的。

更好地解析和编译

研发一个更轻量、更快的解析器的工作正在进行,新的解析器会更消耗内存,并且更有效地利用数据结构。今天,V8 的主线程闪避的 最大 原因是非线性解析成本。来看一个 UMD 的片段:

(function (global, module) { … })(this, function module() { my functions })

V8 不会知道 modules 是否一定是需要的,所以当主脚本编译时不会编译它。当我们决定编译 modules 时,我们需要重新解析所有的内部函数。这就是 V8 的解析时间非线性的原因。深度为 n 的每个函数都被解析 n 次,并导致闪避。

V8 已经在致力于在初始编译期间收集内部函数的信息,从而任何未来的编译都可以 忽略 它们的内部函数。对于 module 风格函数,这应该会导致很大的性能改进。

参阅 ‘The V8 Parser(s) — Design, Challenges, and Parsing JavaScript Better’ 以获得更多信息.

V8 也在探索在启动期间将 JavaScript 编译的部分分配到 后台线程。

预编译 JavaScript?

每隔几年,就会有提供一种方法 预编译 脚本的引擎出现,以帮助我们不浪费时间解析或编译代码。它们的想法是,如果不预解析、编译代码,一个构建时或服务器端工具就能直接生成字节码,我们会看到启动速度的巨大提高。我认为,传输字节码将会增加加载时间(文件会更大),可能需要对代码进行签名并做一些安全处理。V8 的立场是,现在探索避免内部重新解析将有助于看到很大的进步,这是预编译可能不能提供的,但同时我们会对可以获得更快的启动时间的想法持开放讨论的态度。也就是说,当开发者在 Service Worker 中更新站点时,V8 正在探索更积极地编译和缓存脚本代码,我们希望这些工作获得一些成果。

我们与 Facebook 和 Akamai 在 BlinkOn 7 讨论了预编译,我的笔记可以在 这里 找到。

Optimize JS 惰性解析的括号「hack」

像 V8 这样的 JavaScript 引擎有一个惰性解析启发式方法,在进行一轮完整的解析之前,它们会预先解析我们脚本中的大多数函数(例如检查语法错误)。这是因为大多数页面都有懒惰执行的 JS 函数。

预解析可以通过仅检查浏览器需要了解的函数的最小集合来加快启动时间。这与 IIFE 有所分歧,虽然引擎尝试跳过对它们的预解析,但是启发式并不总是可靠的,这就是 optimize-js 等工具发挥作用的地方。

optimize-js 提前解析脚本,在它知道(或通过启发式假设)的函数将立即执行的地方插入括号来使其获得 更快的执行。对一些函数插入括号是稳妥的(比如带有 ! 的 IIFE)。其它的就是基于启发式的(例如在 Browserify 或 Webpack 包中,假定所有模块都急切加载,就不一定适用这种情况)。总而言之,V8 希望这样的 hack 不再被需要,但现在我们可以认为这是一个优化,如果我们知道你在做什么。

V8 也在努力降低编译器判断错误的情况下的成本,这也应该减少对括号的需要

总结

启动性能很重要 较长的解析、编译和执行时间组合起来会变成希望快速启动的页面的真正瓶颈。你应该 测量 你的网页在此阶段花费的时间,探索你可以做什么以使其更快。

我们将尽我们所能从我们的角度继续努力提高 V8 启动性能。 这是我们的承诺;)也希望你们有一个快乐的提高性能的过程!

阅读更多

向 V8 (Toon Verwaest, Camillo Bruni, Benedikt Meurer, Marja Hölttä, Seth Thompson), Nolan Lawson (MS Edge), Malte Ubl (AMP), Tim Kadlec (Synk), Gray Norton (Chrome DX), Paul Lewis, Matt Gaunt and Rob Wormald (Angular) 致谢,同时感谢他们对本文的修订






原文发布时间为:2017年2月16日


本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。

时间: 2024-10-26 05:17:38

JavaScript 启动性能探究的相关文章

JavaScript 启动性能瓶颈分析与解决方案

在 Web 开发中,随着需求的增加与代码库的扩张,我们最终发布的 Web 页面也逐渐膨胀.不过这种膨胀远不止意味着占据更多的传输带宽,其还意味着用户浏览网页时可能更差劲的性能体验.浏览器在下载完某个页面依赖的脚本之后,其还需要经过语法分析.解释与运行这些步骤.而本文则会深入分析浏览器对于 JavaScript 的这些处理流程,挖掘出那些影响你应用启动时间的罪魁祸首,并且根据我个人的经验提出相对应的解决方案.回顾过去,我们还没有专门地考虑过如何去优化 JavaScript 解析/编译这些步骤;我们

用Runtime Syp调整Eclipse的启动性能,第2部分

Runtime Spy 是 Eclipse.org 提供的核心工具 (Core Tools)之一,它是特 别设计的一个透视图及一组视图,用于帮助您找到并诊断插件启动性能问题.其 中的一个案例研究说明了Runtime Spy 如何用于提高 IBM WebSphere Studio Application Developer 的启动性能.上一篇文章,也就是 第1 部分,对 Runtime Spy 进行了介绍. 阅读完本系列文章的 第 1 部分 后,您应该已经对 Runtime Spy 如何来帮 助您

CLR全面透彻解析: 提高应用程序启动性能

由于等待应用程序启动是令许多用户都感到沮丧的一件事情,因此,侧重于提高客户端应用程序的启 动性能将极大增强客户的第一印象,并使他们对您的努力成果印象深刻.同时,鉴于启动性能对用户非常 重要,所以值得研究一下其影响因素,这样才能避免最常见的错误. 应用程序启动通常分为冷启动和热启动.在托管应用程序环境中,冷启动是指 Microsoft .NET Framework 系统程序集和应用程序代码均不在内存中时,因而需要从磁盘提取它们.热启动则是指应用程 序的后续启动,或者当大部分系统代码因之前由另一托管

Windows7快速启动“性能监视器”的方法

在平时测试过程中,经常需要查看"性能监视器"这个工具.Windows7下的性能监视器工具更是得到了增强,非常好用. 这里介绍一下快速启动"性能监视器"的方法: 1.按Win+R键打开运行对话框 2.输入"perfmon", 然后回车即可. 如图: 执行后打开的"性能监视器"如图:         注:更多精彩教程请关注三联windows7教程栏目,三联电脑办公群:189034526欢迎你的加入

JavaScript提升性能的常用技巧总结【经典】_javascript技巧

本文讲述了JavaScript提升性能的常用技巧.分享给大家供大家参考,具体如下: 1.注意作用域 随着作用域链中的作用域数量的增加,访问当前作用域以外的变量的时间也在增加.访问全局变量总是要比访问局部变量慢,因为要遍历作用域链.  1). 避免全局查找   将在一个函数中会多次用到的全局对象存储为局部变量总是没错的. 2). 避免 with 语句  with会创建自己的作用域,因此会增加其中执行代码的作用域链的长度. 2.选择正确的方法 性能问题的一部分是和用于解决问题的算法或者方法有关的.

JavaScript脚本性能优化注意事项_javascript技巧

循环是很常用的一个控制结构,大部分东西要依靠它来完成,在JavaScript中,我们可以使用for(;;),while(),for(in)三种循环,事实上,这三种循环中for(in)的效率极差,因为他需要查询散列键,只要可以就应该尽量少用.for(;;)和while循环的性能应该说基本(平时使用时)等价. 而事实上,如何使用这两个循环,则有很大讲究.我在测试中有些很有意思的情况,见附录.最后得出的结论是: 如果是循环变量递增或递减,不要单独对循环变量赋值,应该在它最后一次读取的时候使用嵌套的++

分享几个Javascript 的性能优化点

Javascript 的性能优化点 a.慎用Eval 谨记:有"eval"的代码比没有"eval"的代码要慢上 100 倍以上.主要原因是:JavaScript 代码在执行前会进行类似"预编译"的操作:首先会创建一个当前执行环境下的活动对象,并将那些用 var 申明的变量设置为活动对象的属性,但是此时这些变量的赋值都是 undefined,并将那些以 function 定义的函数也添加为活动对象的属性,而且它们的值正是函数的定义.但是,如果你使用

Android Studio直接运行影响启动性能

Android Studio直接运行影响启动性能 之前eclipse时代,测试空应用启动性能时,都是直接在IDE中启动,这样修改起来方便.到了Android Studio时代,这个习惯被我保持下来了.结果就被Instant Run功能给小小坑了一下. 从性能日志上看,发现空应用在handleBindApplication的时候,在MTK6753芯片上费时60多毫秒,展讯9832芯片上超过100毫秒.而空应用,既没有Application的onCreate,又没有installProvider之类

使用Function.apply()的参数数组化来提高 JavaScript程序性能的技巧_javascript技巧

我们再来聊聊Function.apply() 在提升程序性能方面的技巧. 我们先从 Math.max() 函数说起, Math.max后面可以接任意个参数,最后返回所有参数中的最大值. 比如 alert(Math.max(5,8)) //8 alert(Math.max(5,7,9,3,1,6)) //9 但是在很多情况下,我们需要找出数组中最大的元素. var arr=[5,7,9,1] alert(Math.max(arr)) // 这样却是不行的.一定要这样写 function getMa