前端性能優化

AJax 優化

  • 緩存 Ajax
  • 請求儘量使用GET, 僅取決於cookie數量

Cookie 優化

  • 減少Cookie的大小
  • 使用無Cookie的域來存放靜態資源(可以利用CDN)

DOM 優化

優化節點修改(使用cloneNode在外部更新節點後在通過replace與原始節點互換)

var orig = document.getElementById('container');
var clone = orig.cloneNode(true);
var list = ['foo', 'bar', 'baz'];
var content;
for (var i = 0; i < list.length; i++) {
  content = document.createTextNode(list[i]);
  clone.appendChild(content);
}
orig.parentNode.replaceChild(clone, orig);

優化節點添加(創建DocumentFragment, 在其中插入節點後再添加到頁面)

createSafeFragment(document) {
  var list = nodeNames.split( "|" ),
  safeFrag = document.createDocumentFragment();

  if (safeFrag.createElement) {
    while (list.length) {
      safeFrag.createElement(
        list.pop();
      );
    };
  };
  return safeFrag;
};

優化CSS樣式轉換(儘量採用觸發reflow次數少的方式, 使用直接設置元素的className來代替逐條更改元素樣式)

// Not Recommended
element.style.fontWeight = 'bold' ;
element.style.marginLeft= '30px' ;
element.style.marginRight = '30px' ;
// Recommended
element.className = 'selectedAnchor' ;

減少DOM元素數量

document.getElementsByTagName( '*' ).length <= 1000

DOM操作優化

DOM操作性能原因

  • DOM元素過多導致元素定位緩慢。
  • 大量的DOM接口調用。
  • DOM操作觸發頻繁的 reflow(layout)(計算頁面元素的幾何信息)和 repaint(繪製頁面元素)
  • layout發生在repaint之前,所以layout相對來說會造成更多性能損耗。
  • 對DOM進行操作會導致瀏覽器執行迴流reflow。

優化DOM操作

  • JAVASCRIPT執行時間是很短的。
  • 最小化DOM訪問次數,儘可能在js端執行。
  • 如果需要多次訪問某個DOM節點,請使用局部變量存儲對它的引用
  • 謹慎處理HTML集合(HTML集合實時連繫底層文檔),把集合的長度緩存到一個變量中,並在迭代中使用它,如果需要經常操作集合,建議把它拷貝到一個數組中
  • 如果可能的話,使用速度更快的API,比如querySelectorAllfirstElementChild
  • 要留意重繪和重排。
  • 批量修改樣式時,離線操作DOM樹。
  • 使用緩存,並減少訪問佈局的次數。
  • 動畫中使用絕對定位,使用拖放代理
  • 使用事件委託來減少事件處理器的數量

優化DOM交互

最小化現場更新
多使用innerHTML替代createElement()appendChild():

reflow迴流

發生場景

  • 改變窗體大小。
  • 更改字體。
  • 添加移除stylesheet塊。
  • 內容改變哪怕是輸入框輸入文字。
  • CSS虛類被觸發如 :hover。
  • 更改元素的className。
  • 當對DOM節點執行新增或者刪除操作或內容更改時。
  • 動態設置一個style樣式時(比如element.style.width="10px")。
  • 當獲取一個必須經過計算的尺寸值時,比如訪問offsetWidth、clientHeight或者其他需要經過計算的CSS值。

解決關鍵: 限制DOM操作所引發的迴流

  • 在對當前DOM進行操作之前,儘可能多的做一些準備工作,保證N次創建,1次寫入。
  • 在對DOM操作之前,把要操作的元素,先從當前DOM結構中刪除:
    • 通過removeChild()或者replaceChild()實現真正意義上的刪除。
    • 設置該元素的display樣式爲“none”。
  • 每次修改元素的style屬性都會觸發迴流操作。element.style.backgroundColor = "blue";
    • 使用更改className的方式替換style.xxx=xxx的方式。
    • 使用style.cssText = '';一次寫入樣式。
    • 避免設置過多的行內樣式。
    • 添加的結構外元素儘量設置它們的位置爲fixedabsolute
    • 避免使用表格來佈局。
    • 避免在CSS中使用JavaScript expressions(IE only)
  • 將獲取的DOM數據緩存起來。這種方法,對獲取那些會觸發迴流操作的屬性(比如offsetWidth等)尤爲重要。
  • 當對HTMLCollection對象進行操作時,應該將訪問的次數儘可能的降至最低,最簡單的,你可以將length屬性緩存在一個本地變量中,這樣就能大幅度的提高循環的效率。

repaint重繪

  • 注意JavaScript代碼優化, 減少重繪。
  • 使用HTML5和CSS3的一些新特性。
  • 避免在HTML裏面縮放圖片。
  • 避免使用插件。
  • 確保使用正確的字體大小。

