您的位置:首页 » 分类: JavaScript & 前端资源 & ES2015 (ES6) & webpack » 文章: JavaScript性能优化技巧:概述

JavaScript性能优化技巧:概述

小编推荐:掘金是一个高质量的技术社区,从 ECMAScript 6 到 Vue.js,性能优化到开源类库,让你不错过前端开发的每一个技术干货。各大应用市场搜索「掘金」即可下载APP,技术干货尽在掌握..

在这篇文章中,涵盖了很多广泛而又多变的环境。我们将尽量坚持“使用工具,而不是规则”的原则,把JS的流行词汇保持在最低限度。由于我们无法在2000字的文章中涵盖与 JS 性能表现相关的所有内容,所以请确保你阅读文中提到的引用资料,并在之后你自己进行了相应的研究。

但在我们深入讨论细节之前,让我们通过回答以下问题来更深入地理解:什么是高性能的 JavaScript,以及它如何适应更广泛的 web 性能指标?

设置场景

首先,让我们从以下方面着手:如果你只是在桌面设备上进行测试,那么你将会排除超过50%的用户。

2016年11月,移动用户的数量已经超过桌面用户的数量

这种趋势只会继续增长,因为新兴市场用户首选是低于100美元的 Android 设备。桌面设备作为访问互联网的主要设备的时代已经结束,接下来的10亿互联网用户将主要通过移动设备访问你的网站。

在 Chrome DevTools 设备模式(device mode) 不是在真实设备上测试的有效替代品。虽然使用CPU和网络节流(network throttling)有所帮助但这种方式非常粗暴,往往事与愿违。请在真实设备上进行测试。

即使您正在真实的移动设备上进行测试,你可能会这样做你的品牌打新的600美元的旗舰手机。问题是,这不是你的用户所拥有的设备。这款设备的中位设备是 Moto G1 ,该设备的内存不足1GB,CPU和GPU非常弱。

让我们来看看在解析一个相同的 JS 包时,他们的堆积图是怎么样的呢。

JS解析阶段跨设备的速度比较

图片来自:Time spent in JS parse & eval for average JS

哎哟。虽然这张图片只涵盖了 JS 的解析和编译时间(稍后会详细介绍),不是一项综合性能,但却与性能紧密相关,可以被视为通用的 JS 性能指标。

引用 Bruce Lawson 的话说:“这是万维网,而不是富有的西方网络“。 所以,你的 web 性能目标是一个比你的 MacBook 或 iPhone 慢25倍的设备。让我们沉浸一下。但情况变得更糟。让我们看看我们真正的目标是什么。

什么是真正的高性能的 JS 代码?

现在我们知道我们的目标平台是什么,我们就可以回答下一个问题:什么是高性能的JS代码?

虽然没有高性能代码的绝的定义,但是我们确实有一个以用户为中心的性能模型,我们可以把它作为参考:RAIL模型

RAIL: Respond/Animate/Idle work/Load

图片来自:Planning for Performance: PRPL

Respond (响应)

如果你的应用能在100毫秒内响应用户操作,用户就能感觉到立即响应。这适用于可点击的元素,但不适用于滚动或拖动。

Animate (动画)

在60Hz的显示器上,我们希望在动画和滚动时,以每秒60帧的帧为目标。结果是每帧大约16ms。在16ms的预算中,您实际拥有8个10ms来完成所有的工作,其余部分被浏览器内部和其他的差异占用。

Idle work

如果你有一个昂贵的且持续运行的任务,请确保将其分割为较小的块,以允许主线程对用户输入做出反应。你不应该有任务延迟超过50ms的用户输入。

Load (加载)

你应该将页面加载定位在1000毫秒以内。所有事情结束了,你的用户开始变得焦躁等待。这在移动设备上是一个非常难达到的目标,因为它涉及到页面交互,不只是把它绘制在屏幕和和可滚动的页面上。在实践中,它甚至更少:

JavaScript 1000ms budget

图片来自:Fast By Default: Modern Loading Best Practices (Chrome Dev Summit 2017)

在实践中,目标是 5s 的交互时间。 这是Chrome在其 Lighthouse 审核中使用的内容。

现在我们已经知道了这些指标,让我们来看看一些统计数据

  • 如果一个移动站点的加载时间超过3秒,那么就会有53%的访问被放弃。
  • 2人中有1个人希望在不到2秒的时间内加载页面。
  • 77% 的移动站点在3G网络上加载,时间超过10秒。
  • 在3G网络上,移动站点的平均加载时间是19秒。

还有一点,由Addy Osmani提供:

  • 应用程序在桌面设备上(使用网线)在8秒内可交互,在手机上为16秒( Moto G4 使用3G网络)
  • 在中位设备上,开发者为他们的页面装载了 410KB gzip 的 JS 压缩文件。

是不是感到十分沮丧啊? 那好,接下来让我们开始修复这些问题。

上下文就是一切

您可能已经注意到,主要的瓶颈是加载你网站所需的时间。具体来说,就是 JavaScript 的下载,解析,编译和执行时间。对于这些好像没有什么好办法来优化,除了加载更少的 JavaScript 和更聪明的加载。

