【瀏覽器渲染原理系列】這是一份關(guān)于加載script和style的實(shí)驗(yàn)報(bào)告
?如果做性能優(yōu)化,一定會(huì)想當(dāng)?shù)囊粋€(gè)優(yōu)化點(diǎn)就是script標(biāo)簽和link標(biāo)簽要放置的位置,當(dāng)然大部分的觀點(diǎn)都是script標(biāo)簽放到</body>之前,link標(biāo)簽放到title中;或者是配合async、defer、preload、prefech使用,當(dāng)然目的只有一個(gè):讓頁(yè)面盡可能快的展示在用戶面前。下面僅僅會(huì)討論瀏覽器獲取到HTML文件后的部分。
?
瀏覽器渲染過(guò)程
?瀏覽器獲取到HTML文件后,開(kāi)始渲染工作。這里以webkit引擎為例。
解析html產(chǎn)生DOM樹(shù) 解析css樣式產(chǎn)生CSSOM樹(shù) DOM樹(shù)和CSSOM樹(shù)合成渲染樹(shù)(RenderTree) 布局RenderTree(layout):確定在屏幕中位置 繪制(paint) 合成(composite): 將多個(gè)圖層合并 為了提高用戶體驗(yàn),渲染引擎會(huì)盡快的把結(jié)果渲染給用戶,它不會(huì)等待所有的html都解析完成才渲染,它會(huì)在網(wǎng)絡(luò)層獲取文檔的同時(shí),將已經(jīng)接收的局部渲染到頁(yè)面中(實(shí)驗(yàn)4將會(huì)證明這一說(shuō)法)
?
實(shí)驗(yàn)環(huán)境準(zhǔn)備
1. 模擬服務(wù)端: 所需要的文件hand.js、style.css、server.js
// server.js
const http = require('http');
const fs = require('fs')
http.createServer(function (request, response) {
if (request.url === '/index.html') {
fs.readFile('./index.html', (err, data) => {
response.setHeader('Content-Type', 'text/html');
if (!err) {
response.end(data);
}else {
response.end('html not found');
}
})
}
if (request.url === '/hand.js') {
fs.readFile('./static/hand.js', (err, data) => {
response.setHeader('Content-Type', 'text/javascript');
if (!err) {
setTimeout(() => {
response.end(data);
}, 100);
}else {
response.end('html not found');
}
})
}
if (request.url === '/style.css') {
fs.readFile('./static/style.css', (err, data) => {
response.setHeader('Content-Type', 'text/css');
if (!err) {
setTimeout(() => {
response.end(data);
}, 1000);
}else {
response.end('html not found');
}
})
}
if (request.url === '/favicon.ico') {
response.end()
}
}).listen(8888);
console.log('port 8888')
// hand.js(無(wú)內(nèi)容)
// style.css
p {
color: red;
}
前端部分
<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<p>我是第一個(gè)</p>
<p>我是第二個(gè)</p>
<p>我是第三個(gè)</p>
</body>
</html>
備注:以下的實(shí)驗(yàn)圖片中,請(qǐng)忽略掉實(shí)驗(yàn)1、3、8、9、10報(bào)告的第一個(gè)渲染幀(因?yàn)闉g覽器會(huì)紀(jì)錄頁(yè)面刷新前的一個(gè)渲染幀)
script標(biāo)簽
為什么script標(biāo)簽會(huì)阻塞頁(yè)面的渲染?
?javascript能操作dom樹(shù),瀏覽器卻不知道腳本中是否有操作dom的代碼(比如document.write等),所以以最壞的打算來(lái)處理:停止dom的解析,所以更準(zhǔn)確的說(shuō)是「script標(biāo)簽會(huì)阻止dom的解析」
解釋幾個(gè)下面實(shí)驗(yàn)用到的名詞(以下解釋均來(lái)源于MDN)
DCL(DOMContentLoaded): 當(dāng)HTML被完全加載以及「解析」時(shí),DOMContentLoaded事件會(huì)被觸發(fā),而不必等待樣式表,圖片或者子框架完成加載
L(load): 當(dāng)整個(gè)頁(yè)面及所有依賴資源如「樣式表和圖片都已完成加載」時(shí),將觸發(fā)load事件
FP(first paint): 頁(yè)面導(dǎo)航與瀏覽器將該網(wǎng)頁(yè)的第一個(gè)像素「渲染」到屏幕上
以上名詞縮寫(xiě)將會(huì)出現(xiàn)在下面的實(shí)驗(yàn)截圖中
?
實(shí)驗(yàn)1: 內(nèi)聯(lián)script標(biāo)簽
<head>
<meta charset="UTF-8">
<script>
var a = 0
for (let i = 0; i< 1000000000; i++) {
a += 1
}
</script>
</head>
<body>
<p>我是第一個(gè)</p>
</body>

