最近在學習性能優化,學習了雅虎軍規 ,但是覺着有點雲裏霧裏的,由於裏面有些東西雖然本身也一直在使用,可是感受不太明白因此然,好比減小DNS查詢,css和js文件的順序。因此就花了時間去了解瀏覽器的工做,有一篇經典的文章《how browsers work》 ,講的很詳細,也有中文譯本 。不過就是文章有點太長,也講了一堆東西,仍是本身總結一下。javascript
爲何要了解瀏覽器加載、解析、渲染這個過程?
好,咱們先說一下,爲何要了解這些呢?若是想寫出一個最佳實踐的頁面,就要好好了解。css
- 瞭解瀏覽器如何進行加載,能夠在引用外部樣式文件,外部js時,將他們放到合適的位置,使瀏覽器以最快的速度將文件加載完畢。
- 瞭解瀏覽器如何進行解析,能夠在構建DOM結構,組織css選擇器時,選擇最優的寫法,提升瀏覽器的解析速率。
- 瞭解瀏覽器如何進行渲染,明白渲染的過程,在設置元素屬性,編寫js文件時,能夠減小」reflow「」repaint「的消耗。
正文開始html
1、瀏覽器的主要功能
瀏覽器的主要功能是將用戶選擇的web資源呈現出來,它須要從服務器請求資源,並將其顯示在瀏覽器窗口中,資源的格式一般是HTML,也包括PDF、image及其餘格式。用戶用URI(Uniform Resource Identifier統一資源標識符)來指定所請求資源的位置,經過DNS查詢,將網址轉換爲IP地址。整個瀏覽器工做的流程,以前博客中有論述:
一、輸入網址。
二、瀏覽器查找域名的IP地址。
3. 瀏覽器給web服務器發送一個HTTP請求
4. 網站服務的永久重定向響應
5. 瀏覽器跟蹤重定向地址 如今,瀏覽器知道了要訪問的正確地址,因此它會發送另外一個獲取請求。
6. 服務器「處理」請求,服務器接收到獲取請求,而後處理並返回一個響應。
7. 服務器發回一個HTML響應
8. 瀏覽器開始顯示HTML
9. 瀏覽器發送請求,以獲取嵌入在HTML中的對象。在瀏覽器顯示HTML時,它會注意到須要獲取其餘地址內容的標籤。這時,瀏覽器會發送一個獲取請求來從新得到這些文件。這些文件就包括CSS/JS/圖片等資源,這些資源的地址都要經歷一個和HTML讀取相似的過程。因此瀏覽器會在DNS中查找這些域名,發送請求,重定向等等…java
那麼,一個頁面,到底是如何從咱們輸入一個網址到最後完整的呈如今咱們面前的呢?還須要瞭解一下瀏覽器是如何渲染的:web
2、瀏覽器的渲染
下面是渲染引擎在取得內容以後的基本流程:後端
解析html以構建dom樹 -> 構建render樹 -> 佈局render樹 -> 繪製render樹瀏覽器
先來看個圖:性能優化
因此,瀏覽器會解析三個東西:
(1) HTML/SVG/XHTML,解析這三種文件會產生一個 DOM Tree。
(2) CSS,解析 CSS 會產生 CSS 規則樹。
(3) JavaScript腳本,主要是經過 DOM API 和 CSSOM API 來操做 DOM Tree 和 CSS Rule Tree.服務器
我今天又糾結了一上午,究竟是怎麼解析怎麼渲染的,個人疑問在於,瀏覽器究竟是先解析生成了DOM樹,而後再加載CSS JS文件進行渲染,仍是在生成DOM的過程當中,遇到了 link script 而後就加載CSS JS,邊加載邊渲染。我有這種疑問的緣由在於,看網上的帖子,說的根本不同好嘛! 好比這篇 我想說,這個寫的讓我直接懵逼,真的是直接懵逼啊,學習的過程當中,總會遇到困難,但此次,讓我真的好難啊。不過正由於不懂才繼續查資料繼續學習嘛 ==!我又查了一上午,本身測試測試測試,而後覺着,我好像是明白點了。真的推薦你們去認真看《how browsers work》這篇文章,學習不懂得知識的時候,仍是要從比較權威的資料看起比較好,也不要像我今天這樣,無頭蒼蠅亂查。markdown
那麼就來講一下圖中的過程,我是按照本身的理解來講,若是有誤,歡迎指正。
當瀏覽器得到一個html文件時,會
「自上而下」
加載,並在加載過程當中進行解析渲染。
解析:
1. 瀏覽器會將HTML解析成一個DOM樹,DOM 樹的構建過程是一個深度遍歷過程:當前節點的全部子節點都構建好後纔會去構建當前節點的下一個兄弟節點。
2. 將CSS解析成 CSS Rule Tree 。
3. 根據DOM樹和CSSOM來構造 Rendering Tree。注意:Rendering Tree 渲染樹並不等同於 DOM 樹,由於一些像 Header 或 display:none 的東西就不必放在渲染樹中了。
4.有了Render Tree,瀏覽器已經能知道網頁中有哪些節點、各個節點的CSS定義以及他們的從屬關係。下一步操做稱之爲Layout,顧名思義就是計算出每一個節點在屏幕中的位置。
5.再下一步就是繪製,即遍歷render樹,並使用UI後端層繪製每一個節點。
重點來了:
上述這個過程是逐步完成
的,爲了更好的用戶體驗,渲染引擎將會盡量早的將內容呈現到屏幕上,並不會等到全部的html都解析完成以後再去構建和佈局render樹。它是解析完一部份內容就顯示一部份內容,同時,可能還在經過網絡下載其他內容。(這段話是《how browsers work》裏面講的,讓我茅塞頓開)
幾個概念:
(1)Reflow(迴流):瀏覽器要花時間去渲染,當它發現了某個部分發生了變化影響了佈局,那就須要倒回去從新渲染。
(2)Repaint(重繪):若是隻是改變了某個元素的背景顏色,文字顏色等,不影響元素周圍或內部佈局的屬性,將只會引發瀏覽器的repaint,重畫某一部分。
Reflow要比Repaint更花費時間,也就更影響性能。因此在寫代碼的時候,要儘可能避免過多的Reflow。
reflow的緣由:
(1)頁面初始化的時候;
(2)操做DOM時;
(3)某些元素的尺寸變了;
(4)若是 CSS 的屬性發生變化了。
減小 reflow/repaint
(1)不要一條一條地修改 DOM 的樣式。與其這樣,還不如預先定義好 css 的 class,而後修改 DOM 的 className。
(2)不要把 DOM 結點的屬性值放在一個循環裏當成循環裏的變量。
(3)爲動畫的 HTML 元件使用 fixed 或 absoult 的 position,那麼修改他們的 CSS 是不會 reflow 的。
(4)千萬不要使用 table 佈局。由於可能很小的一個小改動會形成整個 table 的從新佈局。
我應該是已經把網上全部的關於瀏覽器加載 解析 渲染過程的文章都看全了,其中寫的比較好的一個版本是下面這個:
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文件,從新渲染頁面。
與討論主題相關的其餘思考
編寫CSS時應該注意:
CSS選擇符是從右到左進行匹配的。從右到左!因此,#nav li 咱們覺得這是一條很簡單的規則,秒秒鐘就能匹配到想要的元素,可是,可是,可是,是從右往左匹配啊,因此,會去找全部的li,而後再去肯定它的父元素是否是#nav。,所以,寫css的時候須要注意:
- dom深度儘可能淺。
- 減小inline javascript、css的數量。
- 使用現代合法的css屬性。
- 不要爲id選擇器指定類名或是標籤,由於id能夠惟一肯定一個元素。
- 避免後代選擇符,儘可能使用子選擇符。緣由:子元素匹配符的機率要大於後代元素匹配符。後代選擇符;#tp p{} 子選擇符:#tp>p{}
- 避免使用通配符,舉一個例子,.mod .hd *{font-size:14px;} 根據匹配順序,將首先匹配通配符,也就是說先匹配出通配符,而後匹配.hd(就是要對dom樹上的全部節點進行遍歷他的父級元素),而後匹配.mod,這樣的性能耗費可想而知.
關於script標籤的位置
如今,咱們大都會將script標籤放在body結束標籤以前,那緣由是什麼呢?我今天也作了一個測試。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>測試js代碼位置</title> <script type="text/javascript"> var item = document.getElementById("item"); cosole.log(item); </script> </head> <body> <div id="item" width="100px" height="100px"> 你好 </div> </body> </html>
上述代碼中有一段js代碼,要在控制檯打印一個元素,我把script標籤放在head裏,控制檯裏打印出來的是null。
我又把js代碼放在body結束標籤以前,打印出來的就是div元素了
因此,經過這個簡單的例子咱們能夠看到,js代碼在加載完後,是當即執行的。
我又作了一個測試,在js代碼裏面寫了一個死循環,把它放在head標籤中,
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>測試js代碼位置</title> <script type="text/javascript"> var item = document.getElementById("item"); while(true){ console.log(1); } </script> </head> <body> <div id="item" width="100px" height="100px"> 你好 </div> </body> </html>
頁面是這樣的:
一直在執行那個打印1的死循環,後面的body都沒有加載渲染出來。因此,這個小例子,咱們能夠看出,js的下載和執行會阻塞Dom樹的構建。
因此,Javascript的加載和執行的特色:
(1)載入後立刻執行;
(2)執行時會阻塞頁面後續的內容(包括頁面的渲染、其它資源的下載)。緣由:由於瀏覽器須要一個穩定的DOM樹結構,而JS中頗有可能有 代碼直接改變了DOM樹結構,好比使用 document.write 或 appendChild,甚至是直接使用的location.href進行跳轉,瀏覽器爲了防止出現JS修 改DOM樹,須要從新構建DOM樹的狀況,因此 就會阻塞其餘的下載和呈現。
減小 JavaScript 對性能的影響的方法:
- 將全部的script標籤放到頁面底部,也就是body閉合標籤以前,這能確保在腳本執行前頁面已經完成了DOM樹渲染。
- 儘量地合併腳本。頁面中的script標籤越少,加載也就越快,響應也越迅速。不管是外鏈腳本仍是內嵌腳本都是如此。
- 採用無阻塞下載 JavaScript 腳本的方法:
(1)使用script標籤的 defer 屬性(僅適用於 IE 和 Firefox 3.5 以上版本); (2)使用動態建立的script元素來下載並執行代碼;