但是,除了启动网站之外,你的代码所做的实际工作又如何呢?必须有一些性能上的好处,对吧?

在深入优化代码之前,请考虑你在构建什么。你在构建一个框架还是一个 VDOM 库?你的代码是否需要每秒执行数千次操作?你是否正在做一个时序要求严格的库来处理用户输入 和/或 动画?如果没有,你可能想要把你的时间和精力转移到更加影响性能的地方。

当然,我并不是说编写性能代码并不重要,但它通常不会对项目的宏观计划产生什么影响,尤其是在讨论微观优化时。因此,在通过比较来自JSperf.com 的结果,进入关于 .map.forEachfor 循环的 Stack Overflow 争论之前,一定要看到整片森林,而不只是树木。50k ops/s 听起来好于 1k ops/s 50倍,但在大多数情况下,它并没有带来什么不同。

解析,编译和执行

从根本上说,大多数非性能的 JS 的问题不是运行代码本身,而是在代码开始执行之前必须要执行的所有步骤。

我们这里讨论的是抽象层次。计算机中的 CPU 运行机器代码。你在计算机上运行的大多数代码都是编译后的二进制格式。(我说的是代码,而不是程序,考虑到现在所有的电子应用程序。)也就是说,除了所有操作系统级别的抽象之外,它都是在你的硬件上本地运行的,不需要任何准备工作。

JavaScript 并不是预编译的。它通过浏览器的可读代码到达(通过一个相对较慢的网络),所有意图和目的,为您的JS程序的“操作系统”。

首先需要对代码进行解析,然后读取并将其转换为可用于编译的计算机可索引结构。然后,它会被编译成字节码和最后是机器码,然后它才可以在你的 设备/浏览器 上执行。

另一件非常重要的事情是,JavaScript是单线程的,并且在浏览器的主线程上运行。这意味着一次只能运行一个进程。如果你的 DevTools 的性能时间线充满黄色峰值,表示浏览器正在以 100%的使用率运行你的 CPU ,你会有长/丢帧,卡顿滚动和所有其他的讨厌情况。

DevTools 中一个长时间运行的任务

图片来自:When everything’s important, nothing is!

所以在JS开始工作之前,需要完成所有这些工作。在 Chrome 的 V8 引擎中,解析和编译占JS执行总时间的50%。

移动和桌面设备中 JS解析时间的比较

图片来自:JavaScript Start-up Performance.

您应该在这一章节中理解两件事情:

  1. JS 解析时间与 JS 包大小虽然不一定是线性比例,但是你装载的 JavaScript 肯定是越少越好。
  2. 你使用的每个JS框架(React,Vue,Angular,Preact …) 都是另一个抽象层次 (除非它是一个预编译的框架,比如 Svelte )。它不仅会增加您的包大小,而且还会减慢你的代码,因为你不会直接与浏览器对话。

有一些方法可以缓解这种情况,比如使用 service workers 在后台和另一个线程中执行任务,使用 asm.js 编写代码,更容易编译成机器指令,但这是一个完全不同的话题。

但是,你可以做什么呢?避免使用一切的 JS 动画框架,并阅读什么情况触发浏览器重绘(paint) 和 布局(layout)(愚人码头注:这是高开销的两个点)。只有在完全没有办法使用常规的 CSS transitions 和 animations 来实现动画时,才能使用这些库。

即使他们可能使用 CSS transitions ,合成属性和requestAnimationFrame(),他们仍然在主线程的JS上运行。他们基本上只是每 16ms 用内联样式修改你的 DOM ,因为他们没有别的办法可以做到这一点。你需要确保你所有的JS都会在每帧 8ms 以内完成,以保持动画的平滑。

另一方面,CSS animations 和 transitions ,会在 GPU 的主线上运行,如果能够高效执行,则不会导致重新布局(relayouts)/重排(reflows)。

考虑到大多数动画都是在加载或用户交互过程中运行的,这可以给你的web应用程序提供急需的喘息空间。

Web Animations API 是一个即将推出的功能集合,它可以让你在主线程上做性能的JS动画,但是现在,你要坚持 CSS transitions 和像 FLIP 这样的技术。

包的尺寸就是一切

今天,一切都是关于 bundles(包) 的。Bower的时代和几十个 <script> 标签在放在</body> 结束标签前的形式几乎已经消失了。

现在,不管你在npm上发现了什么闪亮的新玩具,都可以通过 npm install 安装,通过 Webpack 将它们打包在一起放在一个 1MB 的JS文件中,并在用户的数据计划中对你的用户进行攻击。

现在所有关于npm安装在NPM上找到的任何闪亮的新玩具,将它们与Webpack捆绑在一个巨大的单个1MB JS文件中,并迫使用户浏览器装载大文件。