<head>
<meta charset="UTF-8">
</head>
<body>
<p>我是第一個(gè)</p>
<script>
var a = 0
for (let i = 0; i< 1000000000; i++) {
a += 1
}
</script>
</body>

實(shí)驗(yàn)2: 內(nèi)聯(lián)script標(biāo)簽(換一種實(shí)現(xiàn)方式)
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
const node = document.getElementsByTagName('p')
console.log(node[0]) // undefined
</script>
<p>我是第一個(gè)</p>
</body>
<head>
<meta charset="UTF-8">
</head>
<body>
<p>我是第一個(gè)</p>
<script>
const node = document.getElementsByTagName('p')
console.log(node[0]) // <p>我是第一個(gè)</p>
</script>
</body>
實(shí)驗(yàn)3: 外部引入的script標(biāo)簽放在title中
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="http://localhost:8888/hand.js"></script>
</head>
<body>
<p>我是第一個(gè)</p>
</body>
</html>

實(shí)驗(yàn)4: 外部引入的script標(biāo)簽放在body中
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<p>我是第一個(gè)</p>
<p>我是第二個(gè)</p>
<script src="http://localhost:8888/hand.js"></script>
<p>我是第三個(gè)</p>
</body>
</html>

實(shí)驗(yàn)5: 外部引入的script標(biāo)簽放在</body>前
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<p>我是第一個(gè)</p>
<p>我是第二個(gè)</p>
<p>我是第三個(gè)</p>
<script src="http://localhost:8888/hand.js"></script>
</body>
</html>

實(shí)驗(yàn)6: 外部引入的script標(biāo)簽放在title中,并且加入async參數(shù)
<head>
<meta charset="UTF-8">
<script src="http://localhost:8888/hand.js" async></script>
</head>
<body>
<p>我是第一個(gè)</p>
<p>我是第二個(gè)</p>
<p>我是第三個(gè)</p>
</body>

實(shí)驗(yàn)7: 外部引入的script標(biāo)簽放在title中,并且加入defer參數(shù)
<head>
<meta charset="UTF-8">
<script src="http://localhost:8888/hand.js" defer></script>
</head>
<body>
<p>我是第一個(gè)</p>
<p>我是第二個(gè)</p>
<p>我是第三個(gè)</p>
</body>

?總結(jié):
?
內(nèi)聯(lián)的script會(huì)阻塞dom解析,并且不會(huì)使之前解析過(guò)的dom預(yù)先渲染 外部引入的script標(biāo)簽會(huì)阻塞dom的解析,但是之前解析過(guò)的dom瀏覽器會(huì)先渲染 加入async和defer可以強(qiáng)制script標(biāo)簽不去阻塞dom的解析 defer會(huì)阻塞DOMContentLoaded事件,而async不會(huì)
link標(biāo)簽引入的css
?我們一般用link標(biāo)簽引用css樣式文件。如果你看過(guò)vue打包后的文件,會(huì)發(fā)現(xiàn)它的一些腳本文件也是通過(guò)link標(biāo)簽引入的。不過(guò)我們這篇文章中不對(duì)其進(jìn)行討論。「樣式文件不會(huì)阻塞dom的解析,但是會(huì)阻塞dom的渲染」,接下來(lái)用幾個(gè)實(shí)驗(yàn)來(lái)證明css是如何阻塞dom的渲染的
?
實(shí)驗(yàn)8: link標(biāo)簽引入的css放到title中
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="http://localhost:8888/style.css">
</head>
<body>
<p>我是第一個(gè)</p>
<p>我是第二個(gè)</p>
<p>我是第三個(gè)</p>
</body>

