来自 Web前端 2020-05-07 05:42 的文章
当前位置: 网上澳门金莎娱乐 > Web前端 > 正文

高性能JavaScript(您值得一看)

javascript在浏览器中运行的性能,可以认为是开发者所面临的最严重的可用性问题。这个问题因为javascript的阻塞性而变得复杂,事实上,多数浏览器使用单一进程来处理用户界面和js脚本执行,所以同一时刻只能做一件事。js执行过程耗时越久,浏览器等待响应的时间越长。

众所周知浏览器是使用单进程处理UI更新和JavaScript运行等多个任务的,而同一时间只能有一个任务被执行,如此说来,JavaScript运行了多长时间就意味着用户得等待浏览器响应需要花多久时间。

一.提高加载性能

从认知上来说,解析器解析一个界面的时候都是从上至下依次解析的,这就是说界面上出现多少个<script>标签(不管是内联还是外部文件),页面下载和解析必须停止等待脚本下载完成并运行完成(注意这里包括运行),这个过程当中,页面解析和用户交互是被完全阻塞的。

1.IE8,FF,3.5,Safari 4和Chrome都允许并行下载js文件,当script下载资源时不会阻塞其他script的下载。但是js下载仍然会阻塞其他资源的下载,如图片。尽管脚本下载不会互相影响,但页面仍然必须等待所有js代码下载并执行完才能继续。因此仍然存在脚本阻塞问题.推荐将所有js文件放在body标签底部以减少对整个页面的影响。

Javascript第一条定律:将脚本放在底部。

大家都知道,浏览器在遇到<body>标签之前,不会渲染页面中的任何部分,如果我们把<script>标签全部放在<body>标签之前,大家想想界面是不是一片空白,直至浏览器将所有js都下载并执行后才会去渲染页面,这也就是为什么要将所有<script>标签放在尽可能接近<body>标签底部也就是</body>前的原因。

2.减少页面外链脚本文件的数量将会提高页面性能:

Javascript第二条定律:将脚本成组打包。

减少引用外部脚本文件的数量,大家相信都会看到一些大型网站需要多次请求JavaScript文件时会将这个文件整合成一个文件,因为这样只需要一个<script>标签而减少性能损失。

书中介绍了运行一个<script>标签来加载两个js文件(但是自己没有试验成功,如果有知道怎么用的,可以告知我,谢谢!)

网上澳门金莎娱乐 1

 

在此特意说明一下

  • 为什么很多人特别喜欢用外部文件来写js而不用内联js,这是因为1)外部文件易于管理2)浏览器有缓存外部文件的功能,同样一个js文件如果被多个界面共享使用,第二个使用的可以直接从缓存中拿而不用从远程服务器上去下载,省去了好多时间及流量呀
  • 为什么许多人会用CDN(Content Delivery Network)来加载公用js比如说jquery.js,这是因为从别的网站比如说jQuery官网下载jQuery占有的是别人服务器的资源,减轻了本地服务器的负载,节省网站分布式架构的支出成本和运维成本。
  • 上文中讲了将多个js文件打包成一个js文件,可以提高性能,可以试试用Yahoo! Combo handler ,这篇文章介绍了一下http://www.ooso.net/archives/458  不过百度一下js打包工具一大堆,大家可以任意发挥哦

总归一句话就是加载js的时候会阻塞页面渲染,那怎么样让脚本不阻塞页面渲染呢,唯一的方法就是在window.onload发生后开始下载js

 

Javascript第三条定律:使用非阻塞方式下载js。

第一种方法:为script增加了一个defer属性,defer属性指定的script指明脚本不打算修改DOM,所以代码可以稍后执行。但是defer并不是所有浏览器都支持的。当defer指定的脚本文件被下载时,它不会阻塞浏览器的其它处理过程,所以事实上defer指定的脚本文件是可以与页面中的其它资源并行下载的。任何被指定为defer的脚本文件一定会在DOM加载完成之后才会被执行,也就是在onload事件之前被执行。

第二种方法:动态创建脚本也就是通过js创建script标签指定它的src来加载脚本文件,这种方法使得脚本下载和运行不会阻塞其它页面处理程序,所以你甚至可以把这种动态创建脚本的文件放在head标签当中都没有关系。

动态创建脚本的js如下图所示:

网上澳门金莎娱乐 2

 

上述动态创建的脚本只要一指定它的src并append到document中,它就会自动执行里面的代码,那怎么样让它加载完成之后执行回调函数或者说如果我要加载多个js文件合格保证它们的顺序呢

看代码吧! 哈哈(因为动态加载的js,浏览器不保证加载的顺序)

网上澳门金莎娱乐 3

另外一种以非阻塞方式获得脚本文件的方法就是使用XHR(XmlHttpRequest脚本注入)

网上澳门金莎娱乐 4

这种方法的主要优点是你可以下载不立即执行的脚本文件,但是缺点就是你要动态加载的js文件必须与当前运行的页面在同一个域中

下面介绍几个动态加载的脚本库

LazyLoad--可以下载多个脚本文件并保证在所有浏览器上保证顺序执行。(虽然可以动态加载多个文件,但还是建议尽可能的减少文件数量,因为每个下载仍然是一个单独的HTTP请求)

网上澳门金莎娱乐 5

Labs.js  它的script方法用于加载js文件,wait方法用于等待文件下载并运行之后执行的回调函数,通过wait方法允许你哪些文件应该等待其它文件,具体用法可以参照labs使用方法

网上澳门金莎娱乐 6

http请求会带来额外的开销,因此下载单个300k的文件将比下载10个30k的文件效率更高。

杂谈

对于任何一种编程语言来说,数据存储的位置关系到访问速度!

在JavaScript中的直接量包括字符串string、数字number、布尔值boolean、对象object、数组array、函数function、正则表达式regular expression、空值null、未定义数组undefined。而数组项则需要通过数组的数字索引来访问,对象通过字符串进行索引来访问其成员(这里顺便提一句因为数组项是通过数字进行索引、对象成员是通过字符串进行索引,所以这也就是为什么访问对象成员比访问数组项更慢的原因)。

总结一句话就是说直接量和局部变量的访问速度要远已于数组项和对象成员,所以我们应该尽量使用直接量和局部变量,尽量限制使用数组项和对象成员。

了解过JavaScript原型链和闭包的都知道在JavaScript中每个函数事实上就是对象,每个函数内部都会有一个Scope属性,这个属性包含被创建的作用域中对象的集合,所以事实上内部Scope属性就是函数的作用域。在函数运行时,会建立一个运行期上下文(运行期上下文事实上就是函数运行时的环境,对于函数的每次运行而言,运行期上下文都是独一的,函数执行完毕运行期上下文将被销毁,也就是说多次运行同一个函数就会创建多个运行期上下文)。

当函数运行期上下文确定后,它的作用域将被初始化(作用域初始化包括函数参数、函数内部变量、this),作用域初始化之后将被作为激活对象推入作用域链的前端。(事实上这就是原型链的原理,js就是通过原型链来实现继承,通过闭包来实现成员的公有和私有),这也是为什么访问全局变量是最慢的,因为全局变量它是处于运行期上下文作用域链的最后一个位置。

3.动态脚本加载技术:

尽量使用局部变量来保存全局变量

如果需要多次访问全局变量,尽量使用局部变量来保存它(我觉得这条规则运用在哪种编程语言中都适用)

网上澳门金莎娱乐 7

无论何时启动下载,文件的下载和执行都不会阻塞页面其他进程。

尽量少去改变作用域链

  • 使用with
  • try catch

我了解到的JavaScript中改变作用域链的方式只有两种1)使用with表达式 2)通过捕获异常try catch来实现

但是with是大家都深恶痛绝的影响性能的表达式,因为我们完全可以通过使用一个局部变量的方式来取代它(因为with的原理是它的改变作用域链的同时需要保存很多信息以保证完成当前操作后恢复之前的作用域链,这些就明显的影响到了性能)

try catch中的catch子句同样可以改变作用域链。当try块发生错误时,程序自动转入catch块并将异常对象推入作用域链前端的一个可变对象中,也就是说在catch块中,函数所有的局部变量已经被放在第二个作用域链对象中,但是catch子句执行完成之后,作用域链就会返回到原来的状态。应该最小化catch子句来保证代码性能,如果知道错误的概念很高,我们应该尽量修正错误而不是使用try catch

