原來 CSS 與 JS 是這樣阻塞 DOM 解析和渲染的

sea_ljf發表於2017-09-24

hello~各位親愛的看官老爺們大家好。估計大家都聽過,儘量將CSS放頭部,JS放底部,這樣可以提高頁面的效能。然而,為什麼呢?大家有考慮過麼?很長一段時間,我都是知其然而不知其所以然,強行背下來應付考核當然可以,但實際應用中必然一塌糊塗。因此洗(wang)心(yang)革(bu)面(lao),小結一下最近玩出來的成果。

友情提示,本文也是小白向為主,如果直接想看結論可以拉到最下面看的~


由於關係到檔案的讀取,那是肯定需要伺服器的,我會把全部的檔案放在github上,給我點個 star 我會開心!掘金上再給我點個 我就更開心了~

node端唯一需要解釋一下的是這個函式:

function sleep(time) {
  return new Promise(function(res) {
    setTimeout(() => {
      res()
    }, time);
  })
}
複製程式碼

嗯!其實就延時啦。如果CSS或者JS檔名有sleep3000之類的字首時,意思就是延遲3000毫秒才會返回這檔案。

下文使用的HTML檔案是長這樣的:

    <!DOCTYPE html>
    <html lang="en">
    <head>
    	<meta charset="UTF-8">
    	<title>Title</title>
    	<style>
    		div {
    			width: 100px;
    			height: 100px;
    			background: lightgreen;
    		}
    	</style>
    </head>
    <body>
    	<div></div>
    </body>
    </html>
複製程式碼

我會在其中插入不同的JSCSS

而使用的common.css,不論有沒有字首,內容都是這樣的:

div {
  background: lightblue;
}
複製程式碼

好了,話不多數,開始正文!

CSS

關於CSS,大家肯定都知道的是<link>標籤放在頭部效能會高一點,少一點人知道如果<script><link>同時在頭部的話,<script>在上可能會更好。這是為什麼呢?下面我們一起來看一下CSSDOM的影響是什麼。

CSS 不會阻塞 DOM 的解析

注意哦!這裡說的是DOM 解析,證明的例子如下,首先在頭部插入<script defer src="/js/logDiv.js"></script>JS檔案的內容是:

const div = document.querySelector('div');
console.log(div);
複製程式碼

defer屬性相信大家也很熟悉了,MDN對此的描述是用來通知瀏覽器該指令碼將在文件完成解析後,觸發 DOMContentLoaded 事件前執行。設定這個屬性,能保證DOM解析後馬上列印出div

之後將<link rel="stylesheet" href="/css/sleep3000-common.css">插入HTML檔案的任一位置,開啟瀏覽器,可以看到是首先列印出div這個DOM節點,過3s左右之後才渲染出一個淺藍色的div。這就證明了CSS 是不會阻塞 DOM 的解析的,儘管CSS下載需要3s,但這個過程中,瀏覽器不會傻等著CSS下載完,而是會解析DOM的。

這裡簡單說一下,瀏覽器是解析DOM生成DOM Tree,結合CSS生成的CSS Tree,最終組成render tree,再渲染頁面。由此可見,在此過程中CSS完全無法影響DOM Tree,因而無需阻塞DOM解析。然而,DOM TreeCSS Tree會組合成render tree,那CSS會不會頁面阻塞渲染呢?

CSS 阻塞頁面渲染

其實這一點,剛才的例子已經說明了,如果CSS 不會阻塞頁面阻塞渲染,那麼CSS檔案下載之前,瀏覽器就會渲染出一個淺綠色的div,之後再變成淺藍色。瀏覽器的這個策略其實很明智的,想象一下,如果沒有這個策略,頁面首先會呈現出一個原始的模樣,待CSS下載完之後又突然變了一個模樣。使用者體驗可謂極差,而且渲染是有成本的。

因此,基於效能與使用者體驗的考慮,瀏覽器會盡量減少渲染的次數,CSS順理成章地阻塞頁面渲染。

然而,事情總有奇怪的,請看這例子,HTML頭部結構如下:

<header>
    <link rel="stylesheet" href="/css/sleep3000-common.css">
    <script src="/js/logDiv.js"></script>
</header>
複製程式碼

但思考一下這會產生什麼結果呢?

答案是瀏覽器會轉圈圈三秒,但此過程中不會列印任何東西,之後呈現出一個淺藍色的div,再列印出null。結果好像是CSS不單阻塞了頁面渲染,還阻塞了DOM 的解析啊!稍等,在你打算掀桌子瘋狂吐槽我之前,請先思考一下是什麼阻塞了DOM 的解析,剛才已經證明了CSS是不會阻塞的,那麼阻塞了頁面解析其實是JS!但明明JS的程式碼如此簡單,肯定不會阻塞這麼久,那就是JS在等待CSS的下載,這是為什麼呢?

