技术进阶:通过来JavaScript 性能调优提高 Web 应用性能

ngyx2624 6年前
   <h2>前言</h2>    <p>现在的互联网应用中,在Web 开发中经常会遇到性能的问题,尤其是针对当今的 Web2.0 +应用。JavaScript 是当今使用最为广泛的 Web 开发语言,Web 应用的性能问题很大一部分都是由程序员写的 JavaScript 脚本性能不佳所造成的,里面包括了 JavaScript 语言本身的性能问题,以及其与 DOM 交互时的性能问题。本文主要来探讨一下如何尽可能多的避免这类问题,从而最大限度的提高 Web 应用的性能。</p>    <h2>1.JavaScript 性能调优</h2>    <p>JavaScript 语言由于它的单线程和解释执行的两个特点,决定了它本身有很多地方有性能问题,所以可改进的地方有不少。</p>    <h3>1.1 eval 的问题:</h3>    <p>比较下述代码:</p>    <p>清单 1. eval 的问题</p>    <p><img src="https://simg.open-open.com/show/d709e3f6fc093f0085fb3be4a4f82e83.jpg"></p>    <p>有"eval"的代码比没有"eval"的代码要慢上 100 倍以上。</p>    <p>主要原因是:JavaScript 代码在执行前会进行类似"预编译"的操作:首先会创建一个当前执行环境下的活动对象,并将那些用 var 申明的变量设置为活动对象的属性,但是此时这些变量的赋值都是 undefined,并将那些以 function 定义的函数也添加为活动对象的属性,而且它们的值正是函数的定义。但是,如果你使用了"eval",则"eval"中的代码(实际上为字符串)无法预先识别其上下文,无法被提前解析和优化,即无法进行预编译的操作。所以,其性能也会大幅度降低。</p>    <h3>1.2 Function 的用法</h3>    <p>比较下述代码:</p>    <p>清单 2. function 的用法</p>    <p><img src="https://simg.open-open.com/show/d2b8ed01c6a6e0edffc2055f9bb0dee4.jpg"></p>    <p>这里类似之前提到的"eval"方法,这里"func1"的效率会比"func2"的效率差很多,所以推荐使用第二种方式。</p>    <h3>1.3函数的作用域链(scope chain)</h3>    <p>JavaScript 代码解释执行,在进入函数内部时,它会预先分析当前的变量,并将这些变量归入不同的层级(level),一般情况下:</p>    <p>局部变量放入层级 1(浅),全局变量放入层级 2(深)。如果进入"with"或"try &ndash; catch"代码块,则会增加新的层级,即将"with"或"catch"里的变量放入最浅层(层 1),并将之前的层级依次加深。</p>    <p>参考如下代码:</p>    <p>清单 3. 函数作用域链</p>    <p><img src="https://simg.open-open.com/show/a8cecb64d924f0541e498cc966ce474c.jpg"></p>    <p>这里我们可以看到,"images","widget","combination"属于局部变量,在层 1。"document","myObj"属于全局变量,在层 2。</p>    <p>变量所在的层越浅,访问(读取或修改)速度越快,层越深,访问速度越慢。所以这里对"images","widget","combination"的访问速度比"document","myObj"要快一些。所以推荐尽量使用局部变量,可见如下代码:</p>    <p>清单 4. 使用局部变量</p>    <p><img src="https://simg.open-open.com/show/d72af530b7e76327dbf69041c9cb7d29.jpg"></p>    <p>我们用局部变量"doc"取代全局变量"document",这样可以改进性能,尤其是对于大量使用全局变量的函数里面。</p>    <p>再看如下代码:</p>    <p>清单 5. 慎用 with</p>    <p><img src="https://simg.open-open.com/show/c96ec6db3b963a0a40d2f97cce2655e5.jpg"></p>    <p>加上"with"关键字,我们让代码更加简洁清晰了,但是这样做性能会受影响。正如之前说的,当我们进入"with"代码块时,"combination"便从原来的层 1 变到了层 2,这样,效率会大打折扣。所以比较一下,还是使用原来的代码:</p>    <p>清单 6. 改进 with</p>    <p><img src="https://simg.open-open.com/show/97bfb9dcc68624fc8d93cb30c07f3191.jpg"></p>    <p>但是这样并不是最好的方式,JavaScript 有个特点,对于 object 对象来说,其属性访问层级越深,效率越低,比如这里的"myObj"已经访问到了第 3 层,我们可以这样改进一下:</p>    <p>清单 7. 缩小对象访问层级</p>    <p><img src="https://simg.open-open.com/show/af4b7bde2c36745d99fae87d68171643.jpg"></p>    <p>我们用局部变量来代替"myObj"的第 2 层的"container"对象。如果有大量的这种对对象深层属性的访问,可以参照以上方式提高性能。</p>    <h3>1.4字符串(String)相关</h3>    <p>字符串拼接</p>    <p>经常看到这样的代码:</p>    <p>清单 8. 字符串简单拼接</p>    <p>str += "str1" + "str2"</p>    <p>这是我们拼接字符串常用的方式,但是这种方式会有一些临时变量的创建和销毁,影响性能,所以推荐使用如下方式拼接:</p>    <p>清单 9. 字符串数组方式拼接</p>    <p>var str_array = [];</p>    <p>str_array.push("str1");</p>    <p>str_array.push("str2");</p>    <p>str = str_array.join("");</p>    <p>这里我们利用数组(array)的"join"方法实现字符串的拼接,尤其是程序的老版本的 Internet Explore(IE6)上运行时,会有非常明显的性能上的改进。</p>    <p>当然,最新的浏览器(如火狐 Firefox3+,IE8+ 等等)对字符串的拼接做了优化,我们也可以这样写:</p>    <p>清单 10. 字符串快速拼接</p>    <p>str +="str1"</p>    <p>str +="str2"</p>    <p>新的浏览器对"+="做了优化,性能略快于数组的"join"方法。在不久的将来更新版本浏览器可能对"+"也会做优化,所以那时我们可以直接写:str += "str1" + "str2"。</p>    <p>隐式类型转换</p>    <p>参考如下代码:</p>    <p>清单 11. 隐式类型转换</p>    <p><img src="https://simg.open-open.com/show/b872f01d8bb441d3f2ee47792778c820.jpg"></p>    <p>这里我们在每个循环时都会调用字符串的"charAt"方法,但是由于我们是将常量"12345678"赋值给"str",所以"str"这里事实上并不是一个字符串对象,当它每次调用"charAt"函数时,都会临时构造值为"12345678"的字符串对象,然后调用"charAt"方法,最后再释放这个字符串临时对象。我们可以做一些改进:</p>    <p>清单 12. 避免隐式类型转换</p>    <p><img src="https://simg.open-open.com/show/619f7881afa5ab8614f6e3946c798664.jpg"></p>    <p>这样一来,变量"str"作为一个字符串对象,就不会有这种隐式类型转换的过程了,这样一来,效率会显著提高。</p>    <p>字符串匹配</p>    <p>JavaScript 有 RegExp 对象,支持对字符串的正则表达式匹配。是一个很好的工具,但是它的性能并不是非常理想。相反,字符串对象(String)本身的一些基本方法的效率是非常高的,比如"substring","indexOf","charAt"等等,在我们需要用正则表达式匹配字符串时,可以考虑一下:</p>    <p>1)是否能够通过字符串对象本身支持的基本方法解决问题。</p>    <p>2)是否可以通过"substring"来缩小需要用正则表达式的范围。</p>    <p>这些方式都能够有效的提高程序的效率。</p>    <p>关于正则表达式对象,还有一点需要注意,参考如下代码:</p>    <p>清单 13. 正则表达式</p>    <p><img src="https://simg.open-open.com/show/8a1aeff2601054989386ef0754a7e03e.jpg"></p>    <p>这里,我们往"match"方法传入"/^s*extras/"是会影响效率的,它会构建临时值为"/^s*extras/"的正则表达式对象,执行"match"方法,然后销毁临时的正则表达式对象。我们可以这样做:</p>    <p>清单 14. 利用变量</p>    <p><img src="https://simg.open-open.com/show/67005f5eb3f36e67db2d70d04df9b9ea.jpg"></p>    <p>这样就不会有临时对象了。</p>    <p>setTimeout 和 setInterval</p>    <p>"setTimeout"和"setInterval"这两个函数可以接受字符串变量,但是会带来与之前谈到的"eval"类似的性能问题,所以建议还是直接传入函数对象本身。</p>    <p>利用提前退出</p>    <p>参考如下两段代码:</p>    <p>清单 15. 利用提前退出</p>    <p><img src="https://simg.open-open.com/show/c94858f3e7d25a3382696dc3f6e975b2.jpg"></p>    <p>代码 2 多了一个对"name.indexOf( … )"的判断,这使得程序每次走到这一段时会先执行"indexOf"的判断,再执行后面的"match",在"indexOf"比"match"效率高很多的前提下,这样做会减少"match"的执行次数,从而一定程度的提高效率。</p>    <h2>2. DOM 操作性能调优</h2>    <p>JavaScript 的开发离不开 DOM 的操作,所以对 DOM 操作的性能调优在 Web 开发中也是非常重要的。</p>    <h3>2.1 Repaint 和 Reflow</h3>    <p>Repaint 也叫 Redraw,它指的是一种不会影响当前 DOM 的结构和布局的一种重绘动作。如下动作会产生 Repaint 动作:</p>    <p>不可见到可见(visibility 样式属性)</p>    <p>颜色或图片变化(background, border-color, color 样式属性)</p>    <p>不改变页面元素大小,形状和位置,但改变其外观的变化</p>    <p>Reflow 比起 Repaint 来讲就是一种更加显著的变化了。它主要发生在 DOM 树被操作的时候,任何改变 DOM 的结构和布局都会产生 Reflow。但一个元素的 Reflow 操作发生时,它的所有父元素和子元素都会放生 Reflow,最后 Reflow 必然会导致 Repaint 的产生。举例说明,如下动作会产生 Repaint 动作:</p>    <p>浏览器窗口的变化</p>    <p>DOM 节点的添加删除操作</p>    <p>一些改变页面元素大小,形状和位置的操作的触发</p>    <h3>2.2 减少 Reflow</h3>    <p>通过 Reflow 和 Repaint 的介绍可知,每次 Reflow 比其 Repaint 会带来更多的资源消耗,我们应该尽量减少 Reflow 的发生,或者将其转化为只会触发 Repaint 操作的代码。</p>    <p>参考如下代码:</p>    <p>清单 16. reflow 介绍</p>    <p><img src="https://simg.open-open.com/show/f31c959854295cee3990bc6885e9863f.jpg"></p>    <p>这是我们经常接触的代码了,但是这段代码会产生 3 次 reflow。再看如下代码:</p>    <p>清单 17. 减少 reflow</p>    <p><img src="https://simg.open-open.com/show/641d87e43d8835c0f28031c5cc61bad7.jpg"></p>    <p>这里便只有一次 reflow,所以我们推荐这种 DOM 节点操作的方式。</p>    <p>关于上述较少 Reflow 操作的解决方案,还有一种可以参考的模式:</p>    <p>清单 18. 利用 display 减少 reflow</p>    <p><img src="https://simg.open-open.com/show/619f7881afa5ab8614f6e3946c798664.jpg"></p>    <p>先隐藏 pDiv,再显示,这样,隐藏和显示之间的操作便不会产生任何的 Reflow,提高了效率。</p>    <h3>2.3特殊测量属性和方法</h3>    <p>DOM 元素里面有一些特殊的测量属性的访问和方法的调用,也会触发 Reflow,比较典型的就是"offsetWidth"属性和"getComputedStyle"方法。</p>    <p>图 1. 特殊测量属性和方法</p>    <p><img src="https://simg.open-open.com/show/6276bbac98bdb9141d9f1db7ed759a76.jpg"></p>    <p>这些测量属性和方法大致有这些:</p>    <p>&middot; offsetLeft</p>    <p>&middot; offsetTop</p>    <p>&middot; offsetHeight</p>    <p>&middot; offsetWidth</p>    <p>&middot; scrollTop/Left/Width/Height</p>    <p>&middot; clientTop/Left/Width/Height</p>    <p>&middot; getComputedStyle()</p>    <p>&middot; currentStyle(in IE))</p>    <p>这些属性和方法的访问和调用,都会触发 Reflow 的产生,我们应该尽量减少对这些属性和方法的访问和调用,参考如下代码:</p>    <p>清单 19. 特殊测量属性</p>    <p><img src="https://simg.open-open.com/show/2cb65aff184de1b8b50600aabeab105e.jpg"></p>    <p>这里我们可以用临时变量将"offsetWidth"的值缓存起来,这样就不用每次访问"offsetWidth"属性。这种方式在循环里面非常适用,可以极大地提高性能。</p>    <h3>2.4 样式相关</h3>    <p>我们肯定经常见到如下的代码:</p>    <p>清单 20. 样式相关</p>    <p><img src="https://simg.open-open.com/show/3ddf2a9d77bd53c8394c6665a7b13c37.jpg"></p>    <p>但是可以看到,这里的每一个样式的改变,都会产生 Reflow。需要减少这种情况的发生,我们可以这样做:</p>    <p>解决方案 1:</p>    <p>清单 21. className 解决方案</p>    <p><img src="https://simg.open-open.com/show/d0f41414b0c18982a51040a502ed6364.jpg"></p>    <p>用 class 替代 style,可以将原有的所有 Reflow 或 Repaint 的次数都缩减到一个。</p>    <p>解决方案 2:</p>    <p>清单 22. cssText 解决方案</p>    <p><img src="https://simg.open-open.com/show/c2ffd1236d7dcfc0eba97060500d677c.jpg"></p>    <p>一次性设置所有样式,也是减少 Reflow 提高性能的方法。</p>    <h3>2.5 XPath</h3>    <p>一个页面上往往包含 1000 多页面元素,在定位具体元素的时候,往往需要一定的时间。如果用 id 或 name 定位可能效率不会太慢,如果用元素的一些其他属性(比如 className 等等)定位,可能效率有不理想了。有的可能只能通过遍历所有元素(getElementsByTagName)然后过滤才能找到相应元素,这就更加低效了,这里我们推荐使用 XPath 查找元素,这是很多浏览器本身支持的功能。</p>    <p>清单 23. XPath 解决方案</p>    <p><img src="https://simg.open-open.com/show/2bbcb737b4449342b8cfc86385ac49ca.jpg"></p>    <p>浏览器 XPath 的搜索引擎会优化搜索效率,大大缩短结果返回时间。</p>    <h3>2.6 HTMLCollection 对象</h3>    <p>这是一类特殊的对象,它们有点像数组,但不完全是数组。下述方法的返回值一般都是 HTMLCollection 对象:</p>    <p>&middot; document.images, document.forms</p>    <p>&middot; getElementsByTagName()</p>    <p>&middot; getElementsByClassName()</p>    <p>这些 HTMLCollection 对象并不是一个固定的值,而是一个动态的结果。它们是一些比较特殊的查询的返回值,在如下情况下,它们会重新执行之前的查询而得到新的返回值(查询结果),虽然多数情况下会和前一次或几次的返回值都一样:</p>    <p>&middot; Length 属性</p>    <p>&middot; 具体的某个成员</p>    <p>所以,HTMLCollection 对象对这些属性和成员的访问,比起数组来要慢很多。当然也有例外,Opera 和 Safari 对这种情况就处理的很好,不会有太大性能问题。</p>    <p>参考如下代码:</p>    <p>清单 24. HTMLConnection 对象</p>    <p><img src="https://simg.open-open.com/show/e19b08a34a77fdeaf7e8a8f35c027101.jpg"></p>    <p>上述两端代码,下面的效率比起上面一段要慢很多,因为每一个循环都会有"items.length"的触发,也就会导致"document.getElementsByTagName(..)"方法的再次调用,这便是效率便会大幅度下降的原因。我们可以这样解决:</p>    <p>清单 25. HTMLConnection 对象解决方案</p>    <p><img src="https://simg.open-open.com/show/20b2ff9ccd7b1213bc13990bb96f6bed.jpg"></p>    <p>这样一来,效率基本与普通数组一样。</p>    <h3>2.7 动态创建 script 标签</h3>    <p>加载并执行一段 JavaScript 脚本是需要一定时间的,在我们的程序中,有时候有些 JavaScript 脚本被加载后基本没有被使用过 (比如:脚本里的函数从来没有被调用等等)。加载这些脚本只会占用 CPU 时间和增加内存消耗,降低 Web 应用的性能。所以推荐动态的加载 JavaScript 脚本文件,尤其是那些内容较多,消耗资源较大的脚本文件。</p>    <p>清单 26. 创建 script 标签</p>    <p><img src="https://simg.open-open.com/show/e00cd502a38c1dbdfe5eb9195e211b72.jpg"></p>    <h2>写在最后</h2>    <p>这篇文章介绍了 Web 开发中关于性能方面需要注意的一些小细节,从 JavaScript 本身着手,介绍了 JavaScript 中需要避免的一些函数的使用和编程规则,比如 eval 的弊端,function scope chain 以及 String 的用法等等,也分享了一些比较推荐的做法,并扩展到 JavaScript 对 DOM 操作的性能调优,比如利用 Repaint 和 Reflow 的机制,如何使用特殊测量属性,样式相关的性能调优以及 HTMLCollection 对象的原理和使用小技巧。这些小细节我们可以在开发过程中尽量注意一下,以尽可能多的提高我们 Web 应用的性能。</p>    <p> </p>    <p>来自:http://news.51cto.com/art/201802/565921.htm</p>    <p> </p>