function laodScript(url,callback){ var script = document.createElement('script'); script.type = 'text/javascript'; if(script.readyState){ // ie script.onreadystatechange = 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('./a.js',function(){ loadScript('./b.js',function(){ loadScript('./c.js',function(){ console.log('加载完成') }) })})

尽量少去使用闭包

众所周知,闭包最厉害的地方就是允许函数使用局部范围之外的数据。

大家知道当函数运行完成之后,其运行期上下文就将被销毁,当涉及闭包的时候,它就不能被销毁,因为可能它还需要被函数引用,这样无形中就需要更多的内存开销。

  1. 无阻塞加载类库——LABjs,使用方法如下:

    script src="lab.js"/script// 链式调用时文件逐个下载,.wait()用来指定文件下载并执行完毕后所调用的函数$LAB.script('./a.js') .script('./b.js') .wait(function(){ App.init();})// 为了保证执行顺序,可以这么做,此时a必定在b前执行$LAB.script('./a.js').wait() .script('./b.js') .wait(function(){ App.init();})

访问速度与成员嵌套深度有关

成员嵌套越深访问速度越慢

location.href快于window.location.href

window.location.href快于window.location.href.toString()

二. 数据存取与JS性能

缓存变量的值

(事实上就是如果需要访问全局变量或者多次访问具有深度的对象成员,使用局部变量将其保存下来以方便后续使用)

在JavaScript高级程序设计第一章当中就把JavaScript分成三大部分

网上澳门金莎娱乐 8

所以事实上DOM和BOM是两在独立的部分,它们之间的通信是通过相互之间的功能接口来实现的,这样说来两个独立的部分以功能接口必定会带来性能损耗。这也就是为什么大家一致都说尽量少去访问和修改DOM元素(注意我这里说的是访问和修改,为什么包括访问,请继续往下看  哈哈)。

下面用一张图来说明它们各自的作用。

网上澳门金莎娱乐 9

1.在js中,数据存储的位置会对代码整体性能产生重大影响。数据存储共有4种方式:字面量,变量,数组项,对象成员。他们有着各自的性能特点。

1、在修改DOM元素的时候,我们应该尽量使用innerHTML而不是CreateElement再AppendChild(因为经过测试,在所有的浏览器当中使用innerHTML更加快一些)

2.访问字面量和局部变量的速度最快,相反,访问数组和对象相对较慢

2、修改DOM元素内容时另外一个方法是使用element.cloneNode来代替document.createElement()

3.由于局部变量存在于作用域链的起始位置,因此访问局部变量的比访问跨域作用变量更快

3、在循环或者遍历元素时,尽量使用局部变量保存HTML Collections

那具体什么叫HTML Collections,它包括哪些元素呢?

网上澳门金莎娱乐 10

总结一句话就是说HTML Collections事实上是虚拟存在的,意味着当底层文档更新时,它们将自动更新。当每次迭代访问HTML Collections的length属性时,它会导致集合器更新,所以将length属性缓存到一个变量中,然后在循环判断条件中使用这个变量。

网上澳门金莎娱乐 11

4.嵌套的对象成员会明显影响性能,应尽量避免

4、childNodes是一个集合,这也是为什么很多提高JavaScript性能的建议当中有这样一条了“使用firstChild和nextSibling代替childNodes遍历dom元素”  用图来说话吧(不信你可以测试一下,明显testNextSibling的速度比testChildNodes的快特别是当数据量大的时候一眼就能看出来)

网上澳门金莎娱乐 12

 

5.属性和方法在原型链位置越深,访问他的速度越慢

5、如果浏览器使用具有原生的QuerySelectorAll()方法和querySelector()方法尽量使用它们(因为自己实现它们的功能需要编写特别多的代码,还有就是任何编程语言都有一个共通的特性就是原生方法永远是性能最佳的)

6.通常我们可以把需要多次使用的对象成员,数组元素,跨域变量保存在局部变量中来改善js性能

6、最小化重绘和重排

在说明这个的时候顺便理清一下什么叫重绘,什么叫重排

大家都知道当一个页面展示在我们面前的时候,事实上浏览器下载完成所有的html标记,js,css,图片之后,它解析文件并创建两个内部结构,一个是DOM树,一个是渲染树。浏览器根据DOM树来进行排列,根据渲染树来进行绘制。

什么情况下会发什么重排呢

  • 添加或删除可见的DOM元素
  • 元素位置改变
  • 元素尺寸改变(因为边距、填充、边框宽高等属性)
  • 内容改变
  • 最初的页面渲染
  • 浏览器窗口改变尺寸

理解了重排,重绘就更加简单了,从字面意思上来说就是重新绘制,那什么情况下会发生重绘,比如说改变样式背景等

网上澳门金莎娱乐 13

 

网上澳门金莎娱乐,类似的我们知道了这个也应该在脚本中注意尽量减少repaint和reflow,什么情况会导致这两种情况呢

reflow:当DOM元素出现隐藏/显示、尺寸变化、位置变化的时候都会让浏览器重新渲染页面,之前渲染工作白费了

repaint:当元素的背景颜色、边框颜色不引起reflow的变化是会让浏览器重新渲染该元素。貌似还可以接受,但如果我们在开始就定义好了,不让浏览器重复工作就更好了。 

 

 

上图中列举的这些属性的访问将会导致重新刷新渲染树从而导致重排,最好减少对这些属性(布局信息)的查询次数,查询时将它赋给局部变量,并用局部变量参与计算。

再来看一个小例子

网上澳门金莎娱乐 14

这明显是糟糕的代码,改变了三次风格属性,导致浏览器重排了三次。一个提高效率的方法就是只重排一次。看下面代码就明白了

网上澳门金莎娱乐 15

或者使用类名的方法

网上澳门金莎娱乐 16

当你需要对DOM树中的多个元素进行修改的话,最好依据下列步骤来减少重排和重绘

  • 从文档流中摘下此元素
  • 对其应用多重改变
  • 将元素带回文档中

有三种方式可以将元素从文档流中摘除

  • 隐藏元素,对其进行修改后再显示
  • 使用文档片断在已存DOM之外创建一个子树,然后将它拷贝到文档中(推荐使用此方法,因为它是涉及最小数量的DOM操作和重排)
  • 将原始元素拷贝到一个脱离文档的节点中,修改副本,然后副本,然后覆盖原始元素

将第二次方案的代码贴一下,希望大家能使用这种方法来提高性能。

       for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            document.body.appendChild(el);
        }
        //可以替换为:
        var frag = document.createDocumentFragment();
        for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            frag.appendChild(el);
        }
        document.body.appendChild(frag);