HTML 優化

  • 插入HTML。(找一個容器元素,並使用innerHTML來將HTML代碼插入到頁面中。)
  • 避免空的src和href
    • 當link標籤的href屬性爲空、script標籤的src屬性爲空的時候,瀏覽器渲染的時候會把當前頁面的URL作爲它們的屬性值,從而把頁面的內容加載進來作爲它們的值。
  • 爲文件頭指定Expires
  • 重構HTML,把重要內容的優先級提高
  • Post-load(次要加載)不是必須的資源。
  • 利用預加載優化資源。
  • 合理架構,使DOM結構儘量簡單。
  • 利用LocalStorage合理緩存資源。
  • 儘量避免CSS表達式和濾鏡
  • 嘗試使用defer方式加載Js腳本
  • 新特性:will-change,把即將發生的改變預先告訴瀏覽器。
  • 新特性Beacon,不堵塞隊列的異步數據發送。
  • 儘量多地緩存文件。
  • 使用HTML5 Web Workers來允許多線程工作
  • 爲不同的Viewports設置不同大小的Content
  • 正確設置可Tap的目標的大小。
  • 使用響應式圖片
  • 支持新接口協議(如HTTP2)。
  • 未來的緩存離線機制Service Workers
  • 未來的資源優化Resource Hints(preconnect, preload, 和prerender)
  • 使用Server-sent Events
  • 設置一個Meta Viewport
  • 避免跨域
    • 同域:注意避免反斜槓 “/” 的跳轉;
    • 跨域:使用Alias或者mod_rewirte建立CNAME(保存域名與域名之間關係的DNS記錄)

動畫優化

CSS 優化

慎重選擇高消耗的樣式

高消耗屬性在繪製前需要瀏覽器進行大量計算: box-shadows border-radius transparency transforms CSS filters(性能殺手)

避免過分重排

當發生重排的時候,瀏覽器需要重新計算佈局位置與大小,更多詳情

常見的重排元素: width height padding margin display border-width position top left right bottom font-size float text-align overflow-y font-weight overflow font-family line-height vertical-align clear white-space min-height

正確使用 Display 的屬性

Display 屬性會影響頁面的渲染,請合理使用。

  • display: inline後不應該再使用 width height margin padding 以及 float
  • display: inline-block 後不應該再使用 float
  • display: block 後不應該再使用 vertical-align
  • display: table-* 後不應該再使用 margin 或者 float

不濫用 Float

Float在渲染時計算量比較大,儘量減少使用。

動畫性能優化

動畫的基本概念:

  • 幀:在動畫過程中,每一幅靜止畫面即爲一“幀”;
  • 幀率:即每秒鐘播放的靜止畫面的數量,單位是fps(Frame per second);
  • 幀時長:即每一幅靜止畫面的停留時間,單位一般是ms(毫秒);
  • 跳幀(掉幀/丟幀):在幀率固定的動畫中,某一幀的時長遠高於平均幀時長,導致其後續數幀被擠壓而丟失的現象。

一般瀏覽器的渲染刷新頻率是 60 fps,所以在網頁當中,幀率如果達到 50-60 fps 的動畫將會相當流暢,讓人感到舒適。

  • 如果使用基於 javaScript 的動畫,儘量使用 requestAnimationFrame. 避免使用 setTimeoutsetInterval.
  • 避免通過類似 jQuery animate()-style 改變每幀的樣式,使用 CSS 聲明動畫會得到更好的瀏覽器優化。
  • 使用 translate 取代 absolute 定位就會得到更好的 fps,動畫會更順滑。
  • 動畫效果在缺少硬件加速支持的情況下反應緩慢,例如手機客戶端。
  • 特效只在確實能夠改善用戶體驗時才使用
  • 至少給用戶一個可以禁用動畫效果的選項
  • 設置動畫元素爲 position:absolute; 或 position:fixed(只需要repaint),而position: staticposition: relative元素應用動畫效果會造成頻繁的reflow
  • 使用一個timer完成多個元素的動畫
  • 使用一個timer完成多個對象的動畫效果
  • 以腳本爲基礎的動畫, 由瀏覽器控制動畫的更新頻率
  • 避免使用 jQuery 實現動畫
    • 禁止使用 slideUp/Down() fadeIn/fadeOut() 等方法;
    • 儘量不使用 animate() 方法;

高性能動畫

多利用硬件能力,如通過 3D 變形開啓 GPU 加速(3D 變形會消耗更多的內存和功耗)

一般在 Chrome 中,3D或透視變換(perspective transformCSS屬性和對 opacity 進行 CSS 動畫會創建新的圖層,在硬件加速渲染通道的優化下,GPU 完成 3D 變形等操作後,將圖層進行復合操作(Compesite Layers),從而避免觸發瀏覽器大面積重繪和重排。

使用 translate3d 右移 500px 的動畫流暢度要明顯優於直接使用 left

.ball-1 {
  transition: -webkit-transform .5s ease;
  -webkit-transform: translate3d(0, 0, 0);
}
.ball-1.slidein{
  -webkit-transform: translate3d(500px, 0, 0);
}
.ball-2 {
  transition: left .5s ease; left:0;
}
.ball-2.slidein {
  left:500px;
}

提升 CSS 選擇器性能

CSS 選擇器對性能的影響源於瀏覽器匹配選擇器和文檔元素時所消耗的時間,所以優化選擇器的原則是應儘量避免使用消耗更多匹配時間的選擇器。CSS 選擇器匹配的機制, 如子選擇器規則:

#header > a {font-weight:blod;}

CSS 選擇器是從右到左進行規則匹配。
最右邊選擇符爲關鍵選擇器。——更多詳情

  • 避免使用通用選擇器
  • 避免使用標籤或 class 選擇器限制 id 選擇器
  • 避免使用標籤限制 class 選擇器
  • 避免使用多層標籤選擇器。使用 class 選擇器替換,減少css查找
  • 避免使用子選擇器
  • 使用繼承
/* Not recommended */
#bookmarkMenuItem > .menu-left { list-style-image: url(blah) }
/* Recommended */
#bookmarkMenuItem { list-style-image: url(blah) }

JS 載入優化

  • 使用加快JS載入速度的工具, 使JS並行載入
  • 使用CDN
  • 網頁尾部載入JS, 頭部載入必須異步載入
  • 跟蹤代碼等跟頁面關係不大的代碼異步載入或延遲載入
  • 將JS打包成PNG文件, 之後進行拆包, 只要使用畫布API的getImageData()。可以在不縮小數據的情況下,多壓縮35%左右。而且是無損壓縮,對比較龐大的腳本來說,在圖片指向畫布、讀取像素的過程中,會有一段讀取時間。
  • 設置Cache-ControlExpires

    (function() {
    var script,
        scripts = document.getElementsByTagName('script')[0];
    function load(url) {
      script = document.createElement('script');
      script.async = true;
      script.src = url;
      scripts.parentNode.insertBefore(script, scripts);
    }
    
    load('//apis.google.com/js/plusone.js');
    load('//platform.twitter.com/widgets.js');
    load('//s.widgetsite.com/widget.js');
    }());

代碼壓縮

  • 使用代碼壓縮工具精簡混淆壓縮代碼
  • 啓用Gzip壓縮, 比 deflate 更高效
    • 客戶端在請求Accept-Encoding中聲明可以支持Gzip
    • 服務器將請求文檔壓縮,並在Content-Encoding中聲明該回復爲Gzip格式。
    • 客戶端收到之後按照Gzip解壓縮。

Javascript優化

優化原則

  • 只需要爲IE6(未打補丁的JScript 5.6或更早版本)做優化
  • 解釋執行的情況下,在所有操作中,函數調用的效率是較低的。此外,過深的prototype繼承鏈或者多級引用也會降低效率。
  • JS優化總是出現在大規模循環的地方
  • 儘量避免過多的引用層級和不必要的多次方法調用
  • arguments優化:
    • 如果一個可變參數的簡單函數成爲性能瓶頸的時候,可以將其內部做一些改變,不要訪問arguments,而是通過對參數的顯式判斷來處理
    • ES6可以使用...args代替隱式的arguments
// 顯式判斷處理優化
function sum() {  
  var r = 0;  
  for (var i = 0; i < arguments.length; i++) {  
    r += arguments[i];  
  }  
  return r;  
}
// 參數較少時優化
function sum() {  
  switch (arguments.length) {  
    case 1: return arguments[0];  
    case 2: return arguments[0] + arguments[1];  
    case 3: return arguments[0] + arguments[1] + arguments[2];  
    case 4: return arguments[0] + arguments[1] + arguments[2] + arguments[3];  
    default:  
      var r = 0;  
      for (var i = 0; i < arguments.length; i++) {  
        r += arguments[i];  
      }  
      return r;  
  }  
}
// 顯式調用優化 (速度至少快1倍)
function sum(a, b, c, d, e, f, g) {  
  var r = a ? b ? c ? d ? e ? f ? a + b + c + d + e + f : a + b + c + d + e : a + b + c + d : a + b + c : a + b : a : 0;  
  if (g === undefined) return r;  
  for (var i = 6; i < arguments.length; i++) {  
    r += arguments[i];  
  }  
  return r;  
}

常規優化

  • 定時器
    • 使用setInterval取代多次setTimeout, 多次執行相同的代碼
    • setTimeout() setInterval()傳遞方法取代方法字符串
      • setTimeout(test, 1); 取代 setTimeout('test()', 1);
  • 使用原始操作代替方法調用
    • var min = a<b?a:b; 取代 var min = Math.min(a, b);
  • 避免雙重解釋(一般在使用eval函數new Function構造函數setTimeout傳一個字符串時等情況下會遇到)
    • eval("alert('hello world');");
    • var sayHi = new Function("alert('hello world');");
    • setTimeout("alert('hello world');", 100);
  • 使用原生方法
  • 最小化語句數目
    • 多個變量聲明
    • 插入迭代值 var name = values[i++];
  • 使用數組和對象字面量, 避免使用構造函數 Array() Object()
  • 避免使用屬性訪問方法, 直接訪問屬性
  • 減少使用元素位置操作
    • 一般瀏覽器都會使用增量reflow的方式將需要reflow的操作積累到一定程度然後再一起觸發,但是如果腳本中要獲取以下屬性,那麼 積累的reflow將會馬上執行,用來得到準確的位置信息。offsetLeft offsetTop offsetHeight offsetWidth scrollTop/Left/Width/Height clientTop/Left/Width/Height getComputedStyle()
  • 嚴格避免使用eval(): 因爲eval()會導致代碼髒, 消耗大量時間, 無法被壓縮工具壓縮, 容易造成安全漏洞
  • 避免使用with: 儘可能地少用with語句,因爲它會增加with語句以外的數據的訪問代價。

字符串優化

  • 字符串替換、查找等操作,使用正則表達式(用C寫的)
  • 字符串拼接使用+=(如果考慮IE6,則使用Array.join("")), 編譯器已經優化

變量優化

  • 通過 包裝函數來處理 全局變量
    • window對象成員, 會一直存在老生代堆內存中, 直到頁面被關閉
    • 多人協作易產生混淆
    • 作用域鏈中易被幹擾
    • 全局變量需要搜索更長的作用域鏈, 生命週期長, 不利於內存釋放
  • 儘量使用 局部變量
    • 局部變量放在函數的棧裏, 訪問速度比全局變量更快
  • 手動解除變量引用 data = null;
  • 變量查找優化
    • 變量聲明帶上varES6中爲let
    • 通過一條語句聲明變量, 變量用,分隔
    • 緩存重複使用的全局變量(重複調用方法, 也可以用局部緩存提速)
  • 善用回調(後續傳遞風格(Continuation Passing Style, CPS)的技術)
    • 如果傳入的參數是基礎類型(如字符串、數值),回調函數中傳入的形參就會是複製值,業務代碼使用完畢以後,更容易被回收。
    • 通過回調,我們除了可以完成同步的請求外,還可以用在異步編程中,這也就是現在非常流行的一種編寫風格。
    • 回調函數自身通常也是臨時的匿名函數,一旦請求函數執行完畢,回調函數自身的引用就會被解除,自身也得到回收。
      function getData(callback) {
      var data = 'some big data';
      callback(null, data);
      }
      getData(function(err, data) {
      console.log(data);
      });

運算符優化

  • 儘量使用 += -= *= \= 運算符, 而不是直接賦值
  • 儘量使用位運算

邏輯判斷優化

  • swich語句來優化多個if...else語句
  • || && 來優化多個if語句

類型轉換優化

  • 數字=>字符串 ""+num > String() > .toString() > new String()
  • 浮點數=>整型 Math.floor() 或 Math.round()
  • 字符串=>數字 parseInt(str,10)

對象優化

  • 對象創建
    • 儘量減少不必要的創建(JS的垃圾回收調度算法, 會隨着對象個數增加, 性能會開始嚴重下降(複雜度O(n^2)))
    • 儘量採用緩存緩存複雜的Javascript對象
    • 儘量使用JSON創建對象, 而不是var obj = new Object()
  • 對象查找
    • 避免對象的嵌套查詢(因爲JAVASCRIPT的解釋性,a.b.c.d.e嵌套對象,需要進行4次查詢,嵌套的對象成員會明顯影響性能。)
    • 如果出現嵌套對象, 利用局部變量, 把它緩存, 放入臨時的地方查詢
  • 對象屬性
    • 先從本地變量表找到對象。
    • 然後遍歷屬性。
    • 如果在當前對象的屬性列表裏沒找到。
    • 繼續從prototype向上查找。
    • 且不能直接索引,只能遍歷。

數組優化

  • 當需要使用數組時,可使用JSON格式的語法
  • 如果需要遍歷數組,應該先緩存數組長度,將數組長度放入局部變量中,避免多次查詢數組長度。

循環優化

  • 循環性能 do-while > for(;;) ≈ while() > for(in)
    • for(;;)
      • 推薦使用for循環,如果循環變量遞增或遞減,不要單獨對循環變量賦值,而應該使用嵌套的++或–-運算符。
      • 代碼的可讀性對於for循環的優化。
      • -=1
      • 從大到小進行循環(代碼可讀性降低)
      • IE6下, divs.length在每次循環執行中都會計算一下長度
    • for(in)
      • for(in)內部實現是構造一個所有元素的列表,包括array繼承的屬性,然後再開始循環,並且需要查詢hasOwnProperty
  • 避免不必要的屬性查找
    • 訪問變量數組O(1)操作
    • 訪問對象上的屬性是一個O(n)操作。(局部變量緩存)
  • 優化循環
    • 減值迭代更有效
    • 簡化終止條件
    • 簡化循環體(儘可能移除循環的密集計算)
    • 使用後測試循環(do-while是後測試循環, 可以避免最初終止條件的計算)
for(var i = 0; i < values.length; i++) { 
  process(values[i]); 
} 
// 優化1:簡化終止條件 
for(var i = 0, len = values.length; i < len; i++) { 
  process(values[i]); 
} 
// 優化2:使用後測試循環(注意:使用後測試循環需要確保要處理的值至少有一個) 
var i values.length - 1; 
if(i > -1) { 
  do { 
    process(values[i]); 
  } while(--i >= 0); 
}
  • 展開循環
    • 當循環的次數確定時,消除循環並使用多次函數調用往往更快。
    • 當循環的次數不確定時,可以使用Duff裝置來優化。(Duff裝置的基本概念是通過計算迭代的次數是否爲8的倍數將一個循環展開爲一系列語句。)
// Jeff Greenberg for JS implementation of Duff's Device 
// 如上展開循環可以提升大數據集的處理速度。
// 假設:
values.length > 0 
function process(v) { 
  alert(v); 
} 
var values = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17]; 
var iterations = Math.ceil(values.length / 8); 
var startAt = values.length % 8; var i = 0; 
do { 
  switch(startAt) { 
    case 0 : 
      process(values[i++]); 
    case 7 : 
      process(values[i++]); 
    case 6 : 
      process(values[i++]); 
    case 5 : 
      process(values[i++]); 
    case 4 : 
      process(values[i++]); 
    case 3 : 
      process(values[i++]); 
    case 2 : 
      process(values[i++]); 
    case 1 : 
      process(values[i++]); 
  }
  startAt = 0; 
}
while(--iterations > 0); 
// 接下來給出更快的Duff裝置技術,
// 將do-while循環分成2個單獨的循環。(注:這種方法幾乎比原始的Duff裝置實現快上40%。) 
function process(v) {
  alert(v);
}
var values = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17]; 
var iterations = Math.floor(values.length / 8); 
var leftover = values.length % 8; 
var i = 0; 
if(leftover > 0) {
  do {
    process(values[i++]);
  }while(--leftover > 0); 
}
do {
  process(values[i++]); 
  process(values[i++]); 
  process(values[i++]); 
  process(values[i++]); 
  process(values[i++]); 
  process(values[i++]); 
  process(values[i++]); 
  process(values[i++]); 
}while(--iterations > 0);
// 針對大數據集使用展開循環可以節省很多時間,但對於小數據集,額外的開銷則可能得不償失。
  • 避免遍歷大量元素(避免對全局DOM元素進行遍歷,如果parent已知可以指定parent在特定範圍查詢。)

    var elements = document.getElementsByTagName( '*' );
    for (i = 0; i < elements.length; i++) {
    if (elements[i].hasAttribute( 'selected' )) {}
    }
    // 如果已知元素存在於一個較小的範圍內,
    var elements = document.getElementById( 'canvas' ).getElementsByTagName ( '*' );
    for (i = 0; i < elements.length; i++) {
    if (elements[i].hasAttribute( 'selected' )) {}
    }
  • 避免在循環中使用try_catch

    • try-catch-finally語句在catch語句被執行的過程中會動態構造變量插入到當前域中,對性能有一定影響。
    • 如果需要異常處理機制,可以將其放在循環外層使用。
