由網頁假死現象查找到的資料

一、現象說明

前段時間,業務組同事,發現了一個現象;前臺頁面查詢記錄分頁(500條記錄時),(每條記錄的屬性又非常的多,達50個左右)頁面一直卡在那裏,等了將近5分鐘左右;100條時雖然有些慢,但也可以在1-2分鐘響應過來。第一反應,難道是這幫傢伙寫的sql後臺。。。。又一想也不太可能啊,畢竟沒有複雜的業務邏輯,並且數據量也不大。不要瞎猜了,chrome network,timing ;發現後端數據響應很快返回了,那就是頁面處理的問題了。好吧,有人說是easyui處理的問題,那我們使用的是easyui,datagrid; 這個用法有這個問題麼,上網搜索一下。

二、EasyUI問題

http://www.easyui.info/archives/1435.html------------jQuery EasyUI Datagrid性能優化專題
http://www.2cto.com/kf/201312/262455.html--------大數據加載效率慢,優化解決方法

三、JavaScript引擎和渲染引擎---http://cube.qq.com/?p=16



瀏覽器是如何把HTML/CSS結合起來的內容呈現到窗口上呢, 不同瀏覽器略微會有些不同。但基本上都是類似的,見下圖:

JavaScript引擎和渲染引擎
JavaScript引擎和渲染引擎


瀏覽器把獲取到的html代碼解析成1個Dom樹,html中的每個tag都是Dom樹中的1個節點,根節點就是我們常用的document對象( tag)。dom樹就是我們用firebug或者IE Developer Toolbar等工具看到的html結構,裏面包含了所有的html tag,包括display:none隱藏,還有用JS動態添加的元素等。
瀏覽器把所有樣式(主要包括css和瀏覽器的樣式設置)解析成樣式結構體,在解析的過程中會去掉瀏覽器不能識別的樣式,比如IE會去掉-moz開頭的樣式,而firefox會去掉_開頭的樣式。
dom tree和樣式結構體結合後構建呈現樹(render tree),render tree有點類似於dom tree,但其實區別有很大,render tree能識別樣式,render tree中每個node都有自己的style,而且render tree不包含隱藏的節點(比如display:none的節點,還有head節點),因爲這些節點不會用於呈現,而且不會影響呈現的,所以就不會包含到render tree中。注意 visibility:hidden隱藏的元素還是會包含到render tree中的,因爲visibility:hidden 會影響佈局(layout),會佔有空間。根據css2的標準,render tree中的每個節點都稱爲box(Box dimensions),box所有屬性:width,height,margin,padding,left,top,border等。
一旦render tree構建完畢後,瀏覽器就可以根據render tree來繪製頁面了。
如果把這個過程比喻成蓋樓的話,迴流(reflow)就好比是拆掉重來,而重繪(repaint)就相當於在不破壞結構的情況下對外牆重新粉刷。因此,迴流必然會重繪,而重繪不一定會迴流。

迴流的花銷也不小,如果每句JS操作都去迴流重繪的話,瀏覽器可能就會受不了。所以很多瀏覽器都會優化這些操作,瀏覽器會維護1個隊列,把所有會引起迴流、重繪的操作放入這個隊列,等隊列中的操作到了一定的數量或者到了一定的時間間隔,瀏覽器就會把flush隊列,進行一個批處理。這樣就會讓多次的迴流、重繪變成一次迴流重繪。

雖然有了瀏覽器的優化,但有時候我們寫的一些代碼可能會強制瀏覽器提前flush隊列,這樣瀏覽器的優化可能就起不到作用了。當你請求向瀏覽器請求一些style信息的時候,就會讓瀏覽器flush隊列,比如:
1. offsetTop, offsetLeft, offsetWidth, offsetHeight
2. scrollTop/Left/Width/Height
3. clientTop/Left/Width/Height
4. width,height
5. 請求了getComputedStyle(), 或者 ie的 currentStyle

當你請求上面的一些屬性的時候,瀏覽器爲了給你最精確的值,需要flush隊列,因爲隊列中可能會有影響到這些值的操作。
減少迴流、重繪其實就是需要減少對render tree的操作,並減少對一些style信息的請求,儘量利用好瀏覽器的優化策略。