如果是动画元素的话,最好使用绝对定位以让它不在文档流中,这样的话改变它的位置不会引起页面其它元素重排

(我要说的就这些啦,希望大家共同探讨!)

这是我看高性能JavaScript的一点总结,希望对前端感兴趣的可以与我共同研讨!

有兴趣的可以去了解一下lab.js和lazyload  这俩挺好用的。哈哈!也可以跟我共同研讨!

如果觉得这篇文章还算用心,请劳驾点右下角的推荐。小女子在此谢过了。^_^


三. DOM编程

1.访问DOM会影响浏览器性能,修改DOM则更耗费性能,因为他会导致浏览器重新计算页面的几何变化。通常的做法是减少访问DOM的次数,把运算尽量留在JS这一端。

注:如过在一个对性能要求比较高的操作中更新一段HTML,推荐使用innerHTML,因为它在绝大多数浏览器中运行的都很快。但对于大多数日常操作而言,并没有太大区别,所以你更应该根据可读性,稳定性,团队习惯,代码风格来综合决定使用innerHTML还是createElement()

  1. HTML集合优化

HTML集合包含了DOM节点引用的类数组对象,一直与文档保持连接,每次你需要最新的信息时,都会重复执行查询操作,哪怕只是获取集合里元素的个数。

① 优化一——集合转数组collToArr

function collToArr(coll){ for(var i=0, a=[], len=coll.length; ilen; i++){ a.push(coll[i]); } return a}

② 缓存集合length

③ 访问集合元素时使用局部变量

  1. 遍历DOM

① 使用只返回元素节点的API遍历DOM,因为这些API的执行效率比自己实现的效率更高:

属性名被替代属性childrenchildNodeschildElementCountchildNodes.lengthfirstElementChildfirstChildlastElementChildlastChildnextElementSiblingnextSiblingpreviousElementSiblingpreviousSibling

② 选择器API——querySelectorAll()

