在这篇文章中,涵盖了很多广泛而又多变的环境。我们将尽量坚持“使用工具,而不是规则”的原则,把JS的流行词汇保持在最低限度。由于我们无法在2000字的文章中涵盖与 JS 性能表现相关的所有内容,所以请确保你阅读文中提到的引用资料,并在之后你自己进行了相应的研究。
但在我们深入讨论细节之前,让我们通过回答以下问题来更深入地理解:什么是高性能的 JavaScript,以及它如何适应更广泛的 web 性能指标?
首先,让我们从以下方面着手:如果你只是在桌面设备上进行测试,那么你将会排除超过50%的用户。
这种趋势只会继续增长,因为新兴市场用户首选是低于100美元的 Android 设备。桌面设备作为访问互联网的主要设备的时代已经结束,接下来的10亿互联网用户将主要通过移动设备访问你的网站。
在 Chrome DevTools 设备模式(device mode) 不是在真实设备上测试的有效替代品。虽然使用CPU和网络节流(network throttling)有所帮助但这种方式非常粗暴,往往事与愿违。请在真实设备上进行测试。
即使您正在真实的移动设备上进行测试,你可能会这样做你的品牌打新的600美元的旗舰手机。问题是,这不是你的用户所拥有的设备。这款设备的中位设备是 Moto G1 ,该设备的内存不足1GB,CPU和GPU非常弱。
让我们来看看在解析一个相同的 JS 包时,他们的堆积图是怎么样的呢。
图片来自:Time spent in JS parse & eval for average JS
哎哟。虽然这张图片只涵盖了 JS 的解析和编译时间(稍后会详细介绍),不是一项综合性能,但却与性能紧密相关,可以被视为通用的 JS 性能指标。
引用 Bruce Lawson 的话说:“这是万维网,而不是富有的西方网络“。 所以,你的 web 性能目标是一个比你的 MacBook 或 iPhone 慢25倍的设备。让我们沉浸一下。但情况变得更糟。让我们看看我们真正的目标是什么。
现在我们知道我们的目标平台是什么,我们就可以回答下一个问题:什么是高性能的JS代码?
虽然没有高性能代码的绝的定义,但是我们确实有一个以用户为中心的性能模型,我们可以把它作为参考:RAIL模型。
图片来自:Planning for Performance: PRPL
如果你的应用能在100毫秒内响应用户操作,用户就能感觉到立即响应。这适用于可点击的元素,但不适用于滚动或拖动。
在60Hz的显示器上,我们希望在动画和滚动时,以每秒60帧的帧为目标。结果是每帧大约16ms。在16ms的预算中,您实际拥有8个10ms来完成所有的工作,其余部分被浏览器内部和其他的差异占用。
如果你有一个昂贵的且持续运行的任务,请确保将其分割为较小的块,以允许主线程对用户输入做出反应。你不应该有任务延迟超过50ms的用户输入。
你应该将页面加载定位在1000毫秒以内。所有事情结束了,你的用户开始变得焦躁等待。这在移动设备上是一个非常难达到的目标,因为它涉及到页面交互,不只是把它绘制在屏幕和和可滚动的页面上。在实践中,它甚至更少:
图片来自:Fast By Default: Modern Loading Best Practices (Chrome Dev Summit 2017)
在实践中,目标是 5s 的交互时间。 这是Chrome在其 Lighthouse 审核中使用的内容。
现在我们已经知道了这些指标,让我们来看看一些统计数据:
还有一点,由Addy Osmani提供:
是不是感到十分沮丧啊? 那好,接下来让我们开始修复这些问题。
您可能已经注意到,主要的瓶颈是加载你网站所需的时间。具体来说,就是 JavaScript 的下载,解析,编译和执行时间。对于这些好像没有什么好办法来优化,除了加载更少的 JavaScript 和更聪明的加载。
但是,除了启动网站之外,你的代码所做的实际工作又如何呢?必须有一些性能上的好处,对吧?
在深入优化代码之前,请考虑你在构建什么。你在构建一个框架还是一个 VDOM 库?你的代码是否需要每秒执行数千次操作?你是否正在做一个时序要求严格的库来处理用户输入 和/或 动画?如果没有,你可能想要把你的时间和精力转移到更加影响性能的地方。
当然,我并不是说编写性能代码并不重要,但它通常不会对项目的宏观计划产生什么影响,尤其是在讨论微观优化时。因此,在通过比较来自JSperf.com 的结果,进入关于
、
- .map
和
- .forEach
循环的 Stack Overflow 争论之前,一定要看到整片森林,而不只是树木。50k ops/s 听起来好于 1k ops/s 50倍,但在大多数情况下,它并没有带来什么不同。
- for
从根本上说,大多数非性能的 JS 的问题不是运行代码本身,而是在代码开始执行之前必须要执行的所有步骤。
我们这里讨论的是抽象层次。计算机中的 CPU 运行机器代码。你在计算机上运行的大多数代码都是编译后的二进制格式。(我说的是代码,而不是程序,考虑到现在所有的电子应用程序。)也就是说,除了所有操作系统级别的抽象之外,它都是在你的硬件上本地运行的,不需要任何准备工作。
JavaScript 并不是预编译的。它通过浏览器的可读代码到达(通过一个相对较慢的网络),所有意图和目的,为您的JS程序的“操作系统”。
首先需要对代码进行解析,然后读取并将其转换为可用于编译的计算机可索引结构。然后,它会被编译成字节码和最后是机器码,然后它才可以在你的 设备/浏览器 上执行。
另一件非常重要的事情是,JavaScript是单线程的,并且在浏览器的主线程上运行。这意味着一次只能运行一个进程。如果你的 DevTools 的性能时间线充满黄色峰值,表示浏览器正在以 100%的使用率运行你的 CPU ,你会有长/丢帧,卡顿滚动和所有其他的讨厌情况。
图片来自:When everything’s important, nothing is!
所以在JS开始工作之前,需要完成所有这些工作。在 Chrome 的 V8 引擎中,解析和编译占JS执行总时间的50%。
图片来自:JavaScript Start-up Performance.
您应该在这一章节中理解两件事情:
有一些方法可以缓解这种情况,比如使用 service workers 在后台和另一个线程中执行任务,使用 asm.js 编写代码,更容易编译成机器指令,但这是一个完全不同的话题。
但是,你可以做什么呢?避免使用一切的 JS 动画框架,并阅读什么情况触发浏览器重绘(paint) 和 布局(layout)(愚人码头注:这是高开销的两个点)。只有在完全没有办法使用常规的 CSS transitions 和 animations 来实现动画时,才能使用这些库。
即使他们可能使用 CSS transitions ,合成属性和
,他们仍然在主线程的JS上运行。他们基本上只是每 16ms 用内联样式修改你的 DOM ,因为他们没有别的办法可以做到这一点。你需要确保你所有的JS都会在每帧 8ms 以内完成,以保持动画的平滑。
- requestAnimationFrame()
另一方面,CSS animations 和 transitions ,会在 GPU 的主线上运行,如果能够高效执行,则不会导致重新布局(relayouts)/重排(reflows)。
考虑到大多数动画都是在加载或用户交互过程中运行的,这可以给你的web应用程序提供急需的喘息空间。
Web Animations API 是一个即将推出的功能集合,它可以让你在主线程上做性能的JS动画,但是现在,你要坚持 CSS transitions 和像 FLIP 这样的技术。
今天,一切都是关于 bundles(包) 的。Bower的时代和几十个
标签在放在
- <script>
结束标签前的形式几乎已经消失了。
- </body>
现在,不管你在npm上发现了什么闪亮的新玩具,都可以通过
安装,通过 Webpack 将它们打包在一起放在一个 1MB 的JS文件中,并在用户的数据计划中对你的用户进行攻击。
- npm install
现在所有关于npm安装在NPM上找到的任何闪亮的新玩具,将它们与Webpack捆绑在一个巨大的单个1MB JS文件中,并迫使用户浏览器装载大文件。
尝试装载少量的JS。你的项目可能不需要整个 Lodash 库。你绝对需要使用JS框架吗?如果是,你有没有考虑过使用 React 以外的东西?如 Preact 或 Hyperhtml ,它们的大小不到 React 的 1/20 。你需要 TweenMax 的滚动到顶部的动画吗?npm 的便利性 和 框架中孤立组件有一个缺点:开发人员对问题的第一反应就是把更多的 JS 扔在项目中。当你只有一把锤子的时候,一切看起来像钉子。
当你清除哪些无用的代码,并且减少 JS 装载时,试着把它更聪明一些。当你需要的时候,在把你需要的东西装载进来。
Webpack 3具有 惊人 的能力,称为代码分割和动态导入。它不需要将所有的JS模块打包到一个单独的
包中,它可以使用
- app.js
语法自动地分割代码,并异步加载。
- import()
你不需要使用框架,组件和客户端路由来获得它的好处。 假设你有一个驱动
的复杂代码片断,可以在任意数量的页面上。你可以简单地在你的主JS文件中编写下如下内容:
- .mega-widget
- if (document.querySelector('.mega-widget')) {
- import('./mega-widget');
- }
如果您的应用程序在页面上找到该小部件,即
,它将动态加载所需的支持代码。否则,一切都很好。
- .mega-widget
另外,Webpack需要自己的运行时间来工作,并将其注入到它生成的所有
文件中。如果你使用
- .js
插件,您可以使用以下内容 将运行时提取到其自己的块(chunks)中 :
- commonChunks
- new webpack.optimize.CommonsChunkPlugin({
- name: 'runtime',
- }),
它会将运行时从所有其他块(chunks)中剥离出来,放到它自己的文件中,在这种情况下命名为
。只要确保在你的主 JS 包之前加载它就可以了。 例如:
- runtime.js
它将把运行时从所有其他块中剥离出来,放到它自己的文件中,在这种情况下,命名为runtime.js。请确保在您的主JS包之前加载它。例如
- <script src="runtime.js">
- < script src = "main-bundle.js" >
然后就是 编译代码 和 polyfills 的事情了。 如果你正在编写现代 JavaScript (ES6+),你可能使用 Babel 将其转换为 ES5 兼容代码。由于所有的冗余,转译不仅增加了文件的大小,而且还增加了复杂性,与原生 ES6+ 代码相比,ES5 兼容代码的 性能也有所下降 。
除此之外,您可能正在使用
包 和
- babel-polyfill
来修复旧浏览器中缺失的功能。那么,如果你使用
- whatwg-fetch
编写代码,你也使用 generators(生成器)编译时所需要包括的
- async/await
…
- regenerator-runtime
关键是,为了支持更老的浏览器,你的 JS 包中几乎添加了10万字节的数据,这不仅是一个巨大的文件尺寸,而且对于解析和执行来说也是一个巨大的开销。
然而,这对于使用现代浏览器的人来说就是毫无意义的惩罚。我使用的方法是 Philip Walton 在 这篇文章 中提到的方法,是创建两个单独的包,并有条件地加载它们。通过 Babel 的
使这个问题变得简单。例如,你有一个专门用于支持 IE 11 的独立包,另一个没有 polyfills 的独立包专门用于支持最新版本的现代浏览器。
- babel-preset-env
一种肮脏但有效的方法是将以下内容放置在一个内嵌脚本中:
- (function() {
- try {
- new Function('async () => {}')();
- } catch(error) {
- // create script tag pointing to legacy-bundle.js;
- return;
- }
- // create script tag pointing to modern-bundle.js;;
- })();
如果浏览器不能对
函数进行求值,那么我们假设它是一个旧的浏览器,那么需要装载一个 polyfilled 包。否则,用户就会得到整洁和现代的不同版本。
- async
我们希望您从本文中获得的好处是,JS的开销是高昂的,你应该谨慎使用。
请确保在真实的网络环境下,在低端设备上测试你的网站性能。你的站点应该尽快加载并且可交互。这意味着需要减少装载JS,并以任何必要的方式更快地加载和执行。您的代码应该总是被压缩,分割成更小的、可管理的包,并且尽可能地异步加载。在服务器端,确保启用 HTTP/2 ,以更快的并行传输,且 gzip/Brotli 压缩,以大幅减少 JS 的传输字节。
推荐一篇相关的文章:移动端页面的 JavaScript 开销
欢迎留言讨论。
来源: http://www.css88.com/archives/8437