第1章 加载和执行
多数浏览器使用单一进程来处理用户界面(UI)刷新和JavaScript脚本执行。
脚本位置
浏览器在解析到<body>
标签之前,不会渲染页面的任何部分。
推荐将所有的<script>
标签尽可能放到<body>
标签的底部,以尽量减少对整个页面下载的影响。
组织脚本
减少页面包含的<script>
标签能减少页面阻塞。
通过把多个文件合并成一个,可以减少性能消耗。
延迟的脚本
使用defer
或者async
属性异步加载脚本。
带有defer
属性的<script>
标签在解析时开始下载,但不会执行,直到DOM加载完成(onload事件之前触发)。
动态脚本元素
可以通过JavaScript动态创建<script>
标签并插入,这种技术的优点在于无论在何时启动下载,文件的下载和执行过程不会阻塞页面其他进程。
XMLHttpRequest脚本注入
通过请求获取js文件,并通过<script>
标签插入到页面中。
优点是可以自定义推迟脚本的执行,缺点是JavaScript文件必须与所请求的页面处于相同的域。
推荐的无阻塞模式
先添加动态加载所需的代码,然后加载初始化页面所需的剩下的代码。
CSS文件是并行下载,不会阻塞页面的其他进程。
第2章 数据存取
JavaScript有四种基本的数据存取位置:
-
字面量
字面量只代表自身,不存储在特定位置。字符串、数字、布尔值、对象、数组、函数、正则表达式,以及特殊的null和undefined值。
-
本地变量
使用关键字let、const定义的数据存储单元。
-
数组元素
存储在JavaScript数组对象内部,以数组作为索引。
-
对象成员
存储在JavaScript对象内部,以字符串作为索引。
使用字面量和局部变量,减少数组和对象成员的使用可以提高运行速度。
作用域链和标识符解析
每一个JavaScript函数都是Function
对象的一个实例(箭头函数除外),内部属性[[Scope]]
包含了一个函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链。
执行函数会创建一个执行环境的内部对象,当函数执行完毕,执行环境就被销毁。
标识符解析的性能
在执行环境的作用域链中,一个标识符所在的位置越深,它的读写速度也就越慢。
如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量里。
动态作用域
动态作用域只存在于代码执行过程中,无法通过静态分析
function execute(code) {
eval(code);
function subroutine() {
return window;
}
const w = subroutine();
}
execute('const window = {}');
闭包、作用域和内存
使用闭包可能会造成内存泄漏,可以通过引入局部变量来减轻闭包对执行速度的影响。
对象成员
对象成员包括属性和方法,当一个被命名的成员引用了一个函数,该成员就被称为一个“方法”,相反,引用了非函数类型的成员就被称为“属性”。
原型
访问原型链中的位置越深,找到它也就越慢。
const book = {
title: '123',
publisher: '456'
}
console.log(book.hasOwnProperty('title')) // true
console.log(book.hasOwnProperty('toString')) // false
console.log('title' in book) // true
console.log('toString' in book) // true
嵌套成员
对象成员嵌套得越深,读取速度就会越慢。
缓存对象成员值
在函数中如果要多次读取同一个对象属性,最佳做法是将属性值保存到局部变量中。(这种优化技术不推荐用于对象的成员方法,会导致this绑定出问题)。
第3章 DOM编程
DOM访问与修改
访问和修改DOM会导致浏览器重新计算页面的几何变化,造成性能开销。
推荐写法:减少访问DOM的次数,把运算留给ECMAScript来处理。
HTML集合
HTML集合(类数组)一直与文档保持着连接,每次你需要最新的信息时,都会重复执行查询的过程。
在相同的内容和数量下,遍历一个数组的速度快于遍历一个HTML集合。
需要多次访问同一个DOM属性或方法时,最好使用一个局部变量缓存此成员。
重绘与重排(Repaints and Reflows)
浏览器下载完页面中的所有组件之后会解析并生成两个内部数据结构:
-
DOM树
表示页面结构
-
渲染树
表示DOM节点如何显示
DOM树中的每一个需要显示的节点在渲染树中至少存在一个对应的节点(隐藏的DOM元素在渲染树中没有对应的节点)。一旦DOM树和渲染树构建完成,浏览器就开始绘制paint页面元素。
当DOM的变化影响了元素的几何属性(宽和高)——比如改变边框宽度或给段落增加文字,导致行数增加——浏览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。这个过程称为“重排(reflow)”。完成重排后,浏览器会出现绘制受影响的部分到屏幕中,这个过程称为“重绘(repaint)”。
并不是所有的DOM变化都会影响几何属性,例如改变一个元素的背景色不会影响它的宽和高,这种情况下只会发生一次重绘,因为元素的布局并没有改变。
重绘和重排都是代价昂贵的操作,尽可能减少这类过程的发生。
重排何时发生
- 添加或删除可见的DOM元素。
- 元素位置改变。
- 元素尺寸改变(包括:margin、padding、border、width、height等属性改变)。
- 内容改变,例如文本改变或图片被另一个不同尺寸的图片替代。
- 页面渲染器初始化。
- 浏览器窗口尺寸改变。
根据改变的范围和程度,渲染树中或大或小对应的部分也需要重新计算,有些改变会触发整个页面的重排。
渲染树变化的排队与刷新
由于每次重排都会产生计算消耗,大多数浏览器通过队列化修改并批量执行来优化重排过程。然而你可能会不知不觉中强制刷新队列并要求计划任务立即执行。获取布局信息的操作会导致队列刷新,比如以下方法:
- offsetTop、offsetLeft、offsetWidth、offsetHeight
- scrollTop、scrollLeft、scrollWidth、scrollHeight
- clientTop、clientLeft、clientWidth、clientHeight
- getComputedStyle()
以下属性和方法需要返回最新的布局信息,因此浏览器不得不执行渲染队列中的“待处理变化”并触发重排以返回正确的值。
最小化重绘和重排
改变样式
使用cssText
合并多次样式的修改
el.style.cssText += '; border-left: 1px;'
批量修改DOM
- 使元素脱离文档流。(重排)
- 对其应用多重改变。
- 把元素带回文档中。(重排)
使DOM脱离文档的三个基本方法:
- 隐藏元素,应用修改,重新显示。
- 使用文档片段(docuement fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。(推荐)
- 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素。
事件委托
事件绑定占用了处理时间,浏览器需要跟踪每个事件处理器,这也会占用更多的内存。
第4章 算法和流程控制
条件语句
条件数量越大,越倾向于使用switch
而不是if-else
。
优化if-else
if-else
中的条件语句应该总是按照从最大概率到最小概率的顺序排列,以确保运行速度最快。
另一种减少条件判断次数的方法是把if-else
组织成一系列嵌套的if-else
语句。
if (value < 6) {
if (value < 3) {
if (value === 0) {
} else if (value === 1) {
} else {
}
} else {
if (value === 3) {
} else if (value === 4) {
} else {
}
}
} else {
if (value < 8) {
if (value === 6) {
} else {
}
} else {
if (value === 8) {
} else if (value === 9) {
} else {
}
}
}
查找表
当有大量离散值需要测试时,可以使用数组和普通对象来构建查找表。
const results = [result0, result1, result2, result3, result4, result5, result6]
return results[value]
第5章 字符串和正则表达式
正则表达式(回溯)
可以通过把贪婪量词星号(*)
替换成惰性(非贪婪)量词*?
。
更多提高正则表达式效率的方法
-
关注如何让匹配更快失败。
-
正则表达式以简单、必需的字元开始。
起始标记通常是一个锚(^或$)、特定字符串(x或\u263A)、字符类([a-z]或类似\d的速记符)和单词边界(\b)。
-
使用量词模式,使他们后面的字元互斥。
-
减少分支数量,缩小分支范围。
替换前 替换后 cat|bat [cb]at red|read rea?d red|raw r(?:ed|aw) (.|\r|\n) [\s\S] -
使用非捕获组。
-
只捕获感兴趣的文本以减少后处理。
-
暴露必需的字元。
-
使用合适的量词。
-
把正则表达式赋值给变量并重用它们。
-
将复杂的正则表达式拆分为简单的片段。
使用正则表达式去首尾空白
String.prototype.trim = () => {
return this.replace(/^\s+/, "").replace(/\s+$/, "");
}
混合解决方案
使用正则表达式方法过滤头部空白,用非正则表达式的方法过滤尾部字符。
String.prototype.trim = () => {
let str = this.replace(/^\s+/, ""),
end = str.length - 1,
ws = /\s/;
while (ws.test(str.charAt(end))) {
end--;
}
return str.slice(0, end + 1);
}
第6章 快速响应的用户界面
浏览器UI线程
用于执行JavaScript和更新用户界面的进程通常被称为“浏览器UI线程”。UI线程的工作基于一个简单的队列系统,任务会被保存到队列中直到进程空闲。一旦空闲,队列中的下一个任务就被重新提取出来并运行。
大多数浏览器在JavaScript运行时会停止把新任务加入UI线程的队列中。
浏览器限制
浏览器限制了JavaScript任务的运行时间,这种限制确保某些恶意代码不能通过永不停止的密集操作锁住用户的浏览器或计算机。
限制分为两种:调用栈大小限制和长时间运行脚本限制。
多久才算“太久”
研究表明,单个JavaScript操作花费的总时间不应该超过100毫秒。
定时器基础
定时器代码只有在创建它的函数执行完成之后,才有可能被执行。
创建一个定时器会造成UI线程暂停,如同它从一个任务切换到下一个任务。因此,定时器代码会重置所有相关的浏览器限制,包括长时间运行脚本定时器,调用栈也在定时器的代码重置为0,这一特性使得定时器称为长时间运行JavaScript代码理想的跨浏览器解决方案。
如果UI队列中已经存在由同一个setInterval()创建的任务,那么后续任务不会被添加到UI队列中。
使用定时器处理数组
是否可以用定时器取代循环的两个决定性因素:
- 处理过程是否必须同步?
- 数据是否必须按顺序处理?
定时器的延时最好使用至少25毫秒,因为再小的延时,对大多数UI更新来说不够用。
function processArray(items, process, callback) {
const todo = items.concat();
setTimeout(function () {
process(todo.shift());
if (todo.length > 0) {
setTimeout(arguments.callee, 25);
} else {
callback(items);
}
}, 20);
}
第7章 Ajax
数据传输
Ajax,是一种与服务器通信而无须重载页面的方法。
请求数据
-
XMLHttpRequest(XHR)
不支持跨域。支持所有请求方法。
-
Dynamic script tag insertion 动态脚本注入
支持跨域。不支持POST方法。不支持设置请求头。不能设置请求的超时处理或重试。
-
iframes
-
Comet
-
Multipart XHR
通过在服务端将资源(CSS文件、HTML片段、JavaScript代码或base64编码的图片)打包成一个由双方约定的字符串分割的长字符串并发送到客户端。
发送数据
-
XMLHttpRequest(XHR)
-
信标(beacons)。
使用img标签。
数据格式
- XML
- XPath
- JSON
- JSON-P
JSON是高性能Ajax的基础。
第8章 编程实践
避免双重求值
避免使用eval()
和Function()
。
使用Object/Array直接量
避免使用new Object()
和new Array()
。
避免重复工作
别做无关紧要的工作,别重复做已经完成的工作。
延迟加载
function addHandler(target, eventType, handler) {
if (target.addEventListener) {
addHandler = function(target, eventType, handler) {
target.addEventListener(eventType, handler, false)
}
} else {
// ...
}
addHandler(target, eventType, handler)
}
条件预加载
在脚本加载期间提前检测。
const addHandler = document.body.addEventListener ? function() {...} : function() {...}
原生方法
JavaScript原生方法是通过C++编写的,拥有更好的性能,所以最好尽可能使用原生方法和属性,特别是数学运算和DOM操作。