html在瀏覽器中會被轉化爲DOM樹,DOM樹的每一個節點都會轉化爲RenderObject, 多個RenderObject可能又會對應一個或多個RenderLayer。瀏覽器渲染的流程如下:

  1. 獲取 DOM 並將其分割爲多個層(RenderLayer)
  2. 將每個層柵格化,並獨立的繪製進位圖中
  3. 將這些位圖作爲紋理上傳至 GPU
  4. 複合多個層來生成最終的屏幕圖像(終極layer)。
  5. 將這些位圖作爲紋理上傳至 GPU
  6. 複合多個層來生成最終的屏幕圖像(終極layer)。

四、HTML頁面加載和解析流程

1.用戶輸入網址(假設是個html頁面,並且是第一次訪問),瀏覽器向服務器發出請求,服務器返回html文件;
2.瀏覽器開始載入html代碼,發現<head>標籤內有一個<link>標籤引用外部CSS文件;
3.瀏覽器又發出CSS文件的請求,服務器返回這個CSS文件;
4.瀏覽器繼續載入html中<body>部分的代碼,並且CSS文件已經拿到手了,可以開始渲染頁面了;
5.瀏覽器在代碼中發現一個<img>標籤引用了一張圖片,向服務器發出請求。此時瀏覽器不會等到圖片下載完,而是繼續渲染後面的代碼;
6.服務器返回圖片文件,由於圖片佔用了一定面積,影響了後面段落的排布,因此瀏覽器需要回過頭來重新渲染這部分代碼;
7.瀏覽器發現了一個包含一行Javascript代碼的<script>標籤,趕快運行它;
8.Javascript腳本執行了這條語句,它命令瀏覽器隱藏掉代碼中的某個<div> (style.display=”none”)。杯具啊,突然就少了這麼一個元素,瀏覽器不得不重新渲染這部分代碼;
9.終於等到了</html>的到來,瀏覽器淚流滿面……
10.等等,還沒完,用戶點了一下界面中的“換膚”按鈕,Javascript讓瀏覽器換了一下<link>標籤的CSS路徑;
11.瀏覽器召集了在座的各位<div><span><ul><li>們,“大夥兒收拾收拾行李,咱得重新來過……”,瀏覽器向服務器請求了新的CSS文件,重新渲染頁面。

五、JavaScript加載執行順序

一、在HTML中嵌入Javasript的方法

直接在Javascript代碼放在標記對<script>和</script>之間
由<script />標記的src屬性制定外部的js文件
放在事件處理程序中,比如:<p οnclick=”alert(‘我是由onclick事件執行的Javascript’)”>點擊我</p>
作爲URL的主體,這個URL使用特殊的Javascript:協議,比如:<a href=”javascript:alert(‘我是由javascript:協議執行的javascript’)”>點擊我</a>
利用javascript本身的document.write()方法寫入新的javascript代碼
利用Ajax異步獲取javascript代碼,然後執行
第3種和第4種方法寫入的Javascript需要觸發才能執行,所以除非特別設置,否則頁面加載時不會執行。

二、Javascript在頁面的執行順序

頁面上的Javascript代碼是HTML文檔的一部分,所以Javascript在頁面裝載時執行的順序就是其引入標記<script />的出現順序, <script />標記裏面的或者通過src引入的外部JS,都是按照其語句出現的順序執行,而且執行過程是文檔裝載的一部分。
每個腳本定義的全局變量和函數,都可以被後面執行的腳本所調用。
變量的調用,必須是前面已經聲明,否則獲取的變量值是undefined。
1 <script type="text/javascript">//<![CDATA[
2    alert(tmp);  //輸出 undefined
3    var tmp = '111';
4    alert(tmp);  //輸出 111
5    //]]>
6 </script>
同一段腳本,函數定義可以出現在函數調用的後面,但是如果是分別在兩段代碼,且函數調用在第一段代碼中,則會報函數未定義錯誤。
1 <script type="text/javascript">//<![CDATA[
2    test();    //瀏覽器報錯
3    //]]>
4 </script>
5 <script type="text/javascript">//<![CDATA[
6    test();    //輸出 fun!
7    function test(){alert('fun!');}
8    //]]>
9 </script>
document.write()會把輸出寫入到腳本文檔所在的位置,瀏覽器解析完documemt.write()所在文檔內容後,繼續解析document.write()輸出的內容,然後在繼續解析HTML文檔。
01 <script type="text/javascript">//<![CDATA[
02    document.write('<script type="text/javascript" src="test.js"><\/script>');
03    document.write('<script type="text/javascript">');
04    document.write('alert("222");')
05    document.write('alert("變量保存值" + tmpStr);');
06    document.write('<\/script>');
07    //]]>
08 </script>
09 <script type="text/javascript">//<![CDATA[
10    alert("333");
11    //]]>
12 </script>
test.js的內容是:

1 var tmpStr = '111';
2    alert(tmpStr);
在Firefox和Opera中的彈出值的順序是:111、222、變量保存值111、333
在IE中彈出值的順序是:222、111、333, 同時瀏覽器報錯:tmpStr未定義
原因可能是IE在document.write時,並未等待加載SRC中的Javascript代碼完畢後,才執行下一行,所以導致222先彈出,並且執行到document.write(‘document.write(“變量保存值” + tmpStr)’)調用tmpStr時,tmpStr並未定義,從而報錯。

要解決這個問題,可以利用HTML解析時解析完一個HTML標籤,再執行下一個的原理,把代碼拆分來實現(上面第一段js分拆成兩段):

01 <script type="text/javascript">//<![CDATA[
02    document.write('<script type="text/javascript" src="test.js"><\/script>');
03    //]]>
04 </script>
05 <script type="text/javascript">//<![CDATA[
06    document.write('<script type="text/javascript">');
07    document.write('alert("222");')
08    document.write('alert("變量保存值" + tmpStr);');
09    document.write('<\/script>');
10    //]]>
11 </script>
12 <script type="text/javascript">//<![CDATA[
13    alert('333');
14    //]]>
15 </script>
這樣IE下和其他瀏覽器輸出值的順序一致了:111、222、變量保存值111、333。

三、如何改變Javascript在頁面的執行順序

利用window.onload
1 <script type="text/javascript">//<![CDATA[
2 window.onload = func1;
3 function func1(){alert('111');}
4 alert('222');
5 //]]>
6 </script>
輸出值順序是 222、111。


需要注意的是,如果存在多個winodws.onload的話,只有最後一個生效,解決的辦法是:

1 window.onload = function(){f();f1();f2();.....}
利用2級DOM事件類型

1 if(document.addEventListener){
2 window.addEventListener('load',f,false);
3 window.addEventListener('load',f1,false);
4 ...
5 }else{
6 window.attachEvent('onload',f);
7 window.attachEvent('onload',f1);
8 ...
9 }
IE中可以利用defer,defer作用是把代碼加載下來,並不立即執行,等文檔裝載完畢之後再執行,有點類似window.onload,但是沒有window.onload那樣的侷限性,可以重複使用,但是隻在IE中有效,所以上面的例子可以修改成爲
01 <script type="text/javascript">//<![CDATA[
02 document.write('<script type="text/javascript" src="test.js"><\/script>');
03 document.write('<script type="text/javascript" defer="defer">');
04 document.write('alert("222");')
05 document.write('alert("變量保存值" + tmpStr);');
06 document.write('<\/script>');
07 //]]>
08 </script>
09 <script type="text/javascript">//<![CDATA[
10 alert("333");
11 //]]>
12 </script>
這樣IE就不報錯了,輸出值的順序變成:111、333、222、變量保存值111

