CSS 與 JS 是這樣阻塞 DOM 解析和渲染的
估計大家都聽過,儘量將 CSS 放頭部,JS 放底部,這樣可以提高頁面的效能。然而,為什麼呢?大家有考慮過麼?很長一段時間,我都是知其然而不知其所以然,強行背下來應付考核當然可以,但實際應用中必然一塌糊塗。因此洗(wang)心(yang)革(bu)面(lao),小結一下最近玩出來的成果。
友情提示,本文也是小白向為主,如果直接想看結論可以拉到最下面看的~
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>
我會在其中插入不同的 JS 和 CSS。
而使用的 common.css,不論有沒有字首,內容都是這樣的:
div {background: red;}
好了,話不多數,開始正文!
CSS
關於 CSS,大家肯定都知道的是<link>標籤放在頭部效能會高一點,少一點人知道如果<script>與<link>同時在頭部的話,<script>在上可能會更好。這是為什麼呢?下面我們一起來看一下 CSS 對 DOM 的影響是什麼。
CSS 不會阻塞 DOM 的解析
注意哦!這裡說的是 DOM 解析,證明的例子如下,首先在頭部插入<script defer src="/js/logDiv.js"></script>,JS 檔案的內容是:
const div = document.querySelecot('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 Tree 和 CSS 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 下載的原因。先上例子,HTML 內 body 的結構如下:
<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>且沒有 defer 或 async 屬性的 標籤時,會觸發頁面渲染,因而如果前面 CSS 資源尚未載入完畢時,瀏覽器會等待它載入完畢在執行指令碼。
所以,你現在明白為何<script>
最好放底部,<link>
最好放頭部,如果頭部同時有<script>
與<link>
的情況下,最好將<script>
放在<link>
上面了嗎?
結語
感謝您的觀看,如有不足之處,歡迎批評指正。自己是從事了五年的前端工程師,不少人私下問我,2019年前端該怎麼學,方法有沒有?
這裡推薦一下我的前端學習交流圈:784783012 ,裡面都是學習前端的從最基礎的HTML+CSS+JS【炫酷特效,遊戲,外掛封裝,設計模式】到移動端HTML5的專案實戰的學習資料都有整理,送給每一位前端小夥伴。最新技術,與企業需求同步。好友都在裡面學習交流,每天都會有大牛定時講解前端技術!
點選:加入