實(shí)驗(yàn)9: link標(biāo)簽引入的css放到</body>前
<head>
<meta charset="UTF-8">
</head>
<body>
<p>我是第一個(gè)</p>
<p>我是第二個(gè)</p>
<p>我是第三個(gè)</p>
<link rel="stylesheet" href="http://localhost:8888/style.css">
</body>

?總結(jié):
?
css不會(huì)阻塞dom的解析,但是會(huì)阻塞dom的渲染 css若放在body末尾,頁(yè)面會(huì)從無(wú)樣式到有樣式的過(guò)渡,會(huì)讓用戶體驗(yàn)很差
script與css的標(biāo)簽同時(shí)存在
?一個(gè)靜態(tài)文件中一定會(huì)同時(shí)存在script和link標(biāo)簽的情況,它們之間又是互相影響的?
?
實(shí)驗(yàn)10: link與script都放在title中,且link放在script之前
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="http://localhost:8888/style.css" />
<script src="http://localhost:8888/hand.js"></script>
</head>
<body>
<p>我是第一個(gè)</p>
<p>我是第二個(gè)</p>
<p>我是第三個(gè)</p>
</body>

實(shí)驗(yàn)11: link與script都放在title中,且link放在script之后
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="http://localhost:8888/hand.js"></script>
<link rel="stylesheet" href="http://localhost:8888/style.css" />
</head>
<body>
<p>我是第一個(gè)</p>
<p>我是第二個(gè)</p>
<p>我是第三個(gè)</p>
</body>

實(shí)驗(yàn)12: link在title中,script放到</body>前(情況1)
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="http://localhost:8888/style.css" />
</head>
<body>
<p>我是第一個(gè)</p>
<p>我是第二個(gè)</p>
<p>我是第三個(gè)</p>
<script src="http://localhost:8888/hand.js"></script>
</body>

實(shí)驗(yàn)13: link在title中,script放到</body>前(情況2)
// 讓我們修改一下serve.js文件中的js文件和css的返回時(shí)間
if (request.url === '/hand.js') {
fs.readFile('./static/hand.js', (err, data) => {
response.setHeader('Content-Type', 'text/javascript');
if (!err) {
setTimeout(() => {
response.end(data);
}, 1000);
}else {
response.end('html not found');
}
})
}
if (request.url === '/style.css') {
fs.readFile('./static/style.css', (err, data) => {
response.setHeader('Content-Type', 'text/css');
if (!err) {
setTimeout(() => {
response.end(data);
}, 100);
}else {
response.end('html not found');
}
})
}
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="http://localhost:8888/style.css" />
</head>
<body>
<p>我是第一個(gè)</p>
<p>我是第二個(gè)</p>
<p>我是第三個(gè)</p>
<script src="http://localhost:8888/hand.js"></script>
</body>

?總結(jié):
?
css不會(huì)阻塞外部腳本的加載,但是會(huì)阻塞js的執(zhí)行(GUI線程和js線程互斥,因?yàn)橛锌赡躩s會(huì)操作 CSS) 最佳實(shí)踐:script標(biāo)簽放在body的末尾,style標(biāo)簽放在body之前
參考文檔
https://developers.google.cn/web/fundamentals/performance/critical-rendering-path https://developers.google.cn/web/fundamentals/performance/rendering