仔細思考一下,其實這樣做是有道理的,如果指令碼的內容是獲取元素的樣式,寬高等CSS控制的屬性,瀏覽器是需要計算的,也就是依賴於CSS。瀏覽器也無法感知指令碼內容到底是什麼,為避免樣式獲取,因而只好等前面所有的樣式下載完後,再執行JS。因而造成了之前例子的情況。

所以,看官大人明白為何<script><link>同時在頭部的話,<script>在上可能會更好了麼?之所以是可能,是因為如果<link>的內容下載更快的話,是沒影響的,但反過來的話,JS就要等待了,然而這些等待的時間是完全不必要的。

JS

JS,也就是<script>標籤,估計大家都很熟悉了,不就是阻塞DOM解析和渲染麼。然而,其中其實還是有一點細節可以考究一下的,我們一起來好好看看。

JS 阻塞 DOM 解析

首先我們需要一個新的JS檔名為blok.js,內容如下:

const arr = [];
for (let i = 0; i < 10000000; i++) {
  arr.push(i);
  arr.splice(i % 3, i % 7, i % 5);
}
const div = document.querySelector('div');
console.log(div);
複製程式碼

其實那個陣列操作時沒意義的,只是為了讓這個JS檔案多花執行時間而已。之後把這個檔案插入頭部,瀏覽器跑一下。

結果估計大家也能想象得到,瀏覽器轉圈圈一會,這過程中不會有任何東西出現。之後列印出null,再出現一個淺綠色的div。現象就足以說明JS 阻塞 DOM 解析了。其實原因也很好理解,瀏覽器並不知道指令碼的內容是什麼,如果先行解析下面的DOM,萬一指令碼內全刪了後面的DOM,瀏覽器就白乾活了。更別談喪心病狂的document.write。瀏覽器無法預估裡面的內容,那就乾脆全部停住,等指令碼執行完再幹活就好了。

對此的優化其實也很顯而易見,具體分為兩類。如果JS檔案體積太大,同時你確定沒必要阻塞DOM解析的話,不妨按需要加上defer或者async屬性,此時指令碼下載的過程中是不會阻塞DOM解析的。

而如果是檔案執行時間太長,不妨分拆一下程式碼,不用立即執行的程式碼,可以使用一下以前的黑科技:setTimeout()。當然,現代的瀏覽器很聰明,它會“偷看”之後的DOM內容,碰到如<link><script><img>等標籤時,它會幫助我們先行下載裡面的資源,不會傻等到解析到那裡時才下載。

瀏覽器遇到 <script> 標籤時,會觸發頁面渲染

這個細節可能不少看官大人並不清楚,其實這才是解釋上面為何JS執行會等待CSS下載的原因。先上例子,HTMLbody的結構如下:

<body>
	<div></div>
	<script src="/js/sleep3000-logDiv.js"></script>
	<style>
		div {
			background: lightgrey;
		}
	</style>
	<script src="/js/sleep5000-logDiv.js"></script>
	<link rel="stylesheet" href="/css/common.css">
</body>
複製程式碼

這個例子也是很極端的例子,但不妨礙它透露給我們很多重要的資訊。想象一下,頁面會怎樣呢?

答案是先淺綠色,再淺灰色,最後淺藍色。由此可見,每次碰到<script>標籤時,瀏覽器都會渲染一次頁面。這是基於同樣的理由,瀏覽器不知道指令碼的內容,因而碰到指令碼時,只好先渲染頁面,確保指令碼能獲取到最新的DOM元素資訊,儘管指令碼可能不需要這些資訊。

小結

綜上所述,我們得出這樣的結論:

  • CSS 不會阻塞 DOM 的解析,但會阻塞 DOM 渲染。
  • JS 阻塞 DOM 解析,但瀏覽器會"偷看"DOM,預先下載相關資源。
  • 瀏覽器遇到 <script>且沒有deferasync屬性的 標籤時,會觸發頁面渲染,因而如果前面CSS資源尚未載入完畢時,瀏覽器會等待它載入完畢在執行指令碼。

所以,你現在明白為何<script>最好放底部,<link>最好放頭部,如果頭部同時有<script><link>的情況下,最好將<script>放在<link>上面了嗎?

感謝各位看官大人看到這裡,希望本文對你有所幫助,有不同或更好意見的大佬,還望不吝賜教!謝謝~

相關文章