我与高性能js的约会

848 阅读8分钟

2019第一本书产出,纪念一波:


第一章:js加载

  1. js的阻塞特性:

当浏览器在执行js代码的时候,不能同时做其他事情。(界面ui线程和js线程用的是同一进程,所以js执行越久,网页的响应时间越长。)

  1. 脚本的位置

如果把脚本<script>放在<head>中,页面会等js文件全部下载并执行完成后才开始渲染,在这些文件下载和执行的过程中:会导致访问网站的时候有明显的延迟,表现为:页面空白。

这点在这个项目就用到了!


特别是ie,说不定人家给你来个报错呢~(你能拿我怎么样。略略略.ipg)

性能提升:推荐将所有的<script>标签尽可能的放到<body>标签的底部,优先渲染页面,减少页面空白时间。

  1. 组件脚本。

每个<script>标签初始下载的时候都会阻塞页面的渲染。性能提升做法:减少内嵌脚本:减少内嵌的<script>标签,将代码写在一个标签中。

合并外链的js文件:http请求会带来额外的性能开销,栗子:下载一个100KB的js文件比下载4个25kb的js文件更快。具体操作方法自行搜索。

  1. 无阻塞脚本的方法

script标签的aync属性:

async 属性规定一旦脚本可用,则会异步执行。async 属性仅适用于外部脚本(只有在使用 src 属性时)。如果 async="async":脚本相对于页面的其余部分异步地执行(当页面继续进行解析时,脚本将被执行)

script标签的defer属性:

js文件在页面解析到script标签的时候开始下载,但并不会执行,dom加载完成执行。这两个属性的区别在于执行时机。

动态脚本元素。

js操作dom创建<script>标签,自定义生成标签的type、src属性。文件会在该元素被添加到页面的时候开始下载。ps:如果文件顺序很重要的话,最好按照顺序合成一个文件。然后再添加到页面中。这样:无论何时启动下载。文件的下载和执行过程不会阻塞页面的其他进程。

3. XHR ajax脚本注入、

用get请求一个文件,请求好了。然后创建动态脚本,最后添加进去。缺陷:文件要再请求页面的同一个域。

第二章:数据存取

1.字面量和局部变量速度快于数组和对象。

2.三目运算运算更快原因:(cpu原理
  • CPU是通过流水线处理来获得高性能的。所谓流水线,简单来说就是当CPU在处理当前指令的时候,后面已经有N条指令在后面排队等待你去执行了。这样,当你要执行下一条指令的时候,你不用再去找那条指令,它已经乖乖跟在前一条指令的屁股后面等你去执行了。
2.2 if…else…处理模式 那问题就在于,后面的指令需要确认一个排队顺序。如果程序员也是简单的编写流水线式的代码,对于CPU来说指令排序很容易。但是if…else…就不一样了。 if…else…简单来说就是:当我满足条件,那我就执行A,如果我不满足条件,我就执行B。但是对于给指令排队的CPU来说,它还没执行到判断条件这一步,不知道你满不满足呀!这样它就不好给指令排队了。 假设它按照你满足条件,把A指令排在你后面。那么当执行到最后发现你不满足条件,那么就得把之前排好的队列清空,重新给你把B指令排到后面给你执行。这种预测错误的惩罚将会导致CPU处理时间更长。 假设预测准确的话,每次调用函数大概13个时钟周期,而只要预测错误,可能就需要大约44个时钟周期了。


2.3 三元运算处理模式 对于三元运算符,它的处理方法就不同了。 x=y>0?A:B; 当CPU要给这条指令排队时,它会将两种结果都进行排队,也就是表达式A和表达式B都会被CPU进行处理,算出结果。 计算对CPU来说反而是它更喜欢的事情,你只要能排队排好了,让它能流水线模式来执行,对于它来说总体速度反而更快。 CPU会将两种结果A和B都给计算出来(这跟if…else…不同,其只会计算一种结果),最后再判断y>0?。如果y>0,则选择A,抛弃B; 否则就选择B,抛弃A。

3.执行函数的作用域链:
在执行函数过程中,每遇到一个变量,都会经历一次标志符解析过程来决定从哪里获取或存储数据。该过程搜索执行环境的作用域链,从作用域链头部开始,从头到尾,如果没找到就会未定义(undefined).
Function add(num1,num2){
return num1 + num2;
}
函数访问sun1,sum2都会产生搜索过程。这个搜索过程影响性能。(名字相同的变量存在于作用域链不同部分,搜索最先找到哪个,就用哪个。这个就会屏蔽其他的。)

3.解析标志符是会消耗时间的。局部的读写速度总是快于全局的。因为全局在执行环境是最慢的;在执行环境的作用域链的最末端。

4.减少跨作用域的值,把它保存到局部变量里面。
比如:document.getElementById(“go-btn”).onclick = function(){
start();
}
====> var doc = document;
====> var bd = document.body;
document 是全局对象,每次使用都要遍历整个作用域链,直到在最后的全局变量对象中找到。

还有在仓库中取的值,var ss = a.b.c.d.i.d.state;
vm.$post(url, params, prompt, function(source) {
  var rawData = source.orgInfoDetailList.data.basicInfo.rawDatas[0];
  if ( rawData
    && rawData.companyName
    && (rawData.orgNo || rawData.creditCode)
    && rawData.companyIndustry
    && rawData.industryCode
  ) {
  // doSomethingInteresting
    })
    vm.searchShow = false;
  } else {
    prompt.other('客户信息不完整');
  }
})
5.作用域链一般是不可变的,但是with可以做到。还有try catch;(反正我不用with)