// Not recommended
for ( var i = 0; i < 200; i++) {
  try {} catch (e) {}
}
// Recommended
try {
  for ( var i = 0; i < 200; i++) {}
} catch (e) {}

原型優化

JAVASCRIPT中原型的概念,構造函數都有一個prototype屬性,指向另一個對象。這個對象的所有屬性和方法,都會被構造函數的實例繼承

通過原型優化方法定義

  • 如果一個方法類型將被頻繁構造,通過方法原型從外面定義附加方法,從而避免方法的重複定義。
  • 可以通過外部原型的構造方式初始化值類型的變量定義。(這裏強調值類型的原因是,引用類型如果在原型中定義,一個實例對引用類型的更改會影響到其他實例。)

可以把那些不變的屬性和方法,直接定義在prototype對象上

  • 可以通過對象實例訪問保存在原型中的值。
  • 不能通過對象實例重寫原型中的值。
  • 在實例中添加一個與實例原型同名屬性,那該屬性就會屏蔽原型中的屬性。
  • 通過delete操作符可以刪除實例中的屬性。

作用域鏈和閉包優化

作用域

作用域(scope) JAVASCRIPT編程中一個重要的運行機制,在JAVASCRIPT同步和異步編程以及JAVASCRIPT內存管理中起着至關重要的作用。
在JAVASCRIPT中,能形成作用域的有如下幾點

  • 函數的調用
  • with語句 with會創建自已的作用域,因此會增加其中執行代碼的作用域的長度。
  • 全局作用域。
