動態載入的樣式/指令碼對渲染的影響
- Home
- Programming >Front end >Javascript
- 動態載入的樣式/指令碼對渲染的影響
文章目錄
前言
在瀏覽器渲染過程和JS引擎淺析
這篇文章裡,我提到了用同步指令碼非同步載入(指令碼同步,但是請求是由渲染引擎的非同步請求執行緒非同步請求的)的外部資源會影響load
事件發生的時候,如果外部資源載入時間過長,那麼load
時間發生的時間也會推遲。但其實在指令碼中動態載入指令碼或者非同步請求外部資源還有一些細節可以挖掘。
非同步請求樣式表
在指令碼中非同步請求樣式或指令碼是不會阻塞DOM的解析和渲染,唯一影響的就是load
事件發生的時間以及load
事件發生之前的最後一次渲染。看下面這段程式碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Test</title> <script defer src="js/test.js"></script> </head> <body> <div id="test">test1</div> <script> let script = document.createElement("link"); script.setAttribute("rel", "stylesheet"); script.setAttribute( "href", 'https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css' ); var a = document.body.appendChild(script); console.log(123); window.onload = function() { console.log("window load..."); }; </script> <div>test2</div> </body> </html>
//test.js console.log('domcontentloaded');
通過設定瀏覽器的network throttling
(選擇slow 3g
就可以,或者你可以自定義到更慢,能夠區分出效果就可以),讓非同步請求的css
的載入時間變長,然後觀察之後的指令碼是否執行,指令碼之後的元素是否被渲染到頁面上。通過結果的觀察我們可以發現在css還沒有載入完成的時候console.log(123)
已經執行,並且test2
元素也已經被渲染到頁面上,defer標籤中的程式碼也已經執行,說明DOMContentLoaded
事件已經觸發,說明文件解析已經完成。當非同步的css請求到的時候,頁面會對新的css進行解析,然後對頁面進行load
之前的最後一次渲染,此時我們看到test1
和test2
的字型變了,隨後觸發load
事件。
同步新增內聯樣式表
如果我們用同步JS為文件新增style
標籤會怎麼樣呢,下面一段程式碼可以測試出結果:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Test</title> <script defer src="js/test.js"></script> </head> <body> <div id="test">test1</div> <script> var style = document.createElement("style"); style.textContent = "*{ color: red }"; document.body.appendChild(style); var el = document.getElementById("test"); console.log(window.getComputedStyle(el, null).color); window.onload = function() { console.log("window load..."); }; </script> <div>test2</div> </body> </html>
//test.js console.log('domcontentloaded');
輸出如下
rgb(255, 0, 0) domcontentloaded window load...
我們可以看到在頁面還沒有解析完成的時候我們獲取test1
元素的computedstyle
已經是rgb(255,0,0)
了,說明我們新增的style
標籤已經解析並渲染成功。也就是說同步的內聯樣式表是會阻塞解析和渲染的,不過同步的內聯樣式表基本不會產生延遲,也就不會影響頁面的效能。
非同步請求外部指令碼
在同步的JS中非同步請求外部指令碼也會在load
事件之前載入完成,但是對頁面解析和渲染的影響,我們看如下的程式碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Test</title> <script defer src="js/test.js"></script> </head> <body> <div id="test">test1</div> <script> var script = document.createElement("script"); script.src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/react.js"; document.body.appendChild(script); console.log("after script: ", window.React); window.onload = function() { console.log("window load..."); console.log("onload: ", window.React); }; </script> <div>test2</div> </body> </html>
//test.js console.log('domcontentloaded: ' + window.React);
輸出結果:
after script:undefined domcontentloaded: undefined window load... onload:{__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {…}, Children: {…}, Component: ƒ, PureComponent: ƒ, createElement: ƒ, …}
結果顯而易見,和非同步請求外部樣式表的效果相似,請求外部指令碼並不影響頁面的解析和渲染,也沒有阻塞後面的指令碼執行, 這意味著動態插入一個外部指令碼後不可立即使用其內容,需要等待載入完畢。在這段程式碼中,直到load
事件發生後,我們在onload
函式中才能呼叫外部指令碼中的變數。同時也佐證了load
事件發生之前會載入執行完所有的非同步請求的外部資源。
在DOM中插入內聯指令碼
這個直接說結論通過 DOM API(appendChild()、append()、before() 等等)插入的 script 元素,如果這個 script 元素沒有 src 屬性且 type 屬性不是 module,則這個 script 元素的 textContent 就會像你所說的那樣,立刻“同步”執行。關於這一點whatwg
的html
文件中有詳細說明4.12.1 The script element
,按照文件的理解,這個內聯的指令碼會立即被壓到執行棧的頂部立即執行,相當於包含指令碼內的函式一樣,具體看下面這段程式碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Test</title> <script defer src="js/test.js"></script> </head> <body> <div id="test">test1</div> <script> let script = document.createElement("script"); script.text = "console.log(1)"; document.body.appendChild(script); console.log(2); window.onload = function() { console.log("window load..."); console.log("onload: ", window.React); }; </script> <div>test2</div> </body> </html>
//test.js console.log('domcontentloaded');
輸出如下:
1 2 domcontentloaded window load...
從結果可以看書,新增的內聯script
立即執行了,比後面的console.log(1)
還要更早執行,文件中說的是Immediately execute the script block, even if other scripts are already executing.
,這種感覺就像是包含指令碼中的同步函式,執行到這裡立即壓棧執行。顯而易見,動態新增的內聯的指令碼是會阻塞頁面的解析和渲染的,和原本就在文件中的內聯指令碼並沒有不同,如果把script.test
換成如下程式碼:
script.text = ` var now = new Date().getTime(); while(new Date().getTime() - now < 3000) { continue; } `;
我們可以看到頁面會白屏直到這段程式碼執行完成,後面的指令碼才能執行,文件才能繼續解析和渲染。
未連線的CSS/JS不會被載入
如果你建立了一個<link rel="stylesheet">
(或<script>
)但並未連線到DOM樹,那麼它不會被載入。 這是標準行為與瀏覽器實現方式無關,因此你可以放心地利用該特性。 該特性很容易測試,只需建立一個<link rel="stylesheet">
(或<script>
)標籤並檢視是否產生網路請求:
var link = document.createElement("link"); link.rel = "stylesheet"; link.href = "https://cdn.jsdelivr.net/npm/[email protected]/animate.css"; var script = document.createElement("script"); script.src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/react.js";
監聽非同步資源載入
我們可以利用document.onload
事件來監聽資源是否已經從外部載入完成,程式碼如下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Test</title> <script defer src="js/test.js"></script> </head> <body> <div id="test">test1</div> <script> var link = document.createElement("link"); link.rel = "stylesheet"; link.href = "https://cdn.jsdelivr.net/npm/[email protected]/animate.css"; var script = document.createElement("script"); script.src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/react.js"; document.body.appendChild(script); script.onload = function () { console.log('source loaded!'); } window.onload = function() { console.log("window load..."); }; </script> <div>test2</div> </body> </html>
//test.js console.log('domcontentloaded');
輸出結果如下:
domcontentloaded source loaded! window load...
如果你想直到具體載入事件還可以用new Date().getTime()
來檢視具體的毫秒數。
innerHTML
innerHTML
屬性可用來設定 DOM 內容,但不可用來插入並執行<script>
。 下面的內聯指令碼和外部指令碼都不會被執行:
document.body.innerHTML = '<script src="foo.js"></script>' document.body.innerHTML = '<script>console.log("foo")</script>'
在設定 innerHTML 時,瀏覽器會初始化一個新的 HTML Parser 來解析它。 只要與該 Parser 關聯的 DOM 啟用了 JavaScript(通常是啟用的),指令碼的 scripting flag 就為真, 但是即便如此,HTML 片段的解析過程中,指令碼是不會執行的 。
Create a new HTML parser, and associate it with the just created Document node. – 12.4 Parsing HTML fragments, WHATWGThe scripting flag can be enabled even when the parser was originally created for the HTML fragment parsing algorithm, even though script elements don’t execute in that case. – 12.2.3.5 Other parsing state flags, WHATWG
事實上,設定innerHTML和outerHTML都不執行指令碼,但document.write()是會同步執行的。
When inserted using the document.write() method, script elements execute (typically blocking further script execution or HTML parsing), but when inserted using innerHTML and outerHTML attributes, they do not execute at all. – 4.12.1 The script element WHATWG
總結
結合瀏覽器渲染過程和JS引擎淺析 和這篇文章,對於整個瀏覽器的渲染過程,和我們的程式碼會在哪個階段執行,是否會被阻塞應該都能有一個清晰的認識了,雖然扣了很多細節,不過這對於我們寫出更好的程式碼還是有幫助的。