6.var book = {
age:’78’,
sex:’女'
}
Console.log(book.hasOwnProperty(‘age’)) true

Console.log(book.hasOwnProperty(’toString’)) false

hasOwnProperty是用来判定是否包涵特定的实例成员。

要确定是否包涵特定的属性,可以用in操作符。in会搜索实例也会搜索原型。
console.log(’title’ in book); true
console.log(’toString’ in book); true

7.嵌套成员越深,读取速度就会越慢。location.href < windows.location.href;如果这些属性不是对象的实例属性,那么成员解析还需要搜索原型链,花更多的时间。

8. . 和 [] 两者操作并没有明显的区别。只有在safari中,点符号始终会更快。

第三章:dom编程


1.访问dom元素是有代价的——过桥费。修改元素更贵,导致浏览器重新计算页面的几何变化。

循环访问修改DOM元素绝逼是“大佬”!!!!!好吧,我是讽刺。


2.获取某个节点,代码更快更简洁的方式:
Var elements = document.querySelectorAll(‘#menu a’);
代替 document.getElementByTagName……

3.
DOM树:表示页面结构。
渲染树:表示DOM节点如何显示。(css)
一旦长和宽变化,或增加文字,元素的几何属性和位置受到影响。
就会重新构造渲染树———>>>
重排
接着浏览器重新绘制受影响的部分到屏幕中———>>>
重绘
并不是所有的DOM变化都会影响几何。改变颜色的话只会来一次重绘,没有重排。

4.重排何时发生:
1.添加或删除可见的DOM元素。
2.元素位置改变。
3.元素尺寸改变。
4.内容改变,文本改变或者图片被另一个图片替代。
5.页面渲染器初始化。
6.浏览器窗口尺寸改变。

5.offsetTop,scrollTop,clientTop……等等,需要返回最新的布局信息,因此浏览器不得不执行渲染列队中的“待处理变化”并触发重排以返回正确的值。就算以上属性没有任何改变。

6.
cssText 可以对某个节点进行多个样式修改。
ele.style.cssText = ‘border-left:1px;border-right:2px;border-bottom:9px’;
如果不想改变以前的,加一些渲染:ele.style.cssText+=‘border-top:20px’;

7.批量修改dom:通过一下步骤来减少重绘和重排的次数。
1.使元素脱离文档流:absolute,display…(隐藏元素,应用修改,重新显示)
2.对其应用多重改变:对要修改的文档拷贝拿出去修改,再放进来。
3.把元素带回文档中:在文档之外创建并更新一个文档片段,然后把它附加到原始列表中。

8.
一般来说,重排只影响渲染树中的一小部分。
减少使用:hover

9.事件委托:
浏览器需要跟踪每个事件处理器,这也会占用更多的内存。还有并不是100%的按钮或链接会被用户点击。因此,使用事件代理,只需要给外层元素绑定一个处理器,就可以处理在其子元素上触发的所有事件。
捕获——>到达目标——>冒泡

还可以用来判断事件对象来源作相应处理。

第四章:算法和流程控制

1.
for(var prop in object){
//遍历所有属性名
}
不要使用for in 遍历数组成员

2.
for(var i=0;i<items.length;i++) 这样不好,因为每次循环都会再算一次item.length长度。

for(var i=0;len=items.length;i<len;i++) 这样有时能提升25%,甚至50%在ie里面。

使用倒序循环可以优化50%~~60%.因为少了比较步骤和判断两步。
i- -操作本身会影响CPSR(当前程序状态寄存器),CPSR常见的标志有N(结果为负), Z(结果为0),C(有进位),O(有溢出)。i > 0,可以直接通过Z标志判断出来。i++操作也会影响CPSR(当前程序状态寄存器),但只影响O(有溢出)标志,这对于i < n的判断没有任何帮助。所以还需要一条额外的比较指令,也就是说每个循环要多执行一条指令。

3.for 1139 forEach 1086. 其实差不多。


4.大多数情况下,switch优于if-else;但是如果条件不多还是建议if-else,易读。

5.优化if-else:把可能性最大的放第一位。
If(value < 5){
//...
} else if (value > 5 && value < 10) {
//...
} else {
//…
}

6.
If (value === 0) {
//...
} else if (value === 1) {
//...
}else if (value === 2) {
//...
}else if (value === 3) {
//...
}else if (value === 4) {
//...
}else if (value === 5) {
//...
}else if (value === 6) {
//...
}else if (value === 7) {
//...
}else if (value === 8) {
//...
}else if (value === 9) {
//...
}else {
//...
}
这种情况最多可能要10次;所以换一种不稳定,但是提升性能的写法。

if(value < 6){
if (value < 3) {
If (value === 0) {
//...
} else if (value === 1) {
//...
}else {
//...
}
} else {
If (value === 4) {
//...
} else if (value === 5) {
//...
}else {
//...
}
}
} else {
不想写了。。。。。。。。。。你懂的,我懂的。
}

查找表—是个不错的选择。

当条件在1-4:三者都可以。但还是不如查找表
            5-8:switch 优于 if,但是二者都不如查找表
            9以上switch 和 if 一样烂,都不如查找表
var results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10];
return results[value];


第五章:字符串和正则表达式


1.字符串链接:
array.Join() string.concat(),都要比+ +=效率高。

2.str = str + “one” + “two” 效率比 str += “one” + “two”;因为后者需要创建临时变量去存“one” + “two”的值。

3.在正则表达式中,如果用^这个符号效率会更高,因为只要匹配到第一个失败的地方就会停止后面的继续匹配。(大知识)

4.去除字符串首尾空白:
s = ' oo ';
console.log(s.trim()); 原生js有自带trim.
可以自己实现trim()方法,
If (!String.prototype.trim) {
String.prototype.trim = function(){
return this.replace(/^s+/,””).replace(/\s+$/,”")
}
}

第六章: 快速响应的用户界面

1.浏览器限制:是必要的防止恶意代码永不停止的密集操作锁住用户的浏览器或计算机。分为调用栈大小限制和长时间运行脚本限制。100毫秒的响应时间让以用户体验良好。

2.定时器代码只有在创建它的函数执行完成之后,才有可能被执行。

3.如果延时时间到了,主程序还没有运行结束,那么延时代码就会在onclick代码执行期间就加入队列,那么onclick一旦执行完,就会立马执行延时代码,给人造成没有延时的错觉。一般至少25ms。

4.定时器可用来安排代码延迟执行,它使得你可以长时间运行脚本分解成一系列的小任务。


第七章: ajax


1.url长度超过2048个字符,用post.不然如果是get,请求的url被截断。

2.动态脚本注入:
1.优势:克服了XHR的最大限制:跨域请求数据。这是一个hack。
2.缺点:不能设置请求的头信息,参数传递也只能是get。

3.MXHR对于上传较多图片时性能提升4-10倍。

4.使用XHR发送数据到服务器时,get要比post更快,这是因为对于少量数据而言,一个get请求往服务器只发送一个数据包。一个post请求要发送两个数据包。一个装载头信息,另一个装载post正文。post更适合发送大量数据到服务器,因为它不关心额外数据包的量,另一个原因是ie对url有限制,它不可能用过长的get请求。


第八章:编程实践


1.当在一段js代码中,执行执行另一段js代码,都会导致双重求值的性能消耗。
比如:eval setTimeout setinterval
给 setTimeout setinterval传递函数而不是字符串作为参数。

2.使用object/array直接量:
var myobject = new Object();
myobject.name = ’selena’;
myobject.age = ‘12’;
myobject.flag = true;

var myobject = {
name:’selena’,
age:’12’,
flag:true
}

第二段比第一段更快。特别是数量越多,越明显。

3.原生方法总是比自己写的方法更快的,因为是用c++写的存在浏览器中的。这意味着这些方法会被编译成机器码,成为浏览器的一部分,不会像自己写的那样受限制。

我一直记得以前ACM大赛,朋友说要自己造轮子,要比原生更快。


这是以上比较简洁的总结啦,具体demo还是夭折在我的小mac本里啦~

但是个人还是觉得,优化性能也要考虑代码的可读性,如果过于夸张的追求技术show,而忽略了的项目的可维护性,还是不太好的~