var foo = function() {
  var local = {};
};
foo();
console.log(local); //=> undefined

var bar = function() {
  local = {};
};
bar();
console.log(local); //=> {}

/**這裏我們定義了foo()函數和bar()函數,他們的意圖都是爲了定義一個名爲local的變量。在foo()函數中,我們使用var語句來聲明定義了一個local變量,而因爲函數體內部會形成一個作用域,所以這個變量便被定義到該作用域中。而且foo()函數體內並沒有做任何作用域延伸的處理,所以在該函數執行完畢後,這個local變量也隨之被銷燬。而在外層作用域中則無法訪問到該變量。而在bar()函數內,local變量並沒有使用var語句進行聲明,取而代之的是直接把local作爲全局變量來定義。故外層作用域可以訪問到這個變量。**/

local = {};
// 這裏的定義等效於
global.local = {};
作用域鏈

在JAVASCRIPT編程中,會遇到多層函數嵌套的場景,這就是典型的作用域鏈的表示。

function foo() {
  var val = 'hello';
  function bar() {
    function baz() {
      global.val = 'world;'
    };
    baz();
    console.log(val); //=> hello
  };
  bar();
};
foo();

/**在`JAVASCRIPT`中,變量標識符的查找是從當前作用域開始向外查找,直到全局作用域爲止。所以`JAVASCRIPT`代碼中對變量的訪問只能向外進行,而不能逆而行之。baz()函數的執行在全局作用域中定義了一個全局變量val。而在bar()函數中,對val這一標識符進行訪問時,按照從內到外的查找原則:在bar函數的作用域中沒有找到,便到上一層,即foo()函數的作用域中查找。然而,使大家產生疑惑的關鍵就在這裏:本次標識符訪問在foo()函數的作用域中找到了符合的變量,便不會繼續向外查找,故在baz()函數中定義的全局變量val並沒有在本次變量訪問中產生影響。**/

減少作用域鏈上的查找次數
JAVASCRIPT代碼在執行的時候,如果需要訪問一個變量或者一個函數的時候,它需要遍歷當前執行環境的作用域鏈,而遍歷是從這個作用域鏈的前端一級一級的向後遍歷,直到全局執行環境。