尝试装载少量的JS。你的项目可能不需要整个 Lodash 库。你绝对需要使用JS框架吗?如果是,你有没有考虑过使用 React 以外的东西?如 Preact 或 HyperHTML ,它们的大小不到 React 的 1/20 。你需要 TweenMax 的滚动到顶部的动画吗?npm 的便利性 和 框架中孤立组件有一个缺点:开发人员对问题的第一反应就是把更多的 JS 扔在项目中。当你只有一把锤子的时候,一切看起来像钉子。

当你清除哪些无用的代码,并且减少 JS 装载时,试着把它更聪明一些。当你需要的时候,在把你需要的东西装载进来。

Webpack 3具有 惊人 的能力,称为代码分割动态导入。它不需要将所有的JS模块打包到一个单独的 app.js 包中,它可以使用 import() 语法自动地分割代码,并异步加载。

你不需要使用框架,组件和客户端路由来获得它的好处。 假设你有一个驱动 .mega-widget 的复杂代码片断,可以在任意数量的页面上。你可以简单地在你的主JS文件中编写下如下内容:

if (document.querySelector('.mega-widget')) {
    import('./mega-widget');
}

如果您的应用程序在页面上找到该小部件,即 .mega-widget ,它将动态加载所需的支持代码。否则,一切都很好。

另外,Webpack需要自己的运行时间来工作,并将其注入到它生成的所有 .js 文件中。如果你使用 commonChunks 插件,您可以使用以下内容 将运行时提取到其自己的块(chunks)中

new webpack.optimize.CommonsChunkPlugin({
  name: 'runtime',
}),

它会将运行时从所有其他块(chunks)中剥离出来,放到它自己的文件中,在这种情况下命名为 runtime.js。只要确保在你的主 JS 包之前加载它就可以了。 例如:

它将把运行时从所有其他块中剥离出来,放到它自己的文件中,在这种情况下,命名为runtime.js。请确保在您的主JS包之前加载它。例如

<script src="runtime.js">
<script src="main-bundle.js">

然后就是 编译代码 和 polyfills 的事情了。 如果你正在编写现代 JavaScript (ES6+),你可能使用 Babel 将其转换为 ES5 兼容代码。由于所有的冗余,转译不仅增加了文件的大小,而且还增加了复杂性,与原生 ES6+ 代码相比,ES5 兼容代码的 性能也有所下降

除此之外,您可能正在使用 babel-polyfill 包 和 whatwg-fetch 来修复旧浏览器中缺失的功能。那么,如果你使用 async/await 编写代码,你也使用 generators(生成器)编译时所需要包括的 regenerator-runtime

关键是,为了支持更老的浏览器,你的 JS 包中几乎添加了10万字节的数据,这不仅是一个巨大的文件尺寸,而且对于解析和执行来说也是一个巨大的开销。

然而,这对于使用现代浏览器的人来说就是毫无意义的惩罚。我使用的方法是 Philip Walton 在 这篇文章 中提到的方法,是创建两个单独的包,并有条件地加载它们。通过 Babel 的 babel-preset-env 使这个问题变得简单。例如,你有一个专门用于支持 IE 11 的独立包,另一个没有 polyfills 的独立包专门用于支持最新版本的现代浏览器。

一种肮脏但有效的方法是将以下内容放置在一个内嵌脚本中:

(function() {
  try {
    new Function('async () => {}')();
  } catch (error) {
    // create script tag pointing to legacy-bundle.js;
    return;
  }
  // create script tag pointing to modern-bundle.js;;
})();

如果浏览器不能对 async 函数进行求值,那么我们假设它是一个旧的浏览器,那么需要装载一个 polyfilled 包。否则,用户就会得到整洁和现代的不同版本。

结论

我们希望您从本文中获得的好处是,JS的开销是高昂的,你应该谨慎使用。

请确保在真实的网络环境下,在低端设备上测试你的网站性能。你的站点应该尽快加载并且可交互。这意味着需要减少装载JS,并以任何必要的方式更快地加载和执行。您的代码应该总是被压缩,分割成更小的、可管理的包,并且尽可能地异步加载。在服务器端,确保启用 HTTP/2 ,以更快的并行传输,且 gzip/Brotli 压缩,以大幅减少 JS 的传输字节。

推荐阅读:

推荐一篇相关的文章:移动端页面的 JavaScript 开销

欢迎留言讨论。

原文链接:https://www.sitepoint.com/javascript-performance-optimization-tips-an-overview/

正文完。下面还有一个推广让最好的人才遇见更好的机会!

互联网行业的年轻人,他们面对着怎样的职业瓶颈、困惑与未来选择?过去,这鲜有人关心。资深的职场人,也多半优先选择熟人去推荐机会。

100offer致力于改变现状,帮互联网行业最好的人才发现更好的机会。使用 100offer.com 或 100offer App ,可以一周内获得中国、美国等数千家优质企业的工作机会。

马上去遇见更好的机会
推广结束

关注WEB前端开发官方公众号

关注国内外最新最好的前端开发技术干货,获取最新前端开发资讯,致力于打造高质量的前端技术分享公众号

发表评论

电子邮件地址不会被公开。 必填项已用*标注