瘋狂

          STANDING ON THE SHOULDERS OF GIANTS
          posts - 481, comments - 486, trackbacks - 0, articles - 1
            BlogJava :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理

          高性能JavaScript(來自高性能JavaScript一書)

          Posted on 2011-10-09 11:56 瘋狂 閱讀(3038) 評論(0)  編輯  收藏 所屬分類: java性能web

          高性能JavaScript

          一下內容是轉載的,內容應該出自高性能JavaScript一書中,此書值得一讀。

          學習過程中寫的筆記,有誤請指正。

          性能并不是唯一的考慮因素,在對性能要求并非苛刻的環境中,性能也可讓位于:團隊編碼規范,個人編碼習慣,代碼可讀性,模塊可擴展性等因素。

          以下提到的對性能的優化,僅僅提供了從性能的角度去闡釋一些設計思路,但實際上,瀏覽器本身會逐步優化自身的性能問題,而我們那些提高性能的hack,可能會因為瀏覽器的版本更新,導致成為一種無用的hack,甚至讓性能更慢,所以不要無謂的使用一些hack,去優化一些執行次數很少的代碼,而降低代碼的可讀性,或增加代碼量,,一句話:如非必要,請勿hack。

          一 javascript加載和執行

          1 無論是外鏈還是內聯,script標簽都會阻塞頁面的渲染,所以script標簽的位置最好是</body>前
          2 減少http請求,合并多個script文件為一個,1個90k的文件比3個30k的文件載入速度要快
          3 如何不阻塞頁面,而載入一個腳本呢:
          1)給script標簽加defer屬性:<script src=”xxx” defer></script>,這樣不會使頁面執行過程阻塞,但并不是所有瀏覽器都支持這個defer屬性
          2)xmlHttpRequest來請求腳本內容并eval,雖然這樣可以靈活的控制腳本下載過程 及 何時執行,但不能跨域是個硬傷
          3)createElement(‘script’)然后append是個不錯的方案,無阻塞,有onload用onload,IE沒有就用onreadystatechange實現加載完成的事件,注意readyState屬性的值是complete或者loaded都要執行回調函數并清除onreadystatechange,因為這兩個狀態值不穩定。

          4 所以推薦的方式是第三種:在載入js-loader部分的少量代碼后,就用loader去加載其他js吧,注意一些市面上流行的庫如labjs實現了這些功能(廣告下:qwrap實現了依賴管理及動態加載)

          二 數據訪問
          數據存儲在:直接量,變量,數組元素,對象成員中。
          這又讓我想起了編譯型的語言,直接量存在于pe文件的.rdata段中,變量在調用棧上,數組元素和對象成員的訪問需要基址+偏移量來定位,多層對象嵌套需要多次計算基址+偏移量來定位。

          這些在javascript中依然沒有太大變化:

          1 直接量的訪問無疑是迅速的

          2 變量的訪問需要考慮javascript允許函數進行嵌套定義,也就形成了基于函數定義的作用域鏈,而變量的訪問,可能需要跨作用域來訪問,所以這里有一點性能損失,但先進的js引擎用空間換時間(猜測在子作用域中緩存了所有父層作用域鏈上變量的name和地址,所以不會進行上溯作用域鏈,直接執行hash定位即可,但一個函數中如果包含eval,with,catch塊的話,通過靜態代碼分析就沒辦法知道該函數中聲明了哪些變量,也就無法做到這個優化),,不過,從性能上來看,與我的實際測試,大家編碼的時候不需要注意這種性能考慮,按團隊編碼規范和個人編碼習慣來吧。

          3 對象成員的訪問要考慮上溯原型鏈,所以理論上來說訪問實例本身上的成員比訪問原型的成員速度要快。

          4 多層對象的嵌套要慢,但是在對性能要求并非很苛刻的環境中不用關心這些。

          三 dom編程

          1 dom的訪問和修改
          1) 標準dom方式(createElement,createTextNode,appendChild) 和 字符串拼接后設置innerHTML 之間的性能相差無幾,各個瀏覽器不同
          2) 節點克隆速度快一些,先用createElement創建好需要用到的元素類型后,以后在循環中調用元素的cloneNode來克隆
          3) getElementsByName,getElementsByClassName,getElementsByTagName以及.images,.links,.forms屬性返回的都是html集合,是個類數組,沒有數組的方法,但提供了length屬性和索引器,,HTML集合處于“實時狀態”,底層文檔對象更新時后自動更新javascript中的集合,訪問length時候也會去查詢,所以遍歷集合時候要緩存length來提高效率
          4) 遍歷集合前記錄length,循環體中避免對集合中某相同元素進行多次索引,一次索引到局部變量中,因為對html集合的索引性能很差,特別在某些老瀏覽器中
          5) 遍歷dom節點的話,綜合來說使用nextSibling性能會比childNodes快一點,如果只遍歷element的話,children被所有瀏覽器支持,所以盡量用children而不要用childNodes再自行篩選,其他的如childElementCount,firstElementChild,lastElementChild,nextElementSibling,previousElementSibling不被全面支持,注意做特性檢測及兼容處理
          6) 注意所使用的類庫是否支持原生的querySelectorAll優先規則,querySelectorAll返回NodeList而不會返回HTML集合,不存在實時文檔結構的性能問題

          2 重繪和重排

          改變dom節點的幾何屬性會引起重排(reflow),而后發生重繪(repaint)。

          由于重排需要產生大量的計算,所以瀏覽器一般會通過隊列化修改并批量執行來優化重排,獲取最新布局信息的操作會導致強制觸發隊列的執行,如獲取offsetTop,scrollTop,clientTop或調用getComputedStyle方法(currentStyle in IE)的時候,要返回諸如此類的最新的布局信息的時候,瀏覽器就會立即渲染隊列中的變化,觸發重排,然后返回正確的值。

          由于動作的隊列是基于頁面的,所以,即使你獲取的最新布局信息的節點沒有待執行的動作,也會觸發重排。

          所以,盡量將修改元素樣式的操作放在一起,然后再執行獲取元素最新布局信息的操作,盡量不要交叉進行,因為每次獲取元素的最新布局信息,都將觸發重排和重繪操作。

          雖然,現代瀏覽器進行了優化,并不會在每次設置元素的樣式或改變dom的結構時都會重繪和重排,,但舊版瀏覽器仍會有性能問題,所以盡量用以下規則來最小化重繪和重排:
          1) 設置樣式:使用cssText屬性來合并更新的樣式信息: el.style.cssText=”padding-left:10px;border:1px”
          2) 改變dom結構:將元素脫離文檔流,然后進行一系列改變,然后再帶回文檔流中,方法如下:
          (1) 隱藏元素,對元素的dom結構進行一系列更改,再顯示元素
          (2) 使用createDocumentFragment創建文檔碎片,針對文檔碎片進行批量操作,然后一次性添加到文檔中
          (3) 將原始元素clone到一個脫離文檔流的節點中,修改這個副本后,替換原始元素
          3) 緩存布局信息
          盡量減少布局信息的獲取次數,如果有針對布局信息的迭代操作,先將布局信息保存到局部變量中,對該局部變量進行迭代更新,然后將該局部變量更新到dom上
          4) 動畫效果時脫離文檔流
            一個元素進行動畫效果時:
          (1) 將該元素脫離文檔流,比如絕對定位該元素
          (2) 對該元素進行動畫操作,這樣不會觸發其他區域的重排和重繪
          (3) 動畫結束時,將元素恢復文檔流位置,這樣只會對其余區域進行一次重排和重繪

          3 使用事件委托來減少dom樹上的事件響應函數
          對文檔中大量的元素進行事件綁定會導致運行時效率下降,基于事件都會冒泡到父層,可以在父層上綁定一個事件,然后識別target(srcElement)來自行dispatch事件。

          四 算法和流程控制
          1 循環
          1) while,for,do-while性能上基本沒差別,不過while和do-while一般被用于基于某個條件的循環,而for用于數組的迭代或線性的工作,而for-in用于對象的枚舉
          2)減少循環次數或減少循環中的工作量都可以優化性能,如duff循環,但性能提升微乎其微,實際測試,在某些瀏覽器下duff循環不僅不會提升性能,還會降低性能,另外倒序循環可能會快一些,畢竟正序是與長度進行對比后的boolean值,而倒序循環是將表示當前循環進度的數值轉換為boolean
          3) 迭代器如js1.6的forEach方法等,性能比用for進行循環要慢一些,但更語義化
          附(我寫的一個duff循環的js版本)

          1. //注:duff的原理是減少循環次數,從而減少對循環條件的判斷,所以duff的性能優化只對超大數組(10萬次條件判斷會降低為10萬/8次條件判斷)有意義,與循環體中的語句數量無關,所以,duff只適用于循環體執行速度非常快,而循環規模非常大的狀況,在js中只有略微的性能提升
          2. function duff(list,callback){
          3.     var i = list.length % 8;
          4.     var tails = i;
          5.     while(i){
          6.         callback(list[--i]);
          7.     }
          8.     var greatest_factor = list.length-1;
          9.     do{
          10.         process(list[greatest_factor]);
          11.         process(list[greatest_factor-1]);
          12.         process(list[greatest_factor-2]);
          13.         process(list[greatest_factor-3]);
          14.         process(list[greatest_factor-4]);
          15.         process(list[greatest_factor-5]);
          16.         process(list[greatest_factor-6]);
          17.         process(list[greatest_factor-7]);
          18.         greatest_factor-=8;
          19.     }while(greatest_factor>tails);
          20. }

          2 條件語句
          1)switch比if-else快一些,但性能微乎其微,建議在數量較多的分支時使用switch,而進行范圍判斷或多重條件的時候使用if-else
          2)if-else的排列從大概率向小概率
          3)如果條件太多,建議使用查找表,而且查找表具有動態擴充的能力
          3 遞歸
          1) 由于調用棧的限制,遞歸是很危險的
          2) 非自調用,而是交叉調用形成的“隱伏遞歸”是很危險的,出錯之后很難排錯
          3) 盡量將遞歸轉化為迭代,比如樹的遍歷,用非遞歸的dfs,bfs來實現就好
          4) 很常用的函數,進行memoize,用空間換取時間

          五 字符串和正則表達式

          1 理解各個瀏覽器的js引擎字符串合并的內部機制:
          1) 避免產生臨時字符串,如用str+=’abc’;str+=’def’  而不要用 str+=’abc’+'def’
          2) firefox會在編譯期合并字符串常量,如str+= ‘abc’+'def’會被轉化為str+=’abcdef’,yur-compressor也有這個功能
          3) 數組的join方法,性能不會比+連接符更快,因為大多數瀏覽器的+連接符不會開辟新內存空間,而ie7,ie6卻會開辟新空間,所以ie6,7中字符串連接應該用數組的join,而其他瀏覽器用+連接符,,concat方法是最慢的方式

          2 正則表達式優化
          基于各js引擎中正則表達式引擎的不同,以下某些方法會帶來某引擎性能的提升但可能同樣導致其他引擎性能的下降,所以原書原文也只是原理性闡述,實際開發時視具體情況而定。
          1) 循環中使用的正則對象盡量在循環前初始化,并賦予一個變量
          2) 編寫正則表達式時,盡量考慮較少的回溯,比如編寫分支時將大概率分支放在前面,在貪婪模式是從尾部向前回溯,懶惰模式從headPart向尾部逐字符回溯
          3) 關于回溯失控,起因是太寬泛的匹配模式,如最后的part沒能成功匹配,則會記住回溯位置,嘗試修改前面的part的匹配方式,如嘗試讓前面的懶惰模式包含一次endPart,然后從新位置再嘗試匹配,正則引擎會一直嘗試,最終導致回溯失控
          4) 在只是搜索確定的字面量,及字面量位置也確定如行首,行尾等,正則非最佳工具
          (詳細的正則優化先略過,等以后再回來寫吧)

          六 快速響應用戶界面

          1 瀏覽器UI線程和UI隊列
          UIThread:javascript和ui是共用同一個線程的,這樣做的好處是無需考慮運行時的用戶態或核心態的線程同步,也無需在語言層面實現臨界區(critical section),在我最初自己摸索javascript的時候,認為javascript也是多線程的,后來寫代碼測試后發現,javascript并非多線程,且javascript代碼的執行會阻塞和ui共用的這唯一的線程,使用戶的操作得不到ui的反饋。

          UIQueue:上面的UIThread負責執行UIQueue中的task,無論用戶對UI采取的動作觸發的dom事件響應函數,還是javascript執行過程中用setTimeout或setInterval創建的定時器事件響應函數,都會被插入到UIQueue中,當UIThread處于busy狀態時,可能會忽略掉一些task,不置入UIQueue,比如用戶動作的產生的ui更新task和觸發的dom-event兩者,ui更新的task會被忽略不放入UIQueue,而dom-event會放入UIQueue等待UIThread處于idle狀態時執行,而setInterval函數的周期性定時器事件,會視UIQueue中是否有相同的事件響應函數,如沒有才會將該task置入UIQueue。
          UIQueue的task來源:
          1) 用戶操作產生的ui更新重繪
          2) 用戶操作觸發的綁定在dom上的javascript事件
          3) dom節點自身狀態改變觸發的綁定在自身上的javascript事件,如的onload
          4) setTimeout與setInterval設置的定時器事件
          5) ajax過程中的onreadystatechange事件,這個javascript函數并非綁定在dom上,而是綁定在xmlHttpRequest對象上

          瀏覽器限制:為了避免某個javascript事件函數執行時間過長,一直占據UIThread,從而導致用戶操作觸發的UIUpdate任務得不到執行,各個瀏覽器使用不同的方案限制了單個javascript事件函數的執行時間
          1) ie:500萬條,在注冊表有設置
          2) firefox: 10秒,在瀏覽器配置設置中(about:config->dom.max_script_run_time)
          3) safari: 5秒,無法修改
          4) chrome: 依賴通用崩潰檢測系統
          5) opera: 無

          界面多久無反應會讓用戶無法忍受: 最長100毫秒,用戶的動作之后超過100毫秒界面沒有做出響應,用戶就會認為自己和界面失去了聯系。

          2 用定時器讓出時間片斷(分解任務)
          任務分解過程:用戶無法忍受一個10秒的任務占據UI線程,而自己的任何操作得不到反饋,于是我們可以將這個10秒的任務分為200次50毫秒的任務,每執行50毫秒,讓出UI線程去執行UI隊列中的界面更新的任務,讓用戶及時得到反饋,然后再執行50毫秒,直到執行完畢。
          基于大多數長時間UI任務都是對數組的循環操作,于是我們可以將這個循環過程進行拆解,示例代碼:

          1. function timedProcessArray(list, callback, complete, progress){
          2.     var total = list.length;
          3.     var curProgress = 0;
          4.     var preProgress = 0;
          5.    
          6.     (function(list, iteration){
          7.         var fn = arguments.callee;
          8.         var st = +new Date();
          9.         while(list.length && (+new Date() - st < 50) ){
          10.             iteration = callback(list.shift(), iteration);
          11.         }
          12.         if(list.length){
          13.             if(progress){ //如果需要對進度進行通知)
          14.                 curProgress = 100 - (100 / total * list.length ^ 0);
          15.                 if(curProgress != preProgress){
          16.                     preProgress = curProgress;
          17.                     progress(curProgress);
          18.                 }
          19.             }
          20.             setTimeout(function(){
          21.                 fn.call(null, list, iteration);
          22.             }, 25);
          23.         }else{
          24.             progress && progress(100);
          25.             complete && complete(iteration);
          26.         }
          27.     })(list.concat(),0);
          28.    
          29. }

          阻塞和非阻塞(任務分割)方式的示例,:http://lichaosoft.net/case/progress.html

          3 web-workers
          在web-workers之前,javascript是沒有多線程的,web-workers標準帶來了真正的多線程,web-workers本來是html5的一部分,現在已經分離出去成為獨立的規范:http://www.w3.org/TR/workers/

          和web-workers之間僅能通過onmessage和postMessage交互。

          使用web-workers以輔助線程進行計算并擁有進度通知的示例:
          http://lichaosoft.net/case/worker.html(請使用支持web-workers的chrome或safari瀏覽)

          web-workers適用于那些無法拆解的任務,對數組的遍歷是一個可以被拆解的任務,對樹的遍歷通過使用dfs或bfs將樹平坦化為數組后也可以進行拆解,不能拆解的任務:
          1) 編碼/解碼大字符串
          2) 復雜數學運算
          3) 大數組排序

          超過100毫秒的任務,如瀏覽器支持web-workers,優先使用web-workers,如不支持則使用timedProcessArray進行分割運行。

          沒有任何javascript代碼的重要度高于用戶體驗,用戶體驗是至高重要的,無論如何不能讓用戶覺得界面反應速度慢。

          七 AJAX
          從廣義上來看,AJAX是指不重載整個頁面的情況下,與服務端進行數據傳輸,解析數據,并局部刷新頁面區域的改善用戶體驗的行為,那么我們下面介紹:數據傳輸,數據格式。

          1 數據傳輸
          1) XHR: 創建一個XMLHttpRequest對象與服務端通信
          2) 動態腳本注入: 這是一個hack,創建一個script元素并設置src為任意uri-A(可跨域),可在頁面中先定義一個數據處理函數如function newsListProc(list){},然后在該uri-A指向的script文件中調用newsListProc并將數據傳入,這項技術也稱為:JSON-P。
          3) mXHR: 將多個資源文件使用XHR傳輸到瀏覽器端,js負責對數據流分割,然后dispath給不同類型資源文件的處理函數
          4) 流式XHR: 在支持readyState為3時,可訪問已解析好的部分xhr數據,既是支持流式XHR,可進行流式處理來優化執行效率,目前實際測試ff3.6支持,而ie6,7,8及chrome都不支持流式XHR。
          5) iframes: 待續
          6) comet: 待續

          注1:關于mXHR和流式XHR,我寫了一個demo用來演示多資源文件合并,由XHR向客戶端傳輸,并在支持流式XHR的瀏覽器中使用流式XHR,DEMO地址:

          注2:純粹的發送數據而無需接受數據,可用beacons方式,類似動態腳本注入,不過創建的不是script元素,而是Image對象,并設置src為要請求的URI,所以這種方式只能使用GET方式,代碼示例:

          1. function keepalive(uri, delay){
          2.     var beacon, delay = delay || 1000,
          3.     timer = setTimeout(function(){
          4.         var fn = arguments.callee;
          5.         beacon = new Image();
          6.         beacon.onload = beacon.onerror = function(){
          7.             timer = setTimeout(fn, delay);
          8.         }
          9.         beacon.src = uri;
          10.     }, delay);
          11.     return function(){
          12.         console.log('stop');
          13.         clearTimeout(timer);
          14.     };
          15. }

          2 數據格式
          1) XML: 使用responseXML對象的getElementsByTagName,getElementById,node.getAttribute等api對xml文檔進行解析,也可以用XPath進行解析,性能更好些,硬傷是:
          (1) “結構/數據”比 太高
          (2) XPath在支持并不廣泛
          (3) 最重要的就是需要先知道內容的詳細結構,針對每個數據結構編寫特定的解析方法
          2) JSON: 最廣泛的數據格式,解析性能較之xml高,”結構/數據”比低,如使用縮略屬性名或完全使用多層數組格式,結構數據比更低,傳輸性能更高
          3) JSON-P: 無需解析,屬于javascript的正常函數調用,性能最高
          4) HTML: 無需解析,服務端已構造好用于局部更新的html數據,直接用innerHTML更新,數據結構比太高,壓力被集中在服務端,網絡傳輸數據量高
          5) 自定義分隔符: 性能最高,結構數據比最低,對于性能要求比較苛刻的環境中使用

          附:創建xhr對象的代碼:

          1. function createXhrObject(){
          2.     if(window.XMLHttpRequest){
          3.         return new XMLHttpRequest();
          4.     }else{
          5.         var msxml_progid = [
          6.             'MSXML2.XMLHTTP.6.0', //支持readyState的3狀態,但此狀態時讀取responseText為空,覺得似乎沒意義。。
          7.             'MSXML3.XMLHTTP',
          8.             'Microsoft.XMLHTTP',
          9.             'MSXML2.XMLHTTP.3.0'
          10.         ];
          11.         var req;
          12.         for(var i=0;i<msxml_progid.length;i++){
          13.             try{
          14.                 req = new ActiveXObject(msxml_progid[i]);
          15.                 break;
          16.             }catch(ex){}
          17.         }
          18.         return req;
          19.     }
          20. }

          從字符流中異步解析數據的工具類

          1. /*
          2. * @method StringStreamParser 流式字符串異步解析類
          3. * @param onRow 解析出數據行的回調函數
          4. * @param RowSeperator 行分隔符,默認'u0001'
          5. * @param ColSeperator 列分隔符,默認'u0002'
          6. */
          7. function StringStreamParser(onRow, rowSeperator, colSeperator){
          8.     if (!(this instanceof arguments.callee)){
          9.         return new arguments.callee(onRow, rowSeperator, colSeperator);
          10.     }
          11.     var stream = '';                               
          12.     rowSeperator = rowSeperator || 'u0001';
          13.     colSeperator = colSeperator || 'u0002';
          14.    
          15.     /* @method write 向字符流寫入包
          16.      * @param packet 寫入的包
          17.      * @param lastPacket 是否為最后一個包,默認false
          18.      */
          19.     this.write = function(packet, lastPacket){
          20.         stream += packet;
          21.         var rowIdx = stream.indexOf(rowSeperator);
          22.         var colIdx,strRow, dataRow;
          23.        
          24.         while(rowIdx!==-1 || lastPacket){
          25.        
          26.             if(rowIdx===-1){   
          27.                 strRow = stream.substr(0);
          28.                 lastPacket = false;
          29.             }else{
          30.                 strRow = stream.substr(0,rowIdx);
          31.                 stream = stream.substr(rowIdx+1);
          32.                 rowIdx = stream.indexOf(rowSeperator);                   
          33.             }
          34.            
          35.             dataRow = [];
          36.             while(colIdx = strRow.indexOf(colSeperator)){
          37.                 if(colIdx !== -1){
          38.                     dataRow.push(strRow.substr(0, colIdx));
          39.                     strRow = strRow.substr(colIdx+1);
          40.                 }else{
          41.                     dataRow.push(strRow.substr(0));
          42.                     break;
          43.                 }
          44.             }
          45.             onRow.call(null, dataRow);
          46.            
          47.         }
          48.     }
          49. }

          3 其他
          在確定了合適的數據傳輸技術,和數據傳輸格式之后,還可以采取以下方式酌情優化ajax:
          1) 緩存數據: 最快的請求,是不請求,有以下兩種方式緩存:
            (1) 對于GET請求,在response中,設置expires頭信息,瀏覽器即會將此請求緩存,這種緩存是跨會話也是跨頁面的
          (2) 在javascript中,以url作為唯一標識符緩存請求到的數據,無法跨頁面也無法跨會話,但可編程控制緩存過程(不能跨會話也不能跨頁面,這種緩存基本上是無意義的)
          2) 在必要的時候,直接使用XHR對象而非ajax庫,如需要流式的數據處理

          八 常見編碼中的性能提高點
          1 避免雙重求值: eval,Function,setTimeout和setInterval都允許傳入字符串,而此時會創建一個新的編譯器的實例,將會導致很大的性能損失。(另外這些方式執行代碼時,代碼的作用域在各個瀏覽器也不盡相同,容易掉坑)
          2 使用Object/Array的直接量進行定義(且直接量比new Object()然后設置屬性更節省代碼)
          3 不要讓代碼重復運行
          1) 延遲定義

          2) 條件預定義
          4 盡量使用原生javascript
          轉載自:http://www.cnblogs.com/pansly/archive/2011/06/29/2093769.html

          主站蜘蛛池模板: 商城县| 海宁市| 扶风县| 望城县| 桐庐县| 斗六市| 鄱阳县| 彭阳县| 肃北| 鹤岗市| 缙云县| 广南县| 贺兰县| 伊金霍洛旗| 盘锦市| 信丰县| 缙云县| 苗栗县| 大名县| 卓资县| 弥渡县| 陆川县| 鸡东县| 三穗县| 昌邑市| 抚松县| 象州县| 漳平市| 大荔县| 岳阳县| 崇信县| 吉安县| 武山县| 乌拉特中旗| 萨迦县| 青铜峡市| 黑河市| 饶河县| 延吉市| 肇源县| 林西县|