/**效率低**/
for(var i = 0; i < 10000; i++){
    var but1 = document.getElementById("but1");
}
/**效率高**/
/**避免全局查找**/
var doc = document;
for(var i = 0; i < 10000; i++){
    var but1 = doc.getElementById("but1");
}
/**上面代碼中,第二種情況是先把全局對象的變量放到函數裏面先保存下來,然後直接訪問這個變量,而第一種情況是每次都遍歷作用域鏈,直到全局環境,我們看到第二種情況實際上只遍歷了一次,而第一種情況卻是每次都遍歷了,而且這種差別在多級作用域鏈和多個全局變量的情況下還會表現的非常明顯。在作用域鏈查找的次數是`O(n)`。通過創建一個指向`document`的局部變量,就可以通過限制一次全局查找來改進這個函數的性能。**/
閉包

JAVASCRIPT中的標識符查找遵循從內到外的原則。

function foo() {
  var local = 'Hello';
  return function() {
    return local;
  };
}
var bar = foo();
console.log(bar()); //=> Hello

/**這裏所展示的讓外層作用域訪問內層作用域的技術便是閉包(Closure)。得益於高階函數的應用,使foo()函數的作用域得到`延伸`。foo()函數返回了一個匿名函數,該函數存在於foo()函數的作用域內,所以可以訪問到foo()函數作用域內的local變量,並保存其引用。而因這個函數直接返回了local變量,所以在外層作用域中便可直接執行bar()函數以獲得local變量。**/

閉包是JAVASCRIPT的高級特性,因爲把帶有​​內部變量引用的函數帶出了函數外部,所以該作用域內的變量在函數執行完畢後的並不一定會被銷燬,直到內部變量的引用被全部解除。所以閉包的應用很容易造成內存無法釋放的情況。

良好的閉包管理

循環事件綁定、私有屬性、含參回調等一定要使用閉包時,並謹慎對待其中的細節。
循環綁定事件,我們假設一個場景:有六個按鈕,分別對應六種事件,當用戶點擊按鈕時,在指定的地方輸出相應的事件。

var btns = document.querySelectorAll('.btn'); // 6 elements
var output = document.querySelector('#output');
var events = [1, 2, 3, 4, 5, 6];
// Case 1
for (var i = 0; i < btns.length; i++) {
  btns[i].onclick = function(evt) {
    output.innerText += 'Clicked ' + events[i];
  };
}
/**這裏第一個解決方案顯然是典型的循環綁定事件錯誤**/
// Case 2
for (var i = 0; i < btns.length; i++) {
  btns[i].onclick = (function(index) {
    return function(evt) {
      output.innerText += 'Clicked ' + events[index];
    };
  })(i);
}
/**第二個方案傳入的參數是當前循環下標,而後者是直接傳入相應的事件對象。事實上,後者更適合在大量數據應用的時候,因爲在JavaScript的函數式編程中,函數調用時傳入的參數是基本類型對象,那麼在函數體內得到的形參會是一個複製值,這樣這個值就被當作一個局部變量定義在函數體的作用域內,在完成事件綁定之後就可以對events變量進行手工解除引用,以減輕外層作用域中的內存佔用了。而且當某個元素被刪除時,相應的事件監聽函數、事件對象、閉包函數也隨之被銷燬回收。**/
// Case 3
for (var i = 0; i < btns.length; i++) {
  btns[i].onclick = (function(event) {
    return function(evt) {
      output.innerText += 'Clicked ' + event;
    };
  })(events[i]);
}

避開閉包陷阱
閉包是個強大的工具,但同時也是性能問題的主要誘因之一。不合理的使用閉包會導致內存泄漏。
閉包的性能不如使用內部方法,更不如重用外部方法。
由於IE 9瀏覽器的DOM節點作爲COM對象來實現,COM的內存管理是通過引用計數的方式,引用計數有個難題就是循環引用,一旦DOM引用了閉包(例如event handler),閉包的上層元素又引用了這個DOM,就會造成循環引用從而導致內存泄漏。

善用函數, 避免閉包陷阱

  • 使用一個匿名函數在代碼的最外層進行包裹。(function() { // 主業務代碼 })();
  • 甚至更高級一點(傳入參數):
(function(win, doc, $, undefined) {
  // 主業務代碼
})(window, document, jQuery);
  • 甚至連如RequireJS, SeaJS, OzJS 等前端模塊化加載解決方案,都是採用類似的形式:
/**RequireJS**/
define(['jquery'], function($) {
  // 主業務代碼
});
/**SeaJS**/
define('m​​odule', ['dep', 'underscore'], function($, _) {
  // 主業務代碼
});

被定義在全局作用域的對象,可能是會一直存活到進程退出的,如果是一個很大的對象,那就麻煩了。

比如有的人喜歡在JavaScript中做模版渲染:

<?php
  $db = mysqli_connect(server, user, password, 'myapp');
  $topics = mysqli_query($db, "SELECT * FROM topics;");
?>
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>你是猴子請來的逗比麼?</title>
</head>
<body>
  <ul id="topics"></ul>
  <script type="text/tmpl" id="topic-tmpl">
    <li class="topic">
      <h1><%=title%></h1>
      <p><%=content%></p>
    </li>
  </script>
  <script type="text/javascript">
    var data = <?php echo json_encode($topics); ?>;
    var topicTmpl = document.querySelector('#topic-tmpl').innerHTML;
    var render = function(tmlp, view) {
      var complied = tmlp
        .replace(/\n/g, '\\n')
        .replace(/<%=([\s\S]+?)%>/g, function(match, code) {
          return '" + escape(' + code + ') + "';
        });

      complied = [
        'var res = "";',
        'with (view || {}) {',
          'res = "' + complied + '";',
        '}',
        'return res;'
      ].join('\n');

      var fn = new Function('view', complied);
      return fn(view);
    };

    var topics = document.querySelector('#topics');
    function init()
      data.forEach(function(topic) {
        topics.innerHTML += render(topicTmpl, topic);
      });
    }
    init();
  </script>
</body>
</html>

在從數據庫中獲取到的數據的量是非常大的話,前端完成模板渲染以後,data變量便被閒置在一邊。可因爲這個變量是被定義在全局作用域中的,所以JAVASCRIPT引擎不會將其回收銷燬。如此該變量就會一直存在於老生代堆內存中,直到頁面被關閉。可是 如果我們作出一些很簡單的修改,在邏輯代碼外包裝一層函數,這樣效果就大不同了。當UI渲染完成之後,代碼對data的引用也就隨之解除,而在最外層函數執行完畢時,JAVASCRIPT引擎就開始對其中的對象進行檢查,data也就可以隨之被回收

事件優化

  • 當存在多個元素需要註冊事件時,在每個元素上綁定事件本身就會對性能有一定損耗。
  • 由於DOM Level2事件模型中所有事件默認會傳播到上層文檔對象,可以藉助這個機制在上層元素註冊一個統一事件對不同子元素進行相應處理。
    使用事件代理
    // 捕獲型事件先發生。
    // 兩種事件流會觸發DOM中的所有對象,從document對象開始,也在document對象結束。
    <ul id="parent-list">
    <li id="post-1">Item 1</li>
    <li id="post-2">Item 2</li>
    <li id="post-3">Item 3</li>
    <li id="post-4">Item 4</li>
    <li id="post-5">Item 5</li>
    <li id="post-6">Item 6</li>
    </ul>
    // Get the element, add a click listener...
    document.getElementById("parent-list").addEventListener("click",function(e) {
    // e.target is the clicked element!
    // If it was a list item
    if(e.target && e.target.nodeName == "LI") {
      // List item found!  Output the ID!
      console.log("List item ",e.target.id.replace("post-")," was clicked!");
    }
    });

性能測試工具

js性能優化和內存泄露問題及檢測分析工具

  • 性能優化ajax工具diviefirebug
  • web性能分析工具YSlow
    • performance性能評估打分,右擊箭頭可看到改進建議。
    • stats緩存狀態分析,傳輸內容分析。
    • components所有加載內容分析,可以查看傳輸速度,找出頁面訪問慢的瓶頸。
    • tools可以查看js和css,並打印頁面評估報告。
  • 內存泄露檢測工具sIEve
    • sIEve是基於IE的內存泄露檢測工具,需要下載運行,可以查看dom孤立節點和內存泄露及內存使用情況。
    • 列出當前頁面內所有dom節點的基本信息(html id style 等)
    • 頁面內所有dom節點的高級信息 (內存佔用,數量,節點的引用)
    • 可以查找出頁面中的孤立節點
    • 可以查找出頁面中的循環引用
    • 可以查找出頁面中產生內存泄露的節點
  • 內存泄露提示工具leak monitor
    • leak monitor在安裝後,當離開一個頁面時,比如關閉窗口,如果頁面有內存泄露,會彈出一個文本框進行即時提示。
  • 代碼壓縮工具
    • YUI壓縮工具
    • Dean Edwards Packer
    • JSMin
    • Uglify
  • Blink/Webkit瀏覽器中(ChromeSafariOpera),我們可以藉助其中的Developer ToolsProfiles工具來 對我們的程序進行內存檢查

Node.js中的內存檢查

  • OneApm 或 alinode 進行線上監控
  • Node.js中,我們可以使用node-heapdumpnode-memwatch模塊進​​行內存檢查。
var heapdump = require('heapdump');
var fs = require('fs');
var path = require('path');
fs.writeFileSync(path.join(__dirname, 'app.pid'), process.pid);

在業務代碼中引入node-heapdump之後,我們需要在某個運行時期,向Node.js進程發送SIGUSR2信號,讓node-heapdump抓拍一份堆內存的快照。
$ kill -USR2 (cat app.pid)
這樣在文件目錄下會有一個以heapdump-<sec>.<usec>.heapsnapshot格式命名的快照文件,我們可以使用瀏覽器的Developer Tools中的Profiles工具將其打開,並進行檢查。

分析瀏覽器提供的Waterfall圖片來思考優化入口。

新的測試手段Navigation Resource User timing

Developer Tools - Profiles

JITGC優化(內存優化)

  • number+numberstring+string 等等可以使用JIT優化,但特殊情況,如:number+undefined無法被優化
  • list很大時, JIT無法優化

Type-specializing JIT優化

  • 使用代價
    • 前置的掃描類型
    • 編譯優化。
  • 使用場景
    • 熱點代碼。
    • 通過啓發式算法估算出來的有價值的代碼。
  • 當變量類型 發生變化時,引擎有2種處理方式:
    • 少量變更,重編譯,再執行。
    • 大量變更,交給JIT執行。
  • 數組,object properties, 閉包變量 不在優化範疇之列。

JavaScript的內存回收機制

在V8引擎中所有的JAVASCRIPT對象都是通過堆來進行內存分配的。當我們在代碼中聲明變量並賦值時,V8引擎就會在堆內存中分配一部分給這個變量。如果已申請的內存不足以存儲這個變量時,V8引擎就會繼續申請內存,直到堆的大小達到了V8引擎的內存上限爲止(默認情況下,V8引擎的堆內存的大小上限在64位系統中爲1464MB,在32位系統中則爲732MB

V8引擎對堆內存中的JAVASCRIPT對象進行分代管理。

  • 新生代 即存活週期較短的JAVASCRIPT對象,如臨時變量、字符串等
  • 老生代 則爲經過多次垃圾回收仍然存活,存活週期較長的對象,如主控制器、服務器對象等。

垃圾回收算法

  • Scavange算法:通過複製的方式進行內存空間管理,主要用於新生代的內存空間;
  • Mark-Sweep算法和Mark-Compact算法:通過標記來對堆內存進行整理和回收,主要用於老生代對象的檢查和回收。

回收對象

  • 當函數執行完畢時,在函數內部所聲明的對象不一定就會被銷燬。
  • 爲了保證垃圾回收的行爲不影響程序邏輯的運行,JAVASCRIPT引擎不會把正在使用的對象進行回收。所以判斷對象是否正在使用中的標準,就是是否仍然存在對該對象的 引用
  • 引用(Reference)是JAVASCRIPT編程中十分重要的一個機制。是指代碼對對象的訪問這一抽象關係
  • JAVASCRIPT的引用是可以進行轉移的,那麼就有可能出現某些引用被帶到了全局作用域,但事實上在業務邏輯裏已經不需要對其進行訪問了,這個時候就應該被回收,但是JAVASCRIPT引擎仍會認爲程序仍然需要它。
// 當代碼執行完畢時,對象val和bar()並沒有被回收釋放,
// JAVASCRIPT代碼中,每個變量作爲單獨一行而不做任何操作,
// JAVASCRIPT引擎都會認爲這是對對象的訪問行爲,存在了對對象的引用
var val = 'hello world';
function foo() {
  return function() {
    return val;
  };
}
global.bar = foo();

內存泄露及處理

給DOM對象添加的屬性是一個對象的引用。

var MyObject = {};
document.getElementByIdx_x('myDiv').myProp = MyObject;

解決方法:在window.onunload事件中寫上:

document.getElementByIdx_x('myDiv').myProp = null;

DOM對象與JS對象相互引用。

function Encapsulator(element) {
   this.elementReference = element;
   element.myProp = this;
}
new Encapsulator(document.getElementByIdx_x('myDiv'));

解決方法:在window.onunload事件中寫上:

document.getElementByIdx_x('myDiv').myProp = null;

給DOM對象用attachEvent綁定事件。

function doClick() {}
element.attachEvent("onclick", doClick);

解決方法:在onunload事件中寫上:

element.detachEvent('onclick', doClick);

從外到內執行appendChild。這時即使調用removeChild也無法釋放。

var parentDiv = document.createElement_x("div");
var childDiv = document.createElement_x("div");
document.body.appendChild(parentDiv);
parentDiv.appendChild(childDiv);

解決方法:從內到外執行appendChild:

var parentDiv =   document.createElement_x("div");
var childDiv = document.createElement_x("div");
parentDiv.appendChild(childDiv);
document.body.appendChild(parentDiv);

反覆重寫同一個屬性會造成內存大量佔用(但關閉IE後內存會被釋放)。

for(i = 0; i < 5000; i++) {
  hostElement.text = "asdfasdfasdf";
}
// 這種方式相當於定義了5000個屬性

解決方法:無, 避免這樣書寫代碼。
IE下閉包會引起跨頁面內存泄露。

內存不是緩存

  • 不要輕易將內存當作緩存使用。
  • 如果是很重要的資源,請不要直接放在內存中,或者制定過期機制,自動銷燬過期緩存。

CollectGarbage
CollectGarbage是IE的一個特有屬性,用於釋放內存的使用方法,將該變量或引用對象設置爲null或delete然後在進行釋放動作,
在做CollectGarbage前,要必需清楚的兩個必備條件:(引用)。

  • 一個對象在其生存的上下文環境之外,即會失效。
  • 一個全局的對象在沒有被執用(引用)的情況下,即會失效

服務端優化

  • 避免404
  • 刪除重複的JavaScriptCSS
    • 重複調用會增加額外的HTTP請求
    • 多次運算也會浪費時間(IE Firefox中不管腳本是否可緩存, 都存在重複運算的問題)
  • ETags配置Entity標籤, 可以有效減少Web應用負載
  • 權衡DNS查找次數
    • 減少主機名可以節省響應時間。但同時也會減少頁面中並行下載的數量(IE瀏覽器在同一時刻只能從同一域名下載兩個文件)
  • 通過Keep-alive機制減少TCP連接。
  • 通過CDN減少延時。
  • 平行處理請求(參考BigPipe)。
  • 通過合併文件或者Image Sprites減少HTTP請求。
  • 減少重定向( HTTP 301和40x/50x)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章