1. CSS和JS在網頁中的放置順序是怎樣的?
- css放在head標簽內,防止渲染時出現白屏
- js放在最后body尾部,因為js一般會涉及到一些DOM操作,所以要等全部的dom元素都加載完再加載js,js方法執行時,有可能會阻塞頁面加載,如果把js放在前面可能會造成白屏的現象。
2. 解釋白屏和FOUC
白屏現象:
- 瀏覽器需要加載html文件和所有css文件后,構造了渲染樹后,再把樹節點渲染在屏幕上。如果暫時加載不到css文件,則會阻塞渲染過程,造成白屏現象。
- 如果把樣式放在底部,對于IE瀏覽器,在某些場景下(新窗口打開,刷新等)頁面會出現白屏,而不是內容逐步展現。
- 如果使用 @import 標簽(使用@import標簽引入樣式文件的情況下,會等待html文件加載完成后才加載css文件),即使 CSS 放入 link, 并且放在頭部,也可能出現白屏。
FOUC(Flash of Unstyled Content) 無樣式內容閃爍:
- 如果把樣式放在底部,對于IE瀏覽器,在某些場景下(點擊鏈接,輸入URL,使用書簽進入等),會出現 FOUC 現象(逐步加載無樣式的內容,等CSS加載后頁面突然展現樣式).對于 Firefox 會一直表現出 FOUC .
3.async和defer的作用是什么?有什么區別
<script src="script.js"></script>
沒有 defer 或 async,瀏覽器會立即加載并執行指定的腳本,“立即”指的是在渲染該 script 標簽之下的文檔元素之前,也就是說不等待后續載入的文檔元素,讀到就加載并執行。
<script async src="script.js"></script>
有 async,加載和渲染后續文檔元素的過程將和 script.js 的加載與執行并行進行(異步)。
<script defer src="script.js"></script>
有 defer,加載后續文檔元素的過程將和 script.js 的加載并行進行(異步),但 script.js 的執行要在所有元素解析完成之后,DOMContentLoaded 事件觸發之前完成。
在有 async 的情況下,JavaScript 腳本一旦下載好了就會執行,所以很有可能不是按照原本的順序來執行的。如果 JavaScript 腳本前后有依賴性,使用 async 就很有可能出現錯誤。
4.簡述網頁的渲染機制
- 解析html構建DOM樹
- 解析CSS構建CSSOM樹
- 把DOM和CSSOM組合成渲染樹(Render Tree)
- 在渲染樹的基礎上進行布局,計算每個節點的幾何結構(Layout Tree)
- 把每個節點繪制到屏幕上(Painting)
為了更友好的用戶體驗,瀏覽器會盡可能快的展現內容,而不會等到文檔所有內容到達才開始解析和構建/布局渲染樹,而是每次處理一部分,并展現在屏幕上,這也是為什么我們經常可以看到頁面加載的時候內容是從上到下一點一點展現的。
渲染引擎流程
Webkit渲染引擎流程如下圖:
Gecko渲染引擎流程如下圖:
如上圖,Webkit瀏覽器和Gecko瀏覽器渲染流程大致相同,不同的是:
- Webkit瀏覽器中的渲染樹(render tree),在Gecko瀏覽器中對應的則是框架樹(frame tree),渲染對象(render object)對應的是框架(frame);
- Webkit中的布局(Layout)過程,在Gecko中稱為回流(Reflow),本質是一樣的,后文會解釋回流的另一層含義–重新布局;
- Gecko中HTML和DOM樹中間多了一層內容池(Content sink),可以理解成生成DOM元素的工廠。
單線程
渲染引擎是單線程工作的,意味著渲染流程是一步一步漸進完成的。
解析文檔(PARSER HTML)
在詳細介紹瀏覽器渲染文檔之前,先應該理解瀏覽器如何解析文檔:解析文檔的順序,對于CSS和JavaScript如何處理等。
解析順序
瀏覽器按從上到下的順序掃描解析文檔;解析樣式和腳本
- 腳本
由于通常會在JavaScript腳本中改變文檔DOM結構,于是瀏覽器以同步方式解析,加載和執行腳本,瀏覽器在解析文檔時,當解析到<script>標簽時,會解析其中的腳本(對于外鏈的JavaScript文件,需要先加載該文件內容,再進行解析),然后立即執行,這整個過程都會阻塞文檔解析,直到腳本執行完才會繼續解析文檔。就是說由于腳本是同步加載和執行的,它會阻塞文檔解析,這也解釋了為什么現在通常建議將<script>標簽放在</body>標簽前面,而不是放在<head>標簽里。現在HTML5提供defer和async兩個屬性支持延遲和異步加載JavaScript文件,如:
<script defer src="script.js">
- 改進
針對上文說的腳本阻塞文檔解析,主流瀏覽器如Chrome和FireFox等都有一些優化,比如在執行腳本時,開啟另一個線程解析剩余的文檔以找出并加載其他的待下載外部資源(不改變主線程的DOM樹,僅優化加載外部資源)。
- 樣式
不同于腳本,瀏覽器對樣式的處理并不會阻塞文檔解析,大概是因為樣式表并不會改變DOM結構。
- 樣式表與腳本
你可能想問樣式是否會阻塞腳本文件的加載執行呢?正常情況是不會的,但是存在一個問題是通常我們會在腳本中請求樣式信息,但是在文檔解析時,如果樣式尚未加載或解析,將會得到錯誤信息,對于這一問題,FireFox瀏覽器和Webkit瀏覽器處理策略不同:
- 當存在有樣式文件未被加載和解析時,FireFox瀏覽器會阻塞所有腳本;
- 而Webkit瀏覽器只會阻塞操作了改文件內聲明的樣式屬性的腳本。
構建DOM樹
DOM樹,即文檔內所有節點構成的一個樹形結構。
假設瀏覽器獲取返回的如下HTML文檔:
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./theme.css"></link>
<script src="./config.js"></script>
<title>關鍵渲染路徑</title>
</head>
<body>
<h1 class="title">關鍵渲染路徑</h1>
<p>關鍵渲染路徑介紹</p>
<footer>@copyright2017</footer>
</body>
</html>
首先瀏覽器從上到下依次解析文檔構建DOM樹,如下:
構建CSSOM樹
CSSOM樹,與DOM樹結構相似,只是另外為每一個節點關聯了樣式信息。
theme.css樣式內容如下:
html, body {
width: 100%;
height: 100%;
background-color: #fcfcfc;
}
.title {
font-size: 20px;
}
.footer {
font-size: 12px;
color: #aaa;
}
構建CSSOM樹如圖:
執行JAVASCRIPT
上文已經闡述了文檔解析時對腳本的處理,我們得知腳本加載,解析和執行會阻塞文檔解析,而在特殊情況下樣式的加載和解析也會阻塞腳本,所以現在推薦的實踐是<script>標簽放在</body>標簽前面。
構建渲染樹(RENDER TREE)
DOM樹和CSSOM樹都構建完了,接著瀏覽器會構建渲染樹:
渲染樹,代表一個文檔的視覺展示,瀏覽器通過它將文檔內容繪制在瀏覽器窗口,展示給用戶,它由按順序展示在屏幕上的一系列矩形對象組成,這些矩形對象都帶有字體,顏色和尺寸,位置等視覺樣式屬性。對于這些矩對象,FireFox稱之為框架(frame),Webkit瀏覽器稱之為渲染對象(render object, renderer),后文統稱為渲染對象。
這里把渲染樹節點稱為矩形對象,是因為,每一個渲染對象都代表著其對應DOM節點的CSS盒子,該盒子包含了尺寸,位置等幾何信息,同時它指向一個樣式對象包含其他視覺樣式信息。
渲染樹與DOM樹
每一個渲染對象都對應著DOM節點,但是非視覺(隱藏,不占位)DOM元素不會插入渲染樹,如<head>元素或聲明display: none;的元素,渲染對象與DOM節點不是簡單的一對一的關系,一個DOM可以對應一個渲染對象,但一個DOM元素也可能對應多個渲染對象,因為有很多元素不止包含一個CSS盒子,如當文本被折行時,會產生多個行盒,這些行會生成多個渲染對象;又如行內元素同時包含塊元素和行內元素,則會創建一個匿名塊級盒包含內部行內元素,此時一個DOM對應多個矩形對象(渲染對象)。渲染樹及其對應DOM樹如圖:
- 圖中渲染樹viewport即視口,是文檔的初始包含塊,scroll代表滾動區域
- 渲染樹并不會包含顯式或隱式地display:none;的標簽元素。
布局過程
布局是一個從上到下,從外到內進行的遞歸過程,從根渲染對象,即對應著HTML文檔根元素<html>,然后下一級渲染對象,如對應著<body>元素,如此層層遞歸,依次計算每一個渲染對象的幾何信息(位置和尺寸)。
幾何信息-位置和尺寸,即相對于窗口的坐標和尺寸,如根渲染對象,其坐標為(0, 0),尺寸即是視口
尺寸(瀏覽器窗口的可視區域)。
** 每一個渲染對象的布局流程基本如:**
- 計算此渲染對象的寬度(width);
- 遍歷此渲染對象的所有子級,依次:
2.1 設置子級渲染對象的坐標
2.2 判斷是否需要觸發子渲染對象的布局或回流方法,計算子渲染對象的高度(height) - 設置此渲染對象的高度:根據子渲染對象的累積高,margin和padding的高度設置其高度;
- 設置此渲染對象臟位值為false。
繪制(PAINTING)
最后是繪制(paint)階段或重繪(repaint)階段,瀏覽器UI組件將遍歷渲染樹并調用渲染對象的繪制(paint)方法,將內容展現在屏幕上,也有可能在之后對DOM進行修改,需要重新繪制渲染對象,也就是重繪,繪制和重繪的關系可以參考布局和回流的關系。
一個重要的概念reflow和repaint
- Repaint
屏幕的一部分要重畫,比如某個CSS的背景色變了。但是元素的幾何尺寸沒有變。 - Reflow
意味著元件的幾何尺寸變了,我們需要重新驗證并計算Render Tree。是Render Tree的一部分或全部發生了變化。這就是Reflow,或是Layout。(HTML使用的是flow based layout,也就是流式布局,所以,如果某元件的幾何尺寸發生了變化,需要重新布局,也就叫reflow)reflow 會從<html>這個root frame開始遞歸往下,依次計算所有的結點幾何尺寸和位置,在reflow過程中,可能會增加一些frame,比如一個文本字符串必需被包裝起來。
Reflow的成本比Repaint的成本高得多的多。DOM Tree里的每個結點都會有reflow方法,一個節點的reflow很有可能導致子子點,甚至父節點以及同級節點的reflow。在一些高性能的電腦上也許還沒什么,但是如果reflow發生在手機上,那么這個過程是非常痛苦和耗電的。
所以,下面這些動作有很大可能會是成本比較高的。
- 當你增加、刪除、修改DOM結點時,會導致Reflow或Repaint
- 當你移動DOM的位置,或是搞個動畫的時候。
- 當你修改CSS樣式的時候。
- 當你Resize窗口的時候(移動端沒有這個問題),或是滾動的時候。
- 當你修改網頁的默認字體時。
注:display:none會觸發reflow,而visibility:hidden只會觸發repaint,因為沒有發現位置變化。
減少reflow/repaint
- 不要一條一條地修改DOM的樣式。與其這樣,還不如預先定義好css的class,然后修改DOM的className。
- 不要把DOM結點的屬性值放在一個循環里當成循環里的變量。不然這會導致大量地讀寫這個結點的屬性。
- 盡可能的修改層級比較低的DOM。當然,改變層級比較低的DOM有可能會造成大面積的reflow,但是也可能影響范圍很小。
為動畫的HTML元件使用fixed或absoult的position,那么修改他們的CSS是不會reflow的。
千萬不要使用table布局。因為可能很小的一個小改動會造成整個table的重新布局。