大多数浏览器都是用单一进程处理UI界面的刷新和JavaScript的脚本执行,因此同一时间只能作一件事,Javascript执行过程耗时越久,浏览器等待响应的时间就越长。javascript
因此,HTML页面在遇到php
尽管减小Javascript文件的大小并限制HTTP请求次数仅仅只是第一步,下载单个较大的Javascript脚本执行也许要锁死大量的事件,因此无阻塞的脚本的意义在于页面加载完成以后再下载脚本。css
<script defer>
这是告知,延迟脚本内的内容不会更改DOM,只有IE 4+和Firefox 3.5+浏览器支持。html
defer意味着脚本会先下载,但只有到DOM加载完成以前才会执行,不与页面的其余资源冲突,可并行。java
这种方法的优势是,能够下载Javascript代码但不当即执行,并且几乎适用全部主流浏览器。node
局限性在于Javascript文件必须与所请求的页面处于相同的域,因此Javascript文件不能从CDN下载。web
向页面中添加大量的JavaScript的推荐作法只需两步:先添加动态加载所须要的代码,再加载初始化页面所须要的代码。ajax
前者代码精简,执行很快。正则表达式
<script type="text/javascript"> function loadScript(url, callback) { var script = document.createElement("script"); script.type = "text/javascript"; if (script.readyState) { script.onreadstatechange = function() { if (script.readyState == "loaded" || script.readyState = "complete") { script.onreadystatechange = null; callback(); } } } else { script.onload = function() { callback() } } script.src = url; document.getElementsByTagName("head")[0].appendChild(script); } loadScript("the-rest.js", function() { Application.init(); }); </script>
数据的存储位置不一样,代码执行时的数据检索速度也不一样。算法
对于Javascript来讲,有下面四种基础的数据存取位置。
字面量
字面量只表明自身,不存储在特定位置。JS中的字面量有:字符串、数字、布尔值、对象、数组、函数、正则表达式,以及特殊的null和undefined值。
本地变量
开发人员使用关键字var定义的数据存储单元。
数组元素
存储在JS数组对象内部,以数字做为索引。
对象成员
存储在JS对象内部,以字符串做为索引。
做用域概念对于理解Javascript相当重要,不只在性能方面,还在功能方面。
每一个函数都是Function的实例,Function对象与其余对象同样,拥有能够编写的属性以及,一系列只供JavaScript引擎存储的内部属性,其中一个是[[scope]]。
[[scope]]包含了一个函数被建立的做用域中对象的集合。
在函数执行过程当中,每遇到一个变量,都会经历一次标识符解析过程以及从哪里获取存储数据。若是当前执行环境(做用域头)找不到该变量,就搜索下一个做用域,若是都找不到则为undefined。
标识符所在的位置越深,读写速度也就越慢。因此,读取局部变量时最快的,而读取全局变量时最慢的。
对此有个经验法则:若是某个跨做用域的值在函数中被引用一次以上,那么就把它存储到局部变量里。
通常来讲,一个执行环境的做用域链是不会改变的。可是,有两个语句能够在执行时临时改变做用域链。
一个是with语句,另外一个是try-catch语句。
with(context),with语句有一个问题,那就是with()里的参数做为做用域链头后,函数的局部变量都会变成第二个做用域链对象中,这样每次访问都得访问两次。
try-catch的catch子句也有这种效果,它把一个异常对象推到做用域链首部,可是子句执行完毕,做用域链就会返回以前的状态。
with、try-catch和eval()都是动态做用域。
闭包是JS最强大的特性之一,它容许函数访问局部做用于以外的数据。
一般执行环境一销毁,活动对象也应该销毁,可是由于闭包中,对活动对象的引用依旧存在,因此活动对象并不会被销毁,所以也须要更高的内存开销。
JS对象基于原型,实例属性proto指向原型对象且只对开发者可见。
hasOwnProperty()区分原型属性和实例属性。
很少解释。
不太常见的写法:window.location.href,嵌套成员会致使JS引擎搜索全部对象成员。嵌套得越深,读取速度就会越慢。
很少解释
在JS中,数据存储的位置会对代码总体性能产生重大的影响。数据存储有4种方法:字面量、变量、数组项和对象成员。它们有着各自的性能特色。
首先必须先明确一点:用脚本进行DOM操做的代价很是昂贵。
DOM至关于浏览器HTML文档,XML文档与JS的程序接口(API),与语言无关。因此DOM与JS之间的交流消耗费用也就越高。
通用的经验法则:减小访问DOM的次数,把运算尽可能留在ECMAScript这端来处理。
innerHTML非标准可是支持性良好,在老浏览器中innerHTML比DOM更加高效,可是innerHTML最好与数组结合起来使用。这样效率会更高。
element.cloneNode()
方法克隆节点。
HTML集合是包含了DOM节点引用的类数组对象。如下方法的返回值就是一个集合。
类数组对象没有push.slice等方法,可是有length属性且可遍历。
HTML集合与文档时刻保持链接,因此须要最新的消息须要时刻查询。
在循环语句中读取数组的length是不推荐的作法,最好是把数组的长度存储在一个局部变量中。
第一优化原则是把集合存储在局部变量中,并把length缓存在循环外部,而后用局部变量替代这些须要屡次读取的元素。
举个例子:
// 较慢 function collectionGlobal() { var coll = document.getElementsByTagName('div'), len = coll.length, name = ''; for (var count = 0; count < len; count++) { name = document.getElementsByTagName('div')[count].nodeName; name = document.getElementsByTagName('div')[count].nodeType } } // 很快 function collectionGlobal() { var coll = document.getElementsByTagName('div'), len = coll.length, name = '', el = null; for (var count = 0; count < len; count++) { el = coll[count]; name = el.nodeName; name = el.nodeType } }
一般你须要从某一个DOM元素开始,操做周围的元素,或者递归查找全部子节点。你可使用childNodes获得元素集合,或者用nextSibling来获取每一个相邻元素。
DOM元素属性诸如childNodes,firstChild和nextSibling并不区分元素节点和其余类型节点,若是要过滤的话,实际上是没必要要的DOM操做。
如今能区分元素节点和其余节点的DOM属性以下:
属性名 | 被替代的属性 |
---|---|
children | childNodes |
childElementCount | childNodes.length |
firstElementChild | firstChild |
lastElementChild | lastChild |
nextElementSibling | nextSibling |
previousElementSibling | previousSibling |
children属性的支持率较高。
document.querySelectorAll(‘#menu a’);
document.querySelector(”) 选择匹配的第一个节点
浏览器下载完页面中的全部组件,以后会解析并生成两个内部数据结构:
DOM树
表示页面结构
渲染树
表示DOM节点如何显示
DOM树中全部须要显示的节点在渲染树中至少存在一个对应的节点(隐藏的DOM元素在渲染树中没有对应的节点)。
一旦DOM树与渲染树构建完成,浏览器就开始绘制页面元素。
当DOM的变化影响了元素的几何属性(宽和高)——好比改变边框宽度或给段落增长文字,致使行数增长——浏览器须要从新计算元素的几何属性,一样其余元素的集合属性位置也会所以受到影响。浏览器会使渲染树中受到影响的部分失效,并从新构造渲染树,这个过程被称为重排。
完成重排后,浏览器会从新绘制受影响的部分到屏幕中,这个过程被称为重绘。有些样式的改变,好比背景的变化不影响到布局,因此只会重绘。
因为每次重排都会产生计算消耗,大多数浏览器经过队列化修改并批量执行来优化重排过程。然而,你可能会不自以为强制刷新队列并要求计划任务马上执行。获取布局信息的操做会致使列队刷新,好比:
- offsetTop,offsetLeft…
- scrollTop,scrollLeft…
- clientTop,clientLeft…
- getComputedStyle()
由于以上方法要求返回最新的布局信息,因此浏览器不得不把“待处理变化”触发重排。
el.style.cssText = "";
修改样式信息。el.className = "";
当你须要对DOM元素进行一系列操做时,能够经过如下步骤来减小重绘和重排的次数:
1.使元素脱离文档流
2.对其应用多重改变
3.把元素带回文档
有三种方法可使DOM脱离文档:
- 隐藏元素,应用修改,从新显示
- 使用文档片断在当前DOM以外构建一个子树,再把它拷贝回文档
- 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素
举个例子:
<ul id="mylist"> <li><a></a></li> <li><a></a></li> <li><a></a></li> </ul> // 假设要将更多的数据插入到这个列表中 var data = [ { "name": "Nicholas", "url": "...." } ] // 用来更新指定节点数据的通用函数 function appendDataToElement(appendToElement, data) { var a, li; for (var i = 0, max = data.length; i < max; i++) { a = document.createElement('a'); a.href = data[i].url; a.appendChild(document.createTextNode(data[i].name)); li = document.createElement('li'); li.appendChild(a); appendToElement.appendChild(li); } } // 不考虑重排 var ul = document.getElementById('mylist'); appendDataToElement(ul, data); // 第一种方法 var ul = document.getElementById('mylist'); ul.style.display = 'none'; appendDataToElement(ul, data); ul.style.display = 'block'; // 第二种方法(推荐) var fragment = document.createDocumentFragment(); appendDataToElement(fragment, data); document.getElementById('mylist').appendChild(fragment); // 第三种方法 var old = document.getElementById('mylist'); var clone = old.cloneNode(true); appendDataToElement(clone, data); old.parentNode.replaceChild(clone, old);
对于须要操做布局信息的地方,最好用一个局部变量来缓存,否则查询一次就会刷新一次渲染队列并应用全部变动。
通常而言动画的展开与隐藏会影响大量元素的重排,使用一下步骤能够避免页面中大部分重排。
尽可能避免使用hover。
绑定的事件越多,代价也越大,要么加剧了页面负担,要么是增长了运行期的执行时间。
在父元素绑定个事件,利用冒泡。
访问和操做DOM是现代Web应用的重要部分。但每次穿越链接ECMAScript和DOM两个岛屿之间的桥梁,都会被收取“过桥费”。
- 最小化DOM访问次数,尽量在JavaScript端处理。
- 若是须要屡次访问某个DOM节点,请使用局部变量存储它的引用。
- 当心处理HTML集合,由于它实时链接着底层文档。把集合的长度缓存到一个变量中,并在迭代中使用它,若是须要常常操做集合,建议把它拷贝到一个数组中。
- 若是可能的话,使用速度更快的API
- 要留意重绘和重排,批量修改样式时,“离线”操做DOM树,使用缓存,减小访问布局信息的次数
- 动画中使用绝对定位,使用拖放代理
- 使用事件委托来减小事件处理器的数量
代码的数量不是影响代码运行速度的必然因素,代码的组织结构和解决具体问题的思路才是。
大多数编程语言中,代码执行时间都消耗在循环中。
for循环
while循环
do-while循环
for-in循环
不断引起循环性能争论的源头是循环类型的选择。在JS提供的四种循环类型中,只有for-in明显比其余的慢。
优化循环的第一步就是减小对象成员和数组项的查询次数。好比把数组长度赋值给一个局部变量。
第二步是颠倒数组的顺序,一样能够提高循环性能。
第三步是减小迭代次数,达夫设备。
达夫设备其实是把一次迭代操做展开成屡次迭代操做。
思路是,每次循环中最多可调用8次process(),若是有余数,则表示第一次process()执行几回。
var iterations = Math.floor(items.length / 8), startAt = items.length % 8, i = 0; do { switch(startAt) { case 0: process(items[i++]); ... ... case 8: process(items[i++]); } startAt = 0; } while (--iterations);
达夫设备对于1000次以上的循环有很大的提高。
Array.forEach(function(value, index, array){ process(value); })
条件数量少时用if-else,多时用switch。
目标:最小化到达正确分以前所需判断的条件数量。
最简单的优化方法是确保最可能出现的条件放在首位。
还有一种是增长if-else的嵌套,尽量减小判断次数。
当有大量离散值须要测试时,或者条件语句数量很大时,JS能够经过数组和普通对象来构建查找表,速度要快得多。
switch(value) { case 0: return result0; case 1: return result1; } var results = [results0, results1] return results[index]
阶乘就是用递归实现的,可是递归的问题在于终止条件不明确或缺乏终止条件会致使函数长时间运行,并且可能会遇到浏览器的“调用栈大小限制”。
浏览器有栈限制。
有两种递归模式。直接递归和隐伏模式。
任何递归能实现的算法一样能够用迭代来实现。
把计算结果缓存,运行前先判断。
functiom memfactorial(n) {
if (!memfactorial.cache) { memfactorial.cache = { "0": 1, "1": 1 } } if (!memfactorial.cache.hasOwnProperty(n)) { memfactorial.cache[n] = n * memfactorial(n-1) } return memfactorial.cache[n]; }
运行的代码量数量越大,使用这些策略所带来的性能提高也就越明显。
UI线程把一个个JS或者UI渲染任务放到队列中逐个执行,最理想的状况就是队列为空,这样任务能够即刻执行。
浏览器对JS任务的执行时间有限制,从栈大小限制和运行时间两方面来限制。
“若是JS运行了整整几秒钟,那么极可能是你作错了什么….”
单个JS文件的操做总时间不能超过100毫秒。
若是100毫秒内不能解决JS任务,那么就把线程让出来执行UI渲染。
setTimeout()和setInterval()会告诉JS引擎等待一段时间,而后添加一个JS任务到UI队列。
定时器不可用于测量实际时间,有几毫秒的误差。
若是第四章的循环优化仍是没有将JS任务缩短到100毫秒之内,那么下一步的优化步骤就是定时器。
能够的话,将一个大任务分割成无数小任务。
不超过50毫秒的JS任务是很是好的用户体验,可是有时候一次性只执行一个任务,这样执行效率反而不高。
多个重复的定时器同时建立每每会出现性能问题。间隔在1s或者1s以上的重复定时器不会影响Web应用得响应速度。
Web Workers是HTML5最初的一部分,它的出现意味着JS任务能够单独分离出去而不占用浏览器的UI渲染。
Web Workers不能处理UI进程,这就意味着它不能接触不少浏览器的资源。
Web Workers的运行环境由以下部分组成:
- navigator对象,包括appName、appVersion、user Agent和platform。
- 一个location对象,(与window.location同,可是只读)
- 一个self对象,指向全局worker对象。
- 一个importScripts()方法,用来加载Worker所用到外部JS文件
- 全部的ECMAScript对象
- XMLHttpRequest构造器
- setTimeout()和setInterval()方法
-一个close()方法,它能马上中止Worker运行
var worker = new Worker("code.js")
Worker与网页代码经过事件接口进行通讯,网页代码经过postMessage()方法给Worker传递数据,它接收一个参数,即须要传给Worker的数据。此外,Worker还有一个用来接收信息的onmessage事件处理器。
var worker = new Worker('code.js') worker.onmessage = function(event) { process(event.data) } worker.postMessage('data') //code.js self.onmessage = function(event) { self.postMessage("Hello" + event.data) }
importScripts()阻塞
var worker = new Worker("jsonparser.js"); worker.onmessage = function(event) { var jsonData = event.data evaluateData(jsonData) } worker.postMessage(jsonText) self.onmessage = function(event) { var jsonText = event.data var jsonData = JSON.parse(jsonText) self.postMessage(jsonData) }
Ajax能够经过延迟和异步加载大资源。
Ajax从最基本的层面来讲,是一种与服务器通讯而无需重载页面的方法,数据能够从服务器获取或发送给服务器。
有五种经常使用技术用于向服务器请求数据:
- XMLHttpRequest(XHR)
- Dynamic script tag insertion 动态脚本注入
- iframes
- Comet
- Multipart XHR
XHR
var url = '/data.php'; var params = [ 'id=934875', 'limit=20' ]; var req = new XMLHttpRequest(); req.onreadystatechange = function() { if (req.readystate === 4) { } } req.open('get', url + '?' + params.join('&'), true) req.setRequestHeader('X-Requested-with', 'XMLHttpRequst'); req.send(null)
XHR的get请求是幂等行为,即一次请求和屡次请求并不会有反作用。
动态脚本注入
这个与XHR不一样的地方在于它不用在乎跨域问题。
var scriptElement = document.createElement('script'); scriptElement.src = ''; document.getElementsByTagName('head')[0].appendChild(scriptElement)
不能设置头信息,参数传递也只能用GET,不能设置请求的超时处理。
由于响应消息做为脚本标签的源码,它必须是可执行的JS代码。
Multipart XHR
MXHR容许客户端只用一个HTTP请求就能够从服务端向客户端传送多个资源。它经过在服务端将资源打包成一个由双方约定的字符串分割的长字符串并发送到客户端,而后用JS代码处理这个长字符串,并根据它的mime-type类型和传入的其余“头信息”解析出每一个资源。
惟一须要比较的标准就是速度。
须要解析结构,才能读取值。
JSON-P JSON填充,在动态脚本注入时必须放在回调函数里,否则就会被当作另一个JS文件执行(因此也要尤为注意脚本攻击。)
最快的Ajax请求就是没有请求。有两种主要的方法能够避免发送没必要要的请求:
- 在服务端,设置HTTP头信息以确保响应会被浏览器缓存
- 在客户端,把获取到的信息存储到本地,从而避免再次请求。
设置HTTP头信息
若是你但愿Ajax响应可以被浏览器缓存,那么你必须使用GET方式发出请求。但这还不够,你还必须在相应中发送正确的HTTP头信息。
Expires头信息会告诉浏览器应该缓存响应多久,它的值是一个日期,过时以后,对该URL的任何请求都再也不从缓存中获取,而是会从新访问服务器。
本地数据存储
能够把响应文本保存到一个对象中,以URL为键值作索引。
var localCache = {}; function xhrRequest(url, callback) { // 检查此URL的本地缓存 if (localCache[url]) { callback.success(localCache[url]); return; } // 此URL对应的缓存没有找到,则发送请求 var req = createXhrObject(); req.onerror = function() { callback.error(); } req.onreadystatechange = function() { ... localCache[url] = req.responseText; callback.success(req.responseText) } req.open("GET", url, true) req.send(null) }
浏览器之间有些差别,不过大多数类库都有封装。
JS和其余不少脚本语言同样,容许你在程序中提取一个包含代码的字符串,而后动态执行。
有四种标准方法能够实现:eval(),Function(),setTimeout(),setInterval()。
在JS代码中执行另一段JS代码就会形成双重求值,因此eval和Function不推荐使用,setTimeout和setInterval的第一个参数最好是回调函数。
使用直接量建立Object/Array
当一个函数在页面中不会被当即调用时,延迟加载时最好的选择,即若是须要针对不一样的浏览器写不一样的代码,在第一次就判断用的是哪一种,而后在内部重写函数。
var addHandler = document.body.addEventListener ? function1 : function2