大前端進階-js性能優化

內存管理

內存由可讀寫單元組成,表示一片連續可操做的空間。在編程時,能夠經過主動操做來申請,使用和釋放可操做空間。內存管理指的就是主動操做過程,也就是申請內存,使用內存和釋放內存。算法

// 申請內存
let str
// 使用內存
str = 'foo'
// 釋放內存
str = null // 再也不引用,垃圾回收會自動回收內存

垃圾回收

當內存再也不被使用時,其會被視爲垃圾,而後被釋放(回收)。編程

在JavaScript中,垃圾回收是自動進行的。

如何判斷垃圾內存?數組

  1. 對象再也不被引用。
  2. 對象不能從根上訪問到。
「根」在js中,能夠將根看做全局對象。不能從根上訪問到指的就是不能從全局對象上經過某條路徑找到,能夠是直接掛載在全局對象上,也能夠是間接掛載在全局對象上。
function fn(obj1, obj2) {
    obj1['next'] = obj2
    obj2['pre'] = obj1
    return {
        o1: obj1,
        o2: obj2
    }
}
const obj = fn()

上述代碼的關係以下圖所示,此時obj,obj1,obj2均可以從全局對象上找到,所以不能看成垃圾被回收。
微信圖片_20200725155844.png
以下圖所示,若是經過delete將obj的o1屬性和obj2的prev屬性刪除,那麼obj1就沒法從全局對象上找到,此時obj1將會被看成垃圾回收。
微信圖片_20200725155951.png瀏覽器

可達對象

可到對象指的是能訪問到的對象,訪問的方式能夠是引用,也能夠是經過做用域鏈查找到。
判斷一個對象是不是可達對象的標準就是從根出發是否能夠被找到。緩存

GC算法

GC能夠理解爲是垃圾回收機制的簡寫。算法也就指的是查找垃圾,回收垃圾的規則。
經常使用的GC算法包含如下幾個:微信

  1. 引用計數
  2. 標記清除
  3. 標記整理
  4. 分代回收

引用計數算法

經過引用計數器設置內存的引用數,當內存的引用關係發生改變的時候修改引用數,當引用數爲0的時候內存當即被回收。閉包

// {name: 'zs'}所在的空間是一塊內存
// 此時obj1引用這塊內存,因此引用計數器上記爲1
let obj1= {name: 'zs'}
// obj2 一樣引用了這塊內存,因此引用計數器爲2
let obj2 = obj1
// obj1 再也不引用這塊內存,因此計數器變爲1
obj1 = null
// obj2也再也不引用這塊內存,此時計數器爲0.這塊內存會被看成垃圾回收
obj2 = null

算法優勢:app

  • 發現垃圾時當即回收。
  • 最大程度減小程序暫停(垃圾回收時程序會被暫停,若是回收的速度快,那麼暫停的時間也就越少)。

算法缺點:dom

  • 沒法回收循環引用的對象。
function fn() {
    const obj1 = { name: 'zs' }
    const obj2 = { name: 'ls' }
    // 在方法執行完畢之後,obj1和obj2應該被看成垃圾被回收,可是因爲其相互引用,此時引用計數器上不爲0, 因此沒法回收
    obj1['friend'] = obj2
    obj2['friend'] = obj1
}
fn()
  • 時間開銷大(因爲須要引用計數器,當引用計數器對象越大,每次修改引用數的時間越長)。

標記清除算法

標記清除算法將垃圾回收分爲標記和刪除階段,其算法步驟以下:jsp

  1. 遍歷全部對象,找到活動對象進行標記。
  2. 遍歷全部對象,找到全部沒有標記的對象並清除。

以下圖所示,第一不找到全部活動對象,因爲ABCDE能夠經過全局對象找到,因此被標記,a1和b1不能經過全局對象找到,因此不會被標記。第二步,找到沒有被標記的a1和b1,將其看成垃圾回收。
微信圖片_20200725155851.png
與引用計數算法相比。
優勢:

  • 能夠回收循環引用的對象

缺點:

  • 回收後內存地址可能再也不連續,形成碎片化。

假設內存中有一段連續的內存空間ABCDEF,若是BCDE被標記爲活動對象,AB和F沒有被標記,那麼AB,F會被看成垃圾回收。回收完成後,形成存在AB和F兩個碎片內存能夠被使用,其只能放入對應長度的數據。

標記整理算法

標記整理算法和標記清除算法相似,只是多了整理內存步驟。

  1. 遍歷全部對象,找到活動對象進行標記。
  2. 遍歷全部對象,整理標記的內存,而後找到全部沒有標記的對象並清除。

經過整理,能夠解決標記清除算法形成內存碎片化的問題。

V8引擎

V8是一款主流的JavaScript執行引擎,採用即時編譯,內存有限制(64位1.5G,32位800M)。

垃圾回收策略

js中的數據分爲原始數據和對象引用數據兩種,其中原始數據是由語言自己去處理,因此此處的垃圾回收策略主要針對棧上的對象引用數據。
V8採起分代回收的策略,因爲v8對內存大小有限制,因此其將內存分紅新生代和老生代兩種,不一樣的生代採起不一樣的垃圾回收策略。
V8主要採起的GC算法有以下:

  1. 分代回收
  2. 空間複製
  3. 標記清除
  4. 標記整理
  5. 標記增量

新生代

V8將內存分爲兩塊,其中小的空間稱爲新生代(64位32M/32位16M),其主要存儲存活時間較短的對象。新生代內部一樣分爲兩個等大小的空間From和To,經過空間複製和標記整理兩個算法完成垃圾回收。

  1. From爲使用空間,To爲空閒空間,活動對象存儲在From。
  2. 標記整理後從From拷貝到To。
  3. 清理From,將From和To交換空間。

From到To的拷貝過程可能出發晉升,也就是重新生代拷貝到老生代,下面兩種狀況將出發晉升。

  1. 一輪GC以後還存活的新生代。
  2. To空間的使用率超過25%。

老生代

老生代指的是空間較大的內存塊(64位1.4G,32位700M),其內部存儲存活時間長的對象,採用標記清除,標記整理和增量標記三種算法實現垃圾回收。

  1. 首先採用標記清除進行垃圾回收(會遺留空間碎片)。
  2. 新生代向老生代拷貝而且老生代存儲區不足的時候進行空間優化(標記整理)。
  3. 採用增量標記進行效率優化(js代碼執行和垃圾回收互斥,執行垃圾回收時沒法執行js代碼,增量標記指的時將遍歷對象進行標記的過程拆分紅多個小的執行段,這樣js代碼執行和標記過程可交叉進行)。

內存問題

js代碼在瀏覽器中執行的時候,可能出現的和內存相關的問題以下:

  • 內存泄露: 內存使用持續增長。
  • 內存膨脹: 內存使用短期內暴漲,超過內存限制。
  • 分離Dom: Dom節點沒有在Dom樹上,被變量引用致使沒法回收。
  • 頻繁GC: GC操做會暫停代碼執行,頻繁GC會使得頁面卡頓。

代碼優化

慎用全局變量

全局變量會致使的問題以下:

  • 全局變量存在於全局上下文,全局上下文是做用域鏈的頂端,當經過做用域鏈進行變量查找的時候,會延長查找時間。
  • 全局執行上下文會一直存在於上下文執行棧,直到程序推出,這樣會影響GC垃圾回收。
  • 若是局部做用域中定義了同名變量,會遮蔽或者污染全局。

緩存全局變量

將不可避免的全局變量緩存到局部做用域中,減小查找時間,優化性能。適用於在局部做用域中頻繁使用某個全局變量。

function query() {
    // 在局部做用域中直接使用全局的document變量,在執行時,局部做用域找不到該變量,會沿着做用域鏈向上查找直到在全局中找到
    return document.getElementsByTagName('input')
}

function query1() {
    // 經過將全局變量賦值給局部變量,那麼查找時直接在局部做用域找到,不用再向上查找
    let dom = document
    return dom.getElementsByTagName('input')
}

經過原型新增方法

在爲全部的實例對象添加共享方法的時候,經過原型定義比在構造函數中經過this定義性能更好。這是因爲構造函數中this定義的方法在每一個實例中都會保存一份單獨的引用,而經過原型定義,全部的實例會指向同一個引用。

function Person() {
    // 每一個實例對象都會保存一份say的引用,10個就會有10個內存引用
    this.say = function () {
        console.log(1)
    }
}
const zs = new Person()

function Person1() { }
// 全部實例的原型都指向一個內存引用,減小內存開銷
Person1.prototype.say = function () {
    console.log(1)
}
const ls = new Person1()

避開閉包陷井

閉包是指在外部做用域中可使用內部做用域中的變量。

function foo() {
    let str = 'foo'
    return function () {
        console.log(str)
    }
}

let f = foo()
// f在外部執行的時候依然可以訪問foo做用域中的str變量
f()

閉包是一種常見寫法,能夠解決js編程中的不少問題,可是因爲內部做用域中的變量被外部引用,因此此變量不能被垃圾回收,若是使用不當很容易形成內存泄露,所以在編程中不能爲了閉包而閉包

避免屬性訪問方法使用

js在編寫類的時候,很容易的出如今類上提供一個方法,該方法用於訪問類內部的一個屬性。

function Person() {
    this.name = 'foo'
    // 爲了便於控制,在屬性的訪問上添加了一層
    this.getName = function () {
        return this.name
    }
}
const zs = new Person()
console.log(zs.getName)

function Person1() {
    this.name = 'foo'
}
const ls = new Person1()
// 直接訪問屬性
console.log(ls.name)

經過jsperf測試,發現直接訪問會比包裝訪問要快的多。所以拋開代碼編寫規範,單從執行速度上來說,直接訪問更快。

for循環優化

let arr = Array(100).fill('foo')
// 每次循環都要獲取數組長度
for (let i = 0; i < arr.length; i++) {
    console.log(i)
}
// 緩存數組長度,
for (let i = 0, len = arr.length; i < len; i++) {
    console.log(i)
}

緩存數組長度for循環執行速度要更快,特別適合很是大或者很是複雜的數組遍歷。

選擇最優的循環方式

let arr = Array(100).fill('foo')
arr.forEach(function (item) {
    console.log(item)
})

for (let i = 0, len = arr.length; i < len; i++) {
    console.log(i)
}

for (let i in arr) {
    console.log(arr[i])
}

經過jsperf工具發現,forEach的執行速度最快,所以在不影響功能的前提下,儘可能使用forEach可加快代碼的執行速度。

節點添加優化

在日常的js代碼編寫過程當中,經常伴有Dom節點的添加,因爲Dom節點添加操做經常伴有迴流和重繪,這兩個操做比較耗時,可使用文檔碎片優化這種耗時操做。

for (let i = 0; i < 10; i++) {
    let p = document.createElement('p')
    document.body.append(p)
}

let fraEls = document.createDocumentFragment()
for (let i = 0; i < 10; i++) {
    let p = document.createElement('p')
    fraEls.append(p)
}
document.body.append(fraEls)
相關文章
相關標籤/搜索