當HTML解析器遇到一個腳本,它必須按常規終止對文檔的解析並等待腳本執行。爲了解決這個問題HTML4標準定義了defer。通過defer來提示瀏覽器可以繼續解析HTML文檔,並延遲執行腳本。這種延遲在腳本從外部文件載入時非常有用,讓瀏覽器不必等待外部文件全部載入之後才繼續執行,能有效的提高性能。IE是目前唯一支持defer屬性的瀏覽器,但IE並沒有正確的實現了defer屬性,因爲延遲的腳本總是被延遲,直到文檔結束,而不是隻延遲到下一個非延遲的腳本。這意味着,IE中延遲的腳本的執行順序相當混亂,並且不能定義任何後面非延遲腳本並須的函數和變量。在IE中所有的defer的腳本執行時間應該都是HTML文檔樹建立以後,window.onload之前。

利用Ajax。
因爲xmlhttpRequest能判斷外部文檔加載的狀態,所以能夠改變代碼的加載順序。
4、一些注意事項:

以上所講的deffer看似用起來沒什麼問題,但是實際上無法兼容Mozilla。所以通常我們還是要藉助window.onload來實現。但是,之後就沒有問題了麼?假設頁面dom裏有一張圖片,像這樣:

1 <img src="picture.jpg" >
而這張圖片又非常之大(例如30MB,這個數字雖然有點極端,但真的在有些網站出現過!),那麼在dom加載完畢之前,js是無法執行的。問題就在於假設dom提供了用戶交互的功能。例如按鈕,輸入表單等,這個時候他們已經是被呈現了的,因此就很有可能產生無效的用戶行爲。

我們不能指望用戶像我們預期的那樣等待頁面顯示加載完畢後再發生動作,而要把用戶考慮成隨時隨刻會到處亂點的朋友。

這個問題又如何解決呢?既然我們需要頁面結構輸出後執行js,我們不妨把js入口函數定義在頁面最下面好了。

1 <head>
2  <script src="x.js" type="text/javascript"></script>
3 </head>
4 <body>
5   ......
6  <img src="picture.jpg" >
7  <script type="text/javascript">init();</script>
8 </body>
這樣就達到我們的目的了,頁面結構輸出完畢後就執行js,不用考慮圖片的加載。

但是在文檔末尾嵌入一條js腳本,畢竟容易被忽略,把關鍵的程序入口放在這種渺小的角落,總覺得不太合適。那有什麼預留退路的方法沒有呢?

我們可以把結尾的腳本稍微修改一下:

1 <head>
2  <script src="x.js" type="text/javascript"></script>
3 </head>
4 <body>
5   ......
6  <img src="picture.jpg" >
7 <script type="text/javascript">window.onload();</script>
8 </body>
而在js裏預先把入口定義給onload事件:


1 window.onload = function() {
2 alert("load over");
3 }
這時候頁面結構加載完畢後就會調用onload函數,而即使漏寫了dom裏的onload入口,js自身裏的onload定義也會在頁面加載完畢後執行,這樣退路就留出來了。

不過這時候有個問題,onload事件會執行兩次,可以在js的onload實現裏解決這個問題,改成這樣:

1 var flag = false;
2 window.onload = function() {
3  if (flag) {return;}
4   flag= true;
5   alert("load over");
6 }
這樣似乎已經解決我們所有的問題了,不過仍然有些小遺憾,因爲最後一行代碼,致使行爲與結構沒有分離開來,要 unobtrusive 就要 unobtrusive 的徹底,爲了達到完美的分離,還有很大的討論空間。

而對於js文件內部的onload事件,我們還可以參考 Simon Willison 的addLoadEvent函數來優化:

01 function addLoadEvent(func) {
02  var oldonload = window.onload;
03  if (typeof window.onload != 'function') {
04     window.onload = func;
05   } else {
06     window.onload = function() {
07      if (oldonload) {
08         oldonload();
09       }
10       func();
11     }
12   }
13 }
然後,我們就可以在js裏肆無忌憚地不停地將各個不同的函數添加到onload事件響應中了:

1 addLoadEvent(funcA);
2 addLoadEvent(funcB);
3 addLoadEvent(funcC);
當然,同一個js裏設置多個onload響應函數其實沒什麼必要,我們完全可以把funcA、funcB、funcC封裝在一個函數裏add,addLoadEvent函數,更理想的狀態是爲頁面動態調用的多個js文件添加入口。
發佈了138 篇原創文章 · 獲贊 2 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章