最近读了一本关于如果写出性能更优的JavaScript的书,提取了一些相关的知识点

JavaScript 文件加载及执行

我们都知道一个完整前端页面想要展示出来,需要包含 html, css,和JavaScript 文件,其中 html 是框架,css是样式,JavaScript负责交互。但是多数浏览器采用单一进程来处理用户界面ui和JavaScript脚本执行,同一时刻只能做一件事,所以JavaScript执行的时间越久浏览器等待的时间就会越久

浏览器页面在获取到页面时首先会先解析html元素,在解析的过程中,如果遇到css文件或者js文件的时候会去下载文件,下载完成之后然后再去解析文件,但是针对JavaScript文件来说,其实浏览器并不知道js有没有执行改变页面元素的操作,因此浏览器在下载和解析文件的时候,其实是一个暂停的操作,所以当 js 文件执行时间越久,页面空白的时间就越长

解决方式:

  • 将script 脚本尽量放在页面的最下方,保证页面解析完成之后才回去解析JavaScript脚本
  • 针对script 脚本的数量做限制,每遇到一个script标签,浏览器都会停止其他操作去解析script脚本,而script脚本越大,下载时间越长,数量越多,http链接的额外请求的消耗越大。
  • script 标签的async 属性和 defer 属性,async 属性可以让资源在下载过程中不阻塞浏览器的解析,当下载完成之后在解析脚本文件,defer 属性则是延迟加载 script 文件,当页面解析完成之后才会去加载脚本文件
  • 动态加载脚本文件。对于首屏加载不需要的脚本文件,我们可以在使用的时候再去加载文件。我们可以动态创建script标签,当js文件加载完成之后,再把script标签 append 到html文档中

数据存储方式

JavaScript中有四种基本数据存储位置

  • 字面量
    字面量代表自身,不存储在特定位置,JavaScript中的字面量有字符串、数字、布尔值、对象、数组、函数、正则表达式、null、undefined
  • 本地变量
    开发人员使用关键字 var,let const 定义的数据存储单元
  • 数组元素
    存储在JavaScript数组对象内部,以数字作为索引
  • 对象成员
    存储在JavaScript对象内部,以字符串作为索引

大多数情况下,从字面量和本地变量存取数据性能是微不足道的,访问数组元素和对象成员的代价则高一些

作用域

作用域决定了那些数据能够被访问,作用域内的每个对象都被成为一个可变对象,每个对象以键值对的形式存在,当函数创建后,作用域链就会被创建此函数的作用域中科访问的数据对象所填充,在函数内部表现为 [[Scope]] 属性

函数在被执行的时候,就会创建一个执行环境,函数每次执行时对应的执行环境都是独一无二的,同时调用一个函数会创建多个执行环境,函数调用完成后,执行环境就会被销毁

函数的作用域链是以栈的形式存在的,当一个函数被创建时,会创建一个局部作用域放到栈的顶端,当函数执行完成后,作用域会从栈顶删除,当在函数中想要寻找一个变量时,执行环境会沿着作用域链向下寻找,最后到全局作用域,如果找到了变量,就返回变量所存储的内容,否则返回 undefined

因此如果我们查找的对象在作用域链中越深,查找就越耗费时间,因此我们可以在函数的局部作用域链中,创建一个新的变量来存储我们查找的对象

使用 withtry...catch... 方法可以修改函数的作用域链对象,但是实际并不推介这样做

闭包

闭包时 JavaScript 最强大的特性之一,它允许函数访问余部作用域链之外的数据,但是频繁的使用闭包有可能会造成问题。

平时我们使用函数时,会创建一个局部作用域,当函数执行完成后,这个局部作用域会被删除,但是由于引入了闭包,引用依然存在,导致这个局部作用域无法被销毁掉,造成了更多的内存开销,造成内存泄漏

对象

JavaScript的对象是基于原型,原型定义并实现了一个新创建的对象所必须包含的成员泪飙,原型对象为所有对象实例共享。

因此对象中有两种成员类型:实例成员和原型成员,可以用 hasOwnProperty 方法来判断对象是否包含特性的实例成员

当我们存取对象的属性或者方法时,会先查找对象的实例成员是否存在,如果不存在,则会沿着原型链进行查找,因此,对象在原型链存在的位置越深,查找起来越慢

因此我们要学会缓存对象成员的值,尽量避免在同一个函数中多次读取同一个实验对象

DOM对象

DOM 是浏览器对象,是浏览器提供给JavaScript用于访问文档中的数据API,由于javascript和dom两个相互独立的功能只能通过接口彼此连接,因此会产生消耗。

我们每一次访问或者修改dom属性都会产生性能消耗,因此我们可以减少DOM的访问次数,把大部分的运算留在 ECMAScript 这一端

innerHTML 和 DOM方法

innerHTML相对来说要快一点,因为它在绝大部分浏览器中都运行的很快,但是对于大部分的日常操作而言,并没有太大区别,我们更应该根据可读性,稳定性,团队习惯和代码风格来综合决定使用哪种方式

HTML集合

  • document.getELementsByName()
  • document.getELementsByClassName()
  • document.getELementsByTagName()
  • document.images
  • document.links
  • document.forms

这些集合的长度会随着页面元素的变动而变动,所以我们不能简单的使用集合的长度来作为判断,我们可以把集合拷贝一个普通数组,在普通数组上进行操作

在方位内集合属性可以采用局部变量的方式存储集合元素的部分属性,避免多次重复读取,浪费性能

重绘和重排

引起重绘的方法:

  • 元素的颜色等非几何属性发生变化

引起重排的方法:

  • 添加或者删除可见dom元素
  • 元素的位置发生改变
  • 元素的几何尺寸发生变化
  • 内容发生改变
  • 页面初始化
  • 浏览器窗口改变
    以上这些改变都会造成浏览器需要重新计算页面元素的位置,而重绘则不需要重新计算页面元素位置,重排一定会引起重绘,但是重绘不一定会引起重排

我们在操作页面元素时,要尽量减少页面重绘及重排次数,每一次重排都需要大量的计算,消耗浏览器性能,可以将多次重绘重排整合成一次进行操作

  • 将页面元素变为不可见元素,在修改元素的属性,此时只需要发生两次重排
  • 使用 文档碎片在DOM外创建子树再拷贝回文档
  • 拷贝元素,修改副本,在替换原始元素

浏览器尝试通过队列化修改和批量执行的方式最小化重排次数,当你查询布局信息时,比如偏移量,滚动位置,浏览器为了返回最新的值,会刷新队列并应用所有变更,因此我们可以设置局部变量用来存储布局信息,避免重复获取

使用事件委托

当页面存在大量需要一次或者多次绑定事件的元素,会占用更多的内存,我们可以事件委托,只需要在外层元素绑定一个处理器,就可以处理起子元素上的所有事件

算法及流程控制

  • 减少迭代的工作量,对于常用且不便的数据进行缓存处理
  • 减少迭代次数(达夫设备:循环展开技术,使得一次迭代中实际执行力多次迭代)
  • 优化if...else...语句,最常用的判断放在最前面,这样可以减少对比次数
  • 递归操作,根据情况缓存每一次递归操作的值
  • 使用对象或者数组来缓存数据

快速响应的用户界面

  • 任何javascript任务都不应当执行超过 100 ms 过长的运行时间会导致UI更新出现明显的延迟,从而对用户的体验造成负面影响
  • 使用定时器来安排代码的延迟执行,把长时间运行脚本分解成一系列的小任务
  • 使用web worker 开启新的 javascript线程