How Browsers Work 這篇文章把瀏覽器的很多細節講的很細,也有中文的翻譯版本,現在轉載的這篇是陳皓寫的,目的的為了能在上班途中,或是坐馬桶時就能讀完,並能從中學會一些能用在工作上的東西。
無論是作為開發,還是作為黑客,企圖從Web 端注入 SQL,或是XSS 的時候,編碼和解碼都是一個重要的問題。作為瀏覽器,有URL解析引擎,有HTML解析引擎,還有JS解析引擎。其執行的先后順序往往決定了輸出的結果。這種多標簽語言嵌入的,同時又需要客戶端服務交互技術,正是給了XSS 可趁之機。
我們為什么要了解瀏覽器加載、解析、渲染這個過程呢?
這是因為想寫出一個最佳實踐的頁面,就要好好了解:
* 了解瀏覽器如何進行加載,可以在引用外部樣式文件,如外部js時將它們放到合適的位置,使瀏覽器以最快的速度將文件加載完畢;
* 了解瀏覽器如何進行渲染,可以在構建DOM結構,組織css選擇器時,選擇最優的寫法,提高瀏覽器的解析速率;
* 了解瀏覽器如何進行渲染,明白渲染的過程,在設置元素屬性,編寫JS文件時,可以減少“reflow”和“repaint”的消耗。
下面我們要做的就是去了解瀏覽器到底如何解碼,該如何在解碼過程中避免漏洞的產生。在此之上,我們先來看看整個瀏覽器的工作流程。
瀏覽器主要功能
瀏覽器的主要功能是將用戶選擇的Web資源呈現出來,它需要從服務器請求資源,並將其顯示在瀏覽器窗口中,資源的格式通常是HTML,也包括PDF、Image和其他格式。用戶通過URL(Uniform Resource Identifier 統一資源標識符)來指定所請求資源的位置,通過DNS查詢,將網址轉換為IP地址。整個瀏覽器工作的流程,概述如下:
1) 輸入網址
2) 瀏覽器查找域名的IP地址
3) 瀏覽器給Web服務器發送一個HTTP請求
4) 網站服務的永久重定向響應
5) 瀏覽器跟蹤重定向地址獲得要訪問的正確地址,然后會向服務器發送一個獲取請求
6) 服務器接受到獲取請求,處理並返回一個響應
7) 服務器發回一個HTML響應
8) 瀏覽器開始顯示HTML
9) 瀏覽器發送請求,以獲取嵌入在HTML中的對象。
在瀏覽器顯示HTML時,它會注意獲取其他地址內容的標簽。這時,瀏覽器會發送一個獲取請求來重新獲得這些文件。這些文件就包括CSS/JS/圖片等資源,這些資源的地址都要經歷一個和HTML讀取類似的過程。所以瀏覽器會在DNS中查找這些域名,發送請求,重定向等等。
那么一個頁面,究竟是如何從我們輸入一個網址到最后完整地呈現在我們面前呢?還需要了解瀏覽器是如何渲染的:
瀏覽器的渲染
下圖是渲染引擎在取得內容后的基本流程
解析HTML以構建DOM樹 —— 構建render樹 —— 布局render樹 —— 繪制render樹
從圖中,可以看到:
1) 瀏覽器會解析三個東西
* 一個 HTML/SVG/XHTML,解析這三種文件會產生一個DOM Tree
* CSS,解析CSS會產生CSS規則樹
* JavaScript 腳本,主要是通過 DOM API 和 CSSOM API來操作 DOM Tree 和 CSS Rule Tree
當瀏覽器獲得一個HTML文件時,會自上而下加載,並在加載過程中進行渲染。
* 瀏覽器會將HTML解析成一個DOM樹,DOM的構建過程是一個深度遍歷的過程:當前節點的所有子節點都構建好后才會去構建當前節點的下一個兄弟節點。
* 將CSS解析成 CSS Rule tree。
2) 解析完成后,瀏覽器引擎會通過 DOM Tree 和CSS Rule Tree 來構造 Rendering Tree。
注意:
* Rendering Tree 渲染樹並不等同於 DOM Tree,因為一些像 Header 或 display:none 的東西就沒必要放在渲染樹中。 * CSS 的Rule Tree 主要是為了完成匹配並把CSS Rule附加上 Rendering Tree 上的每個 Element,也就是 DOM 節點,也就是所謂的 Frame。
* 有了Render Tree,瀏覽器已經能知道網頁中有哪些節點、各個節點的CSS定義以及它們的從屬關系。 * 然后,計算每個 Frame(也就是每個Element)的位置,這又叫 layout 和 reflow 過程。
3) 最后通過調用操作系統Native GUI的API 繪制
即遍歷render樹,並使用UI后端層繪制每個節點
上述的這個過程是逐步完成的,為了更好的用戶體驗,渲染引擎將會將可能的將內容呈現在屏幕上,並不會等到所有的HTML都解析完成之后再去構建和布局render tree。
它是解析完一部分內容就顯示一部分內容,同時可能還通過網絡下載其他內容。
DOM 解析
HTML 的 DOM Tree解析如下:
<html> <html> <head> <title>Web page parsing</title> </head> <body> <div> <h1>Web page parsing</h1> <p>This is an example Web page.</p> </div> </body> </html>
上面這段HTML 會解析成這樣:
下面是另一個有 SVG 標簽的情況:
CSS 解析
CSS 的解析大概是下面這個樣子(下面主要說的是 Gecko,也就是 Firefox的玩法),假設我們有下面的HTML文檔:
<doc> <title>A few quotes</title> <para class="emph"> Franklin said that <quote>"A penny saved is a penny earned."</quote> </para> <para> FDR said <quote>"We have nothing to fear but <span class="emph">fear itself.</span>"</quote> </para> </doc>
於是DOM Tree 是這個樣子:
然后我們的CSS文檔是這樣的:
/* rule 1 */ doc { display: block; text-indent: 1em; } /* rule 2 */ title { display: block; font-size: 3em; } /* rule 3 */ para { display: block; } /* rule 4 */ [class="emph"] { font-style: italic; }
於是,我們的CSS Rule Tree 會是這樣的:
圖中的第四條規則出現了兩次,一次是獨立的,一次是在規則3的子節點。所以,可以知道建立CSS Rule Tree是需要比照着 DOM Tree來的。CSS 匹配DOM Tree 主要是從右到左解析CSS 的 Selector。所以#nav li 我們以為這是一條很簡單的規則,很容易就能匹配到想要的元素,但是CSS會先去找所有的li 元素,然后再去確定它的父元素是不是#nav。
注意:CSS 匹配HTML 元素是一個相當復雜和有性能問題的事情。所以你可能會在N多地方看到很多人都告訴你,DOM 樹要小,CSS 盡量用id 和 class,千萬不要過渡層疊下去。
* dom深度盡量淺。
* 減少inline javascript、css的數量。
* 使用現代合法的css屬性。
* 不要為id選擇器指定類名或是標簽,因為id可以唯一確定一個元素。
* 避免后代選擇符,盡量使用子選擇符。原因:子元素匹配符的概率要大於后代元素匹配符。后代選擇符;#tp p{} 子選擇符:#tp>p{}
* 避免使用通配符,舉一個例子,.mod .hd *{font-size:14px;} 根據匹配順序,將首先匹配通配符,也就是說先匹配出通配符,然后匹配.hd(就是要對dom樹上的所有節點進行遍歷他的父級元素),然后匹配.mod,這樣的性能耗費可想而知.
通過這兩個樹,我們可以得到一個叫做 Style Context Tree,也就是下面這樣(把CSS Rule 節點 Attach 到 DOM Tree上):
所以,Firefox 基本上來說是通過 CSS解析生成 CSS Rule Tree。然后,通過比對 DOM生成 Style Context Tree,然后 Firefox通過把 Style Context Tree 和其 Render Tree(Frame Tree)關聯上,就完成了。注意:Render Tree會把一些不可見的節點去除掉。而 Firefox 中所謂的Frame 就是一個 DOM節點。
注:Webkit 不像Firefox 要用兩個樹來干這個,Webkit 也有 Style對象,它直接把這個 Style對象存在了相應的 DOM節點上了。
渲染
渲染的流程基本上如下(黃色的四個步驟):
1.計算CSS樣式
2.構建Render Tree
3.Layout - 定位坐標和大小,是否換行,各種position,overflow,z-index屬性等等
4.正式開畫
注意:上圖流程中很多連接線,這表示了 JavaScript動態修改了DOM 屬性或CSS 屬性,有些改變會導致重置 Layout,而不如那些指向天上的箭頭的改變則不會,比如修改后的CSS Rule沒有被匹配到等等。
這里重要說說兩個概念,一個是 Repaint,一個是 Reflow,這兩個不是一回事。
* Repaint(重繪) —— 屏幕一部分要重畫,比如某個元素的背景顏色、文字顏色發生改變等,但是元素的幾何尺寸沒有變,並不影響元素周圍和內容布局屬性,將只會引起瀏覽器的 repaint,重畫某一部分。
* Reflow(回流) —— 瀏覽器要花時間去渲染,當它發現了某個部分發生的變化影響了布局,即意味着元件的幾何尺寸變了,我們需要重新驗證並計算 Render Tree 進行重新渲染。Render Tree的一部分或全部發生了變化,這就是Reflow,或者說是Layout (HTML 使用的是 flow based layout,也就是流式布局,所以如果某元件的幾何尺寸發生了變化,需要重新布局,也就是 Reflow)。reflow 會從 <html> 這個root frame 開始遞歸往下,依此計算所有節點的幾何尺寸和位置。在 reflow 的過程中,可能會增加一些 frame,比如一個文本字符串必須被包裝起來。
下面是一個打開 Wikipedia 時的Layout/Reflow 的視頻(注:HTML 在初始化時也會做一次 reflow,叫 intial reflow),你可以感受下:
Reflow 的成本比Repaint的成本高很多。DOM Tree里的每個節點都會有 reflow 方法,一個節點的 reflow 很有可能導致子節點,甚至父節點以及同級節點的 reflow。在一些高性能的電腦上也許還沒什么,但是如果 reflow 發生在一些手機上,那么這個過程是非常痛苦和耗電的。
所以,下面這些動作有很大可能是成本比較高的:
* 當你增加、刪除、修改DOM節點時,會導致Reflow 或Repaint
* 當你移動DOM位置,或是搞個動畫的時候
* 當你修改CSS樣式的時候
* 當你Resize 窗口的時候(移動端沒有這個問題),或是滾動的時候
* 當你修改網頁的默認字體的時候
注:display:none 會觸發 reflow,而 visibility:hidden 只會觸發 repaint,因為沒有發現位置變化
關於滾動,通常來說如果在滾屏的時候,我們的頁面上的所有的圖像都會跟着滾動,那么性能上沒有什么問題,因為我們的顯卡對於這種把全屏像素往上往下移的算法是很快的。但是如果你有一個 fixed的背景圖,或是有些 Element不跟着滾動,有些 Element是動畫,那么這個滾動的動作對於瀏覽器來說會是一個相當痛苦的過程。你可以看到很多這樣的網頁在滾動的時候性能有多差,因為滾屏也有可能造成 reflow。
基本來說,reflow 有如下幾個原因:
* Initail:網頁初始化的時候
* Incremental:一些JavaScript在操作DOM Tree的時候
* Resize:某些元件的尺寸發生變化時
* Style Change:CSS屬性發生了變化
* Dirty:幾個 Incremental 的reflow 發生在同一個 frame 的子樹上
我們來看一個示例:
var bstyle = document.body.style; // cache bstyle.padding = "20px"; // reflow, repaint bstyle.border = "10px solid red"; // 再一次的 reflow 和 repaint bstyle.color = "blue"; // repaint bstyle.backgroundColor = "#fad"; // repaint bstyle.fontSize = "2em"; // reflow, repaint // new DOM element - reflow, repaint document.body.appendChild(document.createTextNode('dude!'));
當然,我們的瀏覽器是聰明的,它不會像上面那樣,每一次修改樣式就會 reflow 或 repaint。一般來說,瀏覽器會把這樣的操作積攢一批,然后做一次 reflow,這叫異步reflow 或增量異步 reflow。但是有些情況瀏覽器是不會這樣做,比如:resize 窗口,改變了頁面默認的字體等。對於這些操作,瀏覽器會馬上進行 reflow。
但是有些時候,我們的腳本會阻止瀏覽器這么干,比如如果我們請求下面的一些 DOM值:
* offsetTop/Left/Width/Height
* scrollTop/Left/Width/Height
* clientTop/Left/Width/Height
* IE中的 getComputedStyle() 或 currentStyle
因為如果我們需要這些值,那么瀏覽器需要返回最新的值,而一樣會會 flush出去一些樣式的改變,從而造成頻繁的 reflow/repaint
減少reflow/repaint
下面是一些Best Practices:
1) 不要一條一條的修改DOM樣式。與其這樣,還不如預先定義好css的class,然后修改DOM 的className
// bad var left = 10, top = 10; el.style.left = left + "px"; el.style.top = top + "px"; // Good el.className += " theclassname"; // Good el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
2) 把DOM離線后修改,如:
* 使用 documentFragment 對象在內存里操作 DOM * 先把 DOM給 display:none(有一次 reflow),然后想怎么改就怎么改,比如修改100次,然后再把它顯示出來 * clone 一個DOM 節點到內存里,然后想怎么改就怎么改,改完后再和在線的那個交換一下
3) 不要把DOM節點的屬性值放在一個循環里或者當成循環里的變量。不然,這會導致大量的讀寫這個節點的屬性。
4) 盡可能地修改層級比較低的DOM。當然,改變層級比較低的 DOM有可能造成大面積的reflow,但是也可能影響范圍很小。
5) 為動畫的HTML元件使用fixed或absolute的position。那么修改他們的CSS是不會reflow的。
6) 千萬不要使用 table 布局。因為很小的改動就會造成整個 table 的重新布局。
Fixed layout, CSS 2.1 Specification
In this manner, the user agent can begin to lay out the table once the entire first row has been received. Cells in subsequent rows do not affect column widths. Any cell that has content that overflows uses the ‘overflow’ property to determine whether to clip the overflow content.
Automatic layout, CSS 2.1 Specification
This algorithm may be inefficient since it requires the user agent to have access to all the content in the table before determining the final layout and may demand more than one pass.
幾個工具和幾篇文章
有時候,你會發現在IE下,當你不知道修改了什么東西,結果CPU一下子就上到100%,然后過了好幾秒鍾 repaint/reflow 才完成,這種事情在IE時代時常發生。所以,我們需要一些工具來幫助我們查看我們的代碼是否有什么不合適的東西。
* Chrome下,Google的 SpeedTracer 是個非常強悍的工具,能讓你查看你的瀏覽器渲染本有多大。其實Safari 和 Chrome都可以使用開發者工具里的一個Timeline的工具。
* Firefox下基於Firebug 的叫 Firebug Paint Events 插件也不錯
* IE下可以用一個叫 dynaTrace 的IE擴展
最后分享幾篇提高瀏覽器性能的文章:
* Google – Web Performance Best Practices
* Yahoo – Best Practices for Speeding Up Your Web Site
* Steve Souders – 14 Rules for Faster-Loading Web Sites
最后簡單總結下瀏覽器加載、解析、渲染過程
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文件,重新渲染頁面。
相關文章: