第一章
- 浏览器在解析到
<body>
标签之前,不会渲染页面的任何部分。把脚本放在页面顶部会导致明显的延迟,通常表现为显示空白页面。 - 由于JS脚本会阻塞页面其他资源的下载(比如图片),因此推荐将所有的
<script>
标签尽可能的放到<body>
标签的底部。 总结:将脚本放在底部。 - 由于每个
<script>
标签初始下载时都会阻塞页面渲染,所以减少页面包含的<script>
标签数量有助于改善这一情况。可以通过打包工具来合并为一个JS文件。 - 下载单个100kb的文件比下载4个25kb的文件更快。减少页面中外链脚本文件的数量将会改善性能。
- 无阻塞脚本的秘诀在于,在页面加载完成后才加载JavaScript代码。在window对象的onload事件触发后再下载脚本。
- 在
<script>
标签里使用defer 或者 async 属性。对应的JavaScript文件将在页面解析到<script>
标签时开始下载,但并不会执行,直到DOM加载完成。 - 带有defer属性的js文件下载时,不会阻塞浏览器的其他进程。可以并行下载。
第二章
- 如果一个函数引用了三次document,而document是个全局对象。搜索该变量的过程必须遍历整个作用域链,直到最后在全局变量对象中找到。
- 执行函数时会创建一个成为执行环境(执行上下文)的内部对象。一个执行环境定义了一个函数执行时的环境。函数每次执行时对应的执行环境都是独一无二的,所以多次调用同一个函数就会创建多个执行环境。当函数执行完毕,执行环境就会被销毁。
- 在执行环境的作用域链中,一个标识符所在的位置越深,他的读写速度也就越慢。因此,局部变量总是最快的。全局变量通常是最慢的,因为全局变量总是存在于作用域链中的最末端。
- 如果多次访问一个全局对象,可以通过全局变量的引用储存在一个局部变量中。如:
var doc = document
,这样访问全局变量的次数从3次变为1次。而且doc是个局部变量,通过它访问document会更快。 - 使用闭包也会对性能有影响。闭包是JS最强大的功能之一,它允许函数访问局部作用域之外的数据。
- 减轻闭包对执行速度的影响:将常用的跨作用于变量储存在局部变量中,然后直接访问局部变量。
通过
hasOwnProperty()
可以判断对象是否包含特定的实例成员。let book = { name: 'js' } book.hasOwnProperty('name') // true book.hasOwnProperty('toString') // false
- 要确定对象是否包含特定的属性,可以使用in操作符
('name' in book) // true ('toString' in book) // true 因为他即会搜索实例,也会搜索原型
- 搜索实例成员比从字面量或局部变量中读取数据代价更高,再加上便利原型链带来的开销,让性能问题更为严重。
- 对象成员嵌套得越深,读取速度就会越慢。如 读取loaction.href 比 window.loaction.href 快。
- 通常来说,在函数中如果要多次读取同一个对象属性,最佳做法是将属性保存到局部变量中。局部变量能够用来代替属性以避免多次查找带来的性能开销。
- 通过赋值给局部变量的方法不适用于对象的成员方法。因为许多对象方法使用this来判断执行环境,把一个对象方法保存在局部变量会导致this绑定到window,而this值的改变会使得JavaScript引擎无法正确解析它的对象成员,进而导致程序出错。
第三章
- 频繁的访问DOM也会影响性能。访问DOM的次数越多,代码的运行速度越慢。
- 当页面布局和几何属性发生改变时就需要“重排”。
- 每次重排都会产生计算消耗,大多数浏览器通过队列化修改并批量执行来优化重排过程。
- 浏览器需要重排的次数越小,应用程序的响应速度就越快。
- 如果通过dom改变元素的几何属性(宽高),其他元素的几何属性也会受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。这个过程称为重排(reflow)。
- 完成重排后,浏览器会重新绘制受影响的部分到屏幕中,这个过程称为重绘(repaint)。
- 并不是所有的dom变化都会影响几何属性。例如,改变一个元素的背景色并不会影响它的宽和高。在这种情况下,只会发生一次重绘,因为元素的布局并没有改变。
- 重排发生在页面布局和几何属性改变时就需要重排。
- 添加或删除可见的DOM元素
- 元素位置改变
- 元素尺寸改变(包括:外边距、内边距、边框厚度、宽度、高度等属性改变。)
- 内容改变,例如:文本改变或图片被另一个不同尺寸的图片替代
- 页面渲染初始化
- 浏览器窗口尺寸改变
- 当你需要对DOM元素进行一系列操作时,可以通过以下步骤来减少重绘和重排的次数:
- 使元素脱离文档流
- 对其应用多重改变
- 把元素带回文档中
这个过程里会触发两次重排,第一步和第二步。如果你忽略这两步,那么在第二步所产生的任何修改都会触发一次重排。
- 有三种方法可以使dom脱离文档:
- 隐藏元素,应用修改,重新显示
- 使用文档片段在当前DOM之外构建一个子树,再把它拷贝回文档
- 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素
- 在IE中大量:hover会降低性能。
- 使用事件委托来减少事件处理器的数量。
第四章
- 在大多数变成语言中,代码执行时间大部分消耗在循环中。循环处理是最常见的编程模式之一,也是提升性能必须关注的要点之一。
- 循环类型包括 for循环、while循环、do-while循环和for-in循环。这些循环当中只有for-in循环比其他几种明显要慢。
3.在循环中,通过定义一个局部变量的
length
属性,这样循环运行前只进行一次属性查找。可以直接读取局部变量。for(let i=0; i<item.length; i++) 写成 for(let i=0, len=item.length; i<len; i++)
- 通过使用倒序循环来提升性能,如:
var i=items.length i--
Duff's Device
let i = item.length % 8 while(i) { process(item[i--]) } i = Math.floor(item.length % 8) whilte(i) { process(item[i--]) process(item[i--]) process(item[i--]) process(item[i--]) process(item[i--]) process(item[i--]) process(item[i--]) process(item[i--]) }
- 事实证明,大多数情况下switch比if-else运行要快,但只有当条件数量很大时才快得明显。
- 在条件数量较少时使用if-else,而在条件数量较大时使用switch。
- 优化if-else的方法是确保最有可能出现的条件放在首位。if-else中的条件语句应该总是按照从最大概率到最小概率的顺序排列,以确保运行速度最快。
- 优化if-else的另一种方法是使用一系列嵌套的if-else语句。
if (value < 6) { if (value < 3) { if (value === 2) { console.log('result2'); } else if (value === 1) { console.log('result1'); } else { console.log('result0'); } } else { if (value === 3) { console.log('result3'); } else if (value === 4) { console.log('result4'); } else { console.log('result5'); } } } else { if (value < 8) { if (value === 7) { console.log('result7'); } else { console.log('result6'); } } else { if (value === 8) { console.log('result8'); } else if (value === 9) { console.log('result9'); } else { console.log('result10'); } } }
- 使用查找表(数组)比if-else和switch更快。优点是不用书写任何条件判断语句,即便候选值数量增加时,也几乎不会产生额外的性能开销。
- 当单个键和单个值之间存在逻辑映射时,查找表的优势就能体现出来。
switch
语句更适合于每个键都需要对应一个独特的动作或一系列动作的场合。 - JavaScript引擎支持的递归数量与JavaScript调用栈大小直接相关。
- 最常见的导致栈溢出的原因是不正确的终止条件。
- 字符串合并的方法:
The + operator: str = 'a' + 'b' + 'c' The += operator: str = 'a' str += 'b' str += 'c' array.join(): str = ['a', 'b', 'c'].join("") string.concat(): str = 'a' str = str.concat(['a', 'b'])
str += 'one' + 'two'
;此代码运行会经历以下几个步骤:- 在内存中创建一个临时字符串
- 连接后的字符串'onetwo'被赋值给该临时字符串
- 临时字符串与str当前的值连接
- 连接后的值赋值给str
- 优化上述代码:
str += 'one';str += 'two';
或者str = str + 'one' + 'two'
。 - 赋值表达式由str作为基础,每次给他附加一个字符串,由左向右依次连接,因此避免使用了临时字符串。
- 除IE浏览器外,其他浏览器会尝试为表达式左侧的字符串分配更多的内存,然后将第二个字符串拷贝至它的结尾。
- 在IE7及一下用
join()
可以使用如下优化:使用一个数组来接收合并的字符串,然后通过join("")
进行连接。 - 正则表达式慢的原因通常是匹配失败的过程慢,而不是匹配成功的过程慢。所以,如果修改一个正则表达式匹配过程变快而失败过程变慢,是一个错误的修改。
- 一个快速的正则表达式需要:
- 起始标记:
^ $
- 特定的字符串
- 字符类:
\d [a-z]
等 - 单词边界:
\b
- 避免:
- 分组或选择字元开头
- 类似
one|two
的顶层分支
- 起始标记:
- 使用字符集和选项组件来减少对分支的需求:
// 替换前 替换后 cat|bat [cb]at red|read rea?d red|raw r(?:ed|aw) (.|\r|\n) [\s\S]
第六章
- 用于执行JavaScript和更新用户界面的进程通常被称为“浏览器UI线程”。UI线程的工作基于一个简单的队列系统,任务会被保存到队列中直到进程空闲。一旦空闲,队列中的下一个任务就被重新提取出来并运行。这些任务要么是运行JavaScript代码,要么是执行UI更新,包括重绘和重排。
- 定时器的时间从代码创建开始时计时。并在指定时间后加入任务队列。
- 如果执行定时器的函数执行时间较长甚至超过定时器的时间,那么定时器会在函数执行完之后立马执行。
- 每个定时器的真实延时时间在很大程度上来说取决于具体情况。普遍来讲,最好使用最少25毫秒,因为在小的延时,对大多数UI更新来说不够用。
第七章
- 使用xht时,
POST
和GET
的对比: - 对于那些不会改变服务器状态,只会获取数据(幂等行为)的请求,应该使用GET。经GET请求的数据会被缓存起来,如果需要多次请求同一数据的话,他会有助于提升性能。
幂等行为:若干次请求的副作用与单词请求相同或者根本没有副作用,那么这些请求方法就能够被视作“幂等”的。GET请求对服务器不产生其他副作用,所以具有幂等属性。
- 当请求的URL加上参数的长度接近或超过2048个字符时,才应该用POST获取数据。因为有些浏览器显示URL长度,过长时将会导致请求的URL被截断。
- 当使用XHR发送数据到服务器时,GET方式会更快。因为对于少量数据而言,一个GET请求往服务器只发送一个数据包。而一个POST请求,至少要发送两个数据包,一个装载头信息,另一个装载POST正文。POST更适合发送大量数据到服务器。
- 当使用XHR时,JSON数据被当成字符串返回。该字符串紧接着被eval()转换成原生对象。然而,在使用动态脚本注入时,JOSN数据被当成另一个JavaScript文件并作为原生代码执行。为实现这一点,这些数据必须封装在一个回调函数里。这就是所谓的“JSON填充(JSON with padding)”或JSON-P。
- 最快的Ajax请求就是没有请求。有两种主要的方法可避免发送不必要的请求:
- 在服务端,设置HTTP头信息以确保你的响应会被浏览器缓存
- 在客户端,把获取到的信息储存到本地,从而避免再次请求
- 要使相应缓存,可以设置HTTP头部
Expires
,这个请求头能设置一个缓存日期,超过这个日期后,请求就直接在服务器进行请求。 - 可是手动设置缓存。也就是在页面中设置一个变量,然后使用url作为键值,请求之后 储存到变量中。如果用户执行了某些动作导致缓存失效,可以删除变量中的url。
delete localCache['/user/friendlist/']
- 第一种消除函数中的重复工作的方法是延迟加载。延迟加载意味着信息再被使用前不会做任何操作。在方法调用时,首先检查并决定使用哪种方法去绑定或取消绑定事件处理器。然后原始函数被包含正确操作的新函数覆盖。
- 另一种方法是 条件预加载,他会在脚本加载期间提前检测,而不会等到函数被调用。使用三元表达式进行设置。
const addHandler = document.body.addEventListener ? ... : ...
- 条件预加载确保所有函数调用消耗的时间相同。其代价是需要在脚本加载时就检测,而不是加载后。预加载适合用于一个函数马上就要被用到,并且在整个页面的生命周期中频繁出现的场合。
- JavaScript的原生方法总会比你写的任何代码都要快,尽量使用原生方法。
第九章
- 网站提速中的重要一条规则,就是减少页面渲染所需的http请求数,特别是针对那些首次访问网站的用户。一个简单的方式就是把部分文件和代码合并成一个外链文件。
- JavaScript压缩指的是把JavaScript文件中所有与运行无关的部分进行剥离的过程。剥离的内容包括注释和不必要的空白字符。
第十章
可以使用手动插入代码的方式来记录代码的运行时间。
const Timer = { _data: {}, start: key => { Timer._data[key] = +new Data() }, stop: key => { let time = TImer._data[key] if(time) { Timer._data[key] = +new Data() - time } }, getTime: key => { return Timer._data[key] } } Timer.start('create') for(let i=0;i<1000;i++) { element = document.createElement('div') } Timer.stop('create') console.log(Timer.getTime('create'))
- 传统上,浏览器限制每次请求只能发出一个脚本请求。这样做是为了管理文件之间的依赖关系。脚本之间存在间隙就说明脚本被阻塞了。有些浏览器的解决办法是允许并行下载,但阻塞运行。虽然这样做能使文件下载得更快,但页面渲染仍然会被阻塞,直到所有脚本都被执行。