querySelectorAll()方法使用css选择器作为参数并返回一个NodeList——包含着匹配节点的类数组对象,该方法不会返回HTML集合,因此返回的节点不会对应实时文档结构,着也避免了HTML集合引起的性能问题。

let arr = document.querySelectorAll('div.warning, div.notice  p')

4.重绘和重排

浏览器在下载完页面的所有组件——html,js,css,图片等之后,会解析并生成两个内部数据结构——DOM树,渲染树.一旦DOM树和渲染树构建完成,浏览器就开始绘制页面元素.

① 重排发生的条件:

添加或删除可见的DOM 元素位置变化 元素尺寸改变 内容改变 页面渲染器初始化 浏览器窗口尺寸变化 出现滚动条时会触发整个页面的重排

重排必定重绘

5.渲染树变化的排列和刷新

大多数浏览器通过队列化修改并批量执行来优化重排过程,然而获取布局信息的操作会导致队列强制刷新。

offsetTop,offsetWidth...scrollTop,scrollHeight...clientTop,clientHeight...getComputedStyle()

一些优化建议:将设置样式的操作和获取样式的操作分开:

// 设置样式body.style.color = 'red';body.style.fontSize = '24px'// 读取样式let color = body.style.colorlet fontSize = body.style.fontSize

另外,获取计算属性的兼容写法:

function getComputedStyle(el){ var computed = (document.body.currentStyle ? el.currentStyle : document.defaultView.getComputedStyle(el,''); return computed}

6.最小化重绘和重排

①.批量改变样式

/* 使用cssText */el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 20px';

②.批量修改dom的优化方案——使元素脱离文档流-对其应用多重改变-把元素带回文档

function appendDataToEl(option){ var targetEl = option.target || document.body, createEl, data = option.data || []; // 让容器脱离文档流,减少重绘重排 var targetEl_display = targetEl.style.display; targetEl.style.display = 'none'; // *****创建文档片段来优化Dom操作**** var fragment = document.createDocumentFragment(); // 给元素填充数据 for(var i=0, max = data.length; i max; i++){ createEl = document.createElement(option.createEl); for(var item in data[i]){ if(item.toString() === 'text'){ createEl.appendChild(document.createTextNode(data[i][item])); continue; } if(item.toString() === 'html'){ createEl.innerHTML = item,data[i][item]; continue; } createEl.setAttribute(item,data[i][item]); } // ****将填充好的node插入文档片段**** fragment.appendChild(createEl); } // ****将文档片段统一插入目标容器**** targetEl.appendChild(fragment); // 显示容器,完成数据填充 targetEl.style.display = targetEl_display;}// 使用var wrap = document.querySelectorAll('.wrap')[0];var data = [ {name: 'xujaing',text: '选景', title: 'xuanfij'}, {name: 'xujaing',text: '选景', title: 'xuanfij'}, {name: 'xujaing',text: '选景', title: 'xuanfij'}];appendDataToEl({ target: wrap, createEl: 'div', data: data});

上面的优化方法使用了文档片段:当我们把文档片段插入到节点中时,实际上被添加的只是该片段的子节点,而不是片段本身。可以使得dom操作更有效率。

③.缓存布局信息

//缓存布局信息let current = el.offsetLeft;current++;el.style.left = current + 'px';if(current  300){ stop();}

④.慎用:hover

如果有大量元素使用:hover,那么会降低相应速度,CPU升高

⑤.使用事件委托来减少事件处理器的数量,减少内存和处理时间

function delegation(e,selector,callback){ e = e || window.event; var target = e.target || e.srcElement; if(target.nodeName !== selector || target.className !== selector || target.id !== selector){ return; } if(typeof e.preventDefault === 'function'){ e.preventDefault(); e.stopPropagation(); }else{ e.returnValue = false; e.cancelBubble = true; } callback()}

四.算法和流程控制

1.循环中减少属性查找并反转(可以提升50%-60%的性能)

// for 循环for(var i=item.length; i--){ process(item[i]);}// while循环var j = item.length;while(j--){ process(item[i]);}

2.使用Duff装置来优化循环

3.基于函数的迭代

items.forEach(function(value,index,array){ process(value);})

本文由网上澳门金莎娱乐发布于Web前端,转载请注明出处:高性能JavaScript(您值得一看)

关键词: