高性能JavaScript 读书笔记(一)

279 阅读4分钟

高性能JavaScript

第一章:加载与执行

JavaScript 在浏览器中的性能 => 大多数浏览器使用单一进程来处理用户界面(UI)刷新及JavaScript脚本执行,所以同一时刻只能做一件事情, JavaScript执行越久,浏览器等待的响应时间就越长

标签每次出现会让页面等待解析和执行, 无论JavaScript代码以何种形式存在于文件中

HTML4规范指出

理论上来说:把与样式和行为有关的脚本放在一起并且加载这样有助于确保页面渲染和交互的正确性, 但这样仍然会阻塞页面的加载, 在脚本解析是 用户看到的是个空白的页面,尽管有些浏览器允许并行加载JavaScript文件,但是JavaScript下载过程中仍会阻塞其他资源的下载,例如:图片。尽管JavaScript下载过程互不影响,单是必须等到所有JavaScript代码下载并执行完成才能继续, 因此推荐将所有<script>标签尽可能放到<body>标签底部,尽量减少对整个页面下载的影响。

JavaScript加载优化首要规则:将脚本放在底部

组织脚本

JavaScript阻塞页面渲染 多个JavaScript脚本合并成一个 有助于页面加载,

加载4个25KB的JavaScript脚本时间高于加载单个100KB的JavaScript脚本

雅虎提供了JavaScript脚本合并处理器 ,将多个JavaScript脚本URL放到一个

无阻塞脚本

就是 页面加载完成后才加载JavaScript代码,专业术语来说:在window对象的load事件触发后在加载脚本。

延迟脚本

HTML4 为,所以不是个理想的跨浏览器解决方案,在其他浏览器中会呗直接忽略。

带有defer的JavaScript将在页面解析到

动态脚本元素

通过createElement('script') 动态增加script DOM. 通过script.onload 事件来 获得脚本加载完成是的状态

XMLHttpRequest脚本注入

var xhr = new XMLHttpRequest();
xhr.open("get", file.js, true);
xhr.onreadystatechange = function () {
	if(xhr.reasdyState == 4) {
		if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304 ) {
			var script = document.createElement('script');
			script.type = "text/javascript";
			sctip.test = xhr.responseText;
			document.body.appendChild(script)
		}
	}
}
xhr.send(null)

通过发送Get 请求获取 file.js文件 一旦收到有效的响应就会创建一个script标签

优点:可以下载JavaScript代码但不会立即执行, 兼容各类浏览器

局限性: 跨域问题

第二章:数据存取

JavaScript中有一下四种基本的数据储存位置:

字面量

​ 字面量只代表自身,不储存在特定的位置,JavaScript中的字面量有:字符串、数字、布尔值、数组、队形、函数、正则表达式以及特殊的null和undefined值。

本地变量

开发人员使用关键字var 定义的数据存储单元

数组元素

储存在JavaScript数组对象内部,以数字为索引

对象成员

储存在JavaScript对象内部,以字符串为索引

访问速度

总体来说字面量和局部变量的访问速度快意数组项和对象成员,如果在乎访问速度,那么尽量使用局部变量和字面量,减少数组项和对象成员的使用。

以下几种方式来定位和规避问题,优化代码:

管理作用域

首先理解作用域:

作用域链和标示解析符

每一个JavaScript函数都是Function对象的实例,都含有仅供JavaScript引擎存取的内部属性,[[ scope ]] 就是其中之一,它包含一个函数被创建的作用域中的对象集合,这个函数被称为作用域链,它决定哪些数据可以访问

tips:函数每次运行的执行环境都是独一无二的,所以多次运行同一个函数会创建多个执行环境,函数执行完毕,执行环境销毁。

每次函数运行时产生的作用域链都会被推入作用域链的最顶端 成为活动对象。

作用域链的搜索过程,先从最顶部作用域链开始搜索(即活动对象),如果找到就使用这个标识符对应的变量,否则继续搜索作用域链的下一个对象 直到找到为止,如果无法找到,那么就是未定义,正是这个搜索过程影响了性能。

tips:如果两个相同名字的变量存在作用域链中 返回先找的的那个;

标识符解析的性能

标识符所在位置越深,读写速度就越慢; 经验:如果一个跨作用域的值在函数中引用过一次以上,那么就把它储存到局部变量中

function initUI() {
	var bd = document.body,
	links = document.getElementByTagName('a'),
	i = 0,
	len = links.length;
	wile(i < len) {
		update(links[i++]);
	}
	document.getElementById('go-on').onclick = function() {
		start();
	}
	bd.className = "active";
}

该函数引用了3次document, document是全局对象,每次用时都要去最底层作用域搜索,影响性能,

优化后:

function initUI() {
	var doc = document,
  bd = doc.body,
	links = doc.getElementByTagName('a'),
	i = 0,
	len = links.length;
	wile(i < len) {
		update(links[i++]);
	}
	doc.getElementById('go-on').onclick = function() {
		start();
	}
	bd.className = "active";
}

改变作用域链

with语句可以改变作用域链

修改后代码:

function initUI() {
	  with(document) { // 避免
      var bd = body,
          links = getElementByTagName('a'),
          i = 0,
          len = links.length;
      wile(i < len) {
            update(links[i++]);
          }
      getElementById('go-on').onclick = function() {
            start();
          }
       bd.className = "active";
    }
}

with生成了新的作用域链,意味着函数的局部变量储存在第二位,with生成的心作用域放到了作用域链的首位,查找代价更高了

Try-catch 中catch语句拥有同样的问题

动态作用域

function execute(code) {
	eval(code);
	function sub() {
		return window;
	}
	var w = sub()
}

上边这段代码大多数情况下 w等同于全局对象,但是当 execute("var window = {}");时 w = {}, 这段代码只有在运行时才会发现问题,所以如果不是必要不使用动态作用域。

闭包、作用域和内存

闭包:它允许函数访问局部变量之外的数据,但使用闭包可能会导致性能问题。

尝试理解与闭包有关的性能问题

function assignEvent() {
	var id = "sdi333";
	document.getElementById("save").onclick = function (event) {
		saveDocument(id)
	}
}

由于闭包的作用域链包含了与执行环境相同的对象的引用,因此会产生副作用,

将常用的跨作用域变量储存在局部变量中,然后直接访问局部变量,来减少开销。

对象成员
原型

对象又两种成员类型:实力成员和原型成员,实力成员直接存在于对象实例中,原型成员则从对象原型继承而来

var book = {
	title: "High Performent JavaScript",
  publisher: "Press!!!"
}
alert(book.toString()); // "[object object]"

book对象没有toString方法却顺利执行了

原型链
function Book(title,publisher){
  this.title = title;
  this.publisher = publisher
}
Book.prototype.sayTitle = function() {
  alert(this.title)
}
var book1 = new Book("Heigh Performation JavaScript","Press!!!");
var book2 = new Book("JavaScript: Title","Press!!!!")
alert(book1 instanceof Book);
alert(book1 instanceof Object);
book1.sayTitle(); // "Heigh Performation JavaScript"
alert(book1.toString()) // "[object Object]"

嵌套成员

例如:window.location.href

对象成员嵌套越深,查找越费时, window.location.href 总比 location.href 费时, 如果需要解析原型链的话会花更多时间

缓存对象成员

小结

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

  • 访问字面量和局部变量的速度最快,相反,访问数组项和对象成员相对较慢
  • 由于局部变量存在于作用域链的起始位置,因此访问局部变量比访问跨作用域变量更快。变量在作用域链中的位置越深,访问所需时间越长。由于全局变量总处于作用域链的最末端,所以访问速度也是最慢的。
  • 避免使用with语句,因为它会改变执行环境作用域链,同样try-catch语句中catch子句也有同样影响,因此也要谨慎使用。
  • 嵌套对象成员会明显影响性能,所以尽量少用
  • 属性或方法在原型链中的位置越深,访问他的速度也越慢。
  • 通常来说,你可以通过吧常用的对象成员、数组项、跨域变量等保存在局部变量中来改善JavaScript性能,因为局部变量访问速度更快。

第三章DOM编程

本章讨论以下三类问题:

  • 访问和修改DOM元素

  • 修改DOM元素样式会导致重绘(repaint)和重排(reflow)

  • 通过DOM事件处理与用户的交互

首先----什么是DOM?它为什么慢?

浏览器中的DOM

文档对象模型(DOM)是一个独立于语言的,用于操作XML和HTML文档的程序接口(API).

浏览器通常会把DOM和JavaScript独立实现

天生就慢

简单理解:两个相互独立的功能,只要通过接口彼此连接,就会产生消耗。

DOM访问与修改

访问DOM元素是有代价的,修改元素更为昂贵,因为它会导致浏览器重新计算页面的几何变化。

最坏的情况是在循环中访问和修改元素,尤其是对HTML元素集合循环操作

代码示例:

function innerHTMLLoop() {
	for(var count = 0; count < 15000; count++) {
    document.getElementById('here').innerHtml += "a";
  }
}

每次循环迭代元素都会访问两次:一次读取innerHTML属性值,另一次重写他

换一种效率更高的方法:

function innerHTMLLoop() {
  let str = ''
	for(var count = 0; count < 15000; count++) {
     str += "a";
  }
  document.getElementById('here').innerHtml += str;
}

通用的经验法则:减少访问DOM的次数,把运算尽量留在ECMAScript端。

innerHTML对比DOM方法

修改页面区域的方法:innerHTML 和 document.createElement(), 性能几乎相差无几,但是最新出版本的浏览器:innerHTML更快些

但是日常操作用的话 这两个性能相差无几

节点克隆

使用DOM方法更新页面内容的另一个途径是:克隆已有元素 而不是创建新的元素。 element.coloneNode()(element表示已有节点) 代替 document.createElement()

HTML集合

HTML集合就是包含DOM节点引用的类数组对象。以下方法的返回值就是一个集合:

  • document.getElementsByName()

  • document.getElementsByClassName()

  • document.getElementsByTagName()

下面的属性同样返回HTML集合:

document.images 页面中所有的img 元素

document.links 所有 a 元素

document.forms[0].elements 页面中第一个表单的所有字段

以上方法和属性返回HTML集合 这是个类似数组的列表,他们并不是真正的数组,但有一个类似数组中的length属性,并且还能以数字索引的方式访问列表中的元素。

事实上,HTML集合一直与文档保持着连接,每次你需要最新的信息是,都会重复执行查询的过程,哪怕只是获取集合里的元素个数(即访问集合的length属性)也是如此。这正是低效之源。

昂贵的集合
var alldivs = document.getElemetnsByTagName('div');
for (var i = 0; i < alldivs.length; i++) {
  document.body.appendChlid(document.createElement('div'))
}

这段代码是个死循环, 引文alldivs的长度在每次迭代是都会增加。

访问集合时使用局部变量
//较慢
function collectionGlobal() {
  var coll = docuemnt.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;
    name = document.getElementsByTagName('div')[count].tagName;
  }
  return name;
}
// 较快
function collectionLocal() {
  var coll = docuemnt.getElementsByTagName('div'),
      len = coll.length,
      name = "";
  for (var count = 0; count < len; count++) {
    name = coll[count].nodeName;
    name = coll[count].nodeType;
    name = coll[count].tagName;
  }
  return name;
}
// 最快
function collectionLocal() {
  var coll = docuemnt.getElementsByTagName('div'),
      len = coll.length,
      name = "",
      el = null;
  for (var count = 0; count < len; count++) {
    el = coll[count]
    name = el.nodeName;
    name = el.nodeType;
    name = el.tagName;
  }
  return name;
}
元素节点

childNodes, firstChild和 nextSibling并不区分元素节点和其他类型的节点 (比如:注释和文本节点(两个节点间的空格))。 在某些情况下需要过滤掉非元素节点。这些类型检查个过滤其实是不必要的DOM操作。

使用children 代替childNodes 会更快,因为集合更少。

选择器API
var elements = docuemnt.querySelectorAll('#menu a');
var elements = docuemnt.gerElementById('menu').getElementsByTagName('a')

如上: querySelectorAll() 更快。

所以:需要处理大量组合查询,使用querySelectorAll()的话会更有效率。

重绘与重排

浏览器下载完成页面中的所有组件————HTML标记、JavaScript、CSS、图片 ————之后会解析并生成两个内部数据结构:

DOM树

​ 表示页面结构

渲染树

​ 表示DOM节点如何显示

DOM树中的每一个需要显示的节点在渲染树中至少存在一个对应的节点(隐藏的DO们元素在渲染树中没有对应的节点)。渲染树中的节点被称为“帧(frames)”或“盒(boxes)”,符合CSS模型的定义,理解页面元素为一个基友内边距(padding),外边距(margins),边框(borders)和 位置(position)的盒子。一旦DOM和熏染树构建完成,浏览器即开始显示(绘制“paint”)页面元素。

当DOM的变化影响了元素的集合属性(宽和高)——比如改变边框宽度或给段落增加文字,导致行数增加——浏览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。这个过程称为“重排(reflow)”。完成重排后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为“重绘(repaint)”。

重排何时发生

当页面布局和几何属性改变是就需要“重排”。下述情况会发生重排。

  • 添加或删除可见的DOM元素。

  • 元素位置改变。

  • 元素尺寸改变(包括:外边距、内边距、边框厚度、宽度、高度等属性改变)。

  • 内容改变,例如:文本改变或者图片被另一个不同尺寸的图片代替。

  • 页面渲染器初始化。

  • 浏览器窗口尺寸改变

根据改变的范围和程度,渲染树中或大或小的对应的部分也需要重新计算,这些改变会触发整个页面的重排:例如,当滚动条出现时。

渲染树变化的排队与刷新

获取布局信息的操作会导致列队刷新,比如一下方法:

  • offsetTop, offsetLeft, offsetRight, officeWidth
  • scrollTop, scrollLeft, scrollRight, scrollWidth
  • clientTop, clientLeft, clientRight, clientWidth
  • getComputedStyle()(currentStyle in IE)
var computed,
    tmp = "",
    bodystyle = document.body.style;
if(document.body.currentStyle){ // IE, OPERA
  computed = document.body.currentStyle;
}else { //W3C
  computed = document.defaultView.getComputedStyle(document.body,'');
}
// 修改同一属性低效的方式
// 然后获取样式信息
// bad
bodyStyle.color = 'red';
tmp = computed.backgroundColor;
bodyStyle.color = 'white';
tmp = computed.backgroundImage;
bodyStyle.color = 'green';
tmp = computed.backgroundAttachment;
// good
bodyStyle.color = 'red';
bodyStyle.color = 'white';
bodyStyle.color = 'green';
tmp = computed.backgroundColor;
tmp = computed.backgroundImage;
tmp = computed.backgroundAttachment;

最小化重绘和重排

改变样式
// bad
var el = document.getElementById('mydiv');
el.style.borderLeft = '1px;'
el.style.borderRight = '2px;'
el.style.padding = '5px;'

// good
var el = document.getElementById('mydiv');
el.style.cssText = 'border-left:1px;border-right:2px;padding:5px;';

// 或一次性修改css的class名称
var 
el = document.getElementById('mydiv');
el.className = 'active';
批量修改DOM

当你需要对DOM元素进行一系列操作时,可以通过一下步骤来减少重绘和重排的次数:

  1. 使文档脱离文档流
  2. 对其应用多重改变
  3. 把元素带回文档中

该过程里会触发两次重排——第一步和第三部。如果你忽略这两个步骤,那么第二步产生的任何修改都会触发一次重排。

缓存布局信息

当你查询布局信息时,比如获取偏移量(offsets)、滚动位置(scroll values)或计算出的样式值时,浏览器为了返回最新值,会刷新列队应用所有变更。

// 低效的
myElement.style.left = 1 + myElement.offsetLeft + 'px';
myElement.style.top = 1 + myElement.offsetTop + 'px';
if(myElement.offsetLeft >= 500) {
  stopAnimation()
}

// good
current++
myElement.style.left = current + 'px';
myElement.style.top = current + 'px';
if(current >= 500) {
  stopAnimation()
}
让元素脱离动画流

使用一下步骤可以避免页面中的大部分重排:

  1. 使用绝对位置定位页面上的动画元素使其脱离文档流。
  2. 让元素动起来。当他扩大时。会临时覆盖部分页面。但这只是页面一个小区域的重绘过程,不会产生重排并重绘页面的大部分内容。
  3. 当动画结束时恢复定位,从而只会下移一次文档的其他元素。

小结

访问和操作DOM是现代Web应用的重要部分。但每次穿越连接ECMAScript 和DOM两个岛屿之间的桥梁,都会被收取“过桥费”。为了减少DOM编程带来的性能损失,请记住一下几点:

  • 最小化DOM访问次数,尽可能在JavaScript端处理。
  • 如果需要多次访问某个DOM节点,请使用局部变量储存它的引用。
  • 小心处理HTML集合,因为它实时连接着底层文档。把集合的长度缓存到一个变量中,并在迭代中使用它。如果需要经常操作集合,建议把它拷贝到一个数组中。
  • 如果可能的话,使用速度更快的API,比如:querySelectorAll()和firstElementChild。
  • 要留意重绘和重排;批量修改样式时,“离线”操作DOM树,使用缓存,并减少访问布局信息次数。
  • 动画中使用绝对定位,使用拖放代理
  • 使用事件委托来减少时间处理器的数量。