寫在最前
在部落格魔改過程中,不可避免的會引入大量的第三方腳本(js),而基于頁面讀取js的加載順序,每當浏覽器在加載html的過程中遇到
<script>js代碼片段</script>
這樣的标簽時,浏覽器會暫停繼續建構html,而是優先執行目前的js腳本,等執行完畢後再繼續加載後面的html。
至于外部腳本
<script src="js外鍊"></script>
這樣的寫法,更是要先下載下傳腳本,然後再執行,之後才能繼續處理剩餘的頁面。
無形中,多出了一大把的加載時間。這也是我們常說魔改是對部落格速度的負優化的原因之一。
是以可以通過給
<script></script>
添加
defer
和
ansyc
屬性來實作異步加載,調整js的加載時間和順序,確定浏覽器建構HTML的過程一切順利。
參考内容
- javascript.INFO——script-async-defer
- script中defer跟async是什麼?
- CSS異步加載最簡單的實作方式
- 異步加載CSS
原理分析
首先要清楚defer、async是什麼,有什麼差別。
defer
和
async
是
<script>
标簽的兩個屬性,用來控制js腳本的加載。
以下先引用參考教程的原文。
教程原文
defer
defer
特性告訴浏覽器不要等待腳本。相反,浏覽器将繼續處理
HTML
,建構
DOM
。腳本會在背景下載下傳,然後等
DOM
建構完成後,腳本才會執行。
這是與上面那個相同的示例,但是帶有
defer
特性:
<p>...content before script...</p>
<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
<!-- 立即可見 -->
<p>...content after script...</p>
複制
換句話說:
- 具有
特性的腳本不會阻塞頁面。defer
- 具有
特性的腳本總是要等到defer
解析完畢,但在DOM
DOMContentLoaded
事件之前執行。
下面這個示例示範了上面所說的第二句話:
<p>...content before scripts...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!"));
</script>
<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
<p>...content after scripts...</p>
複制
- 頁面内容立即顯示。
-
事件處理程式等待具有DOMContentLoaded
defer
特性的腳本執行完成。它僅在腳本下載下傳且執行結束後才會被觸發。
具有
特性的腳本保持其相對順序,就像正常腳本一樣。defer
假設,我們有兩個具有
defer
特性的腳本:
long.js
在前,
small.js
在後。
<script defer src="https://javascript.info/article/script-async-defer/long.js"></script>
<script defer src="https://javascript.info/article/script-async-defer/small.js"></script>
複制
浏覽器掃描頁面尋找腳本,然後并行下載下傳它們,以提高性能。是以,在上面的示例中,兩個腳本是并行下載下傳的。
small.js
可能會先下載下傳完成。
……但是,
defer
特性除了告訴浏覽器不要阻塞頁面之外,還可以確定腳本執行的相對順序。是以,即使
small.js
先加載完成,它也需要等到
long.js
執行結束才會被執行。
當我們需要先加載
JavaScript
庫,然後再加載依賴于它的腳本時,這可能會很有用。
defer
特性僅适用于外部腳本
如果
<script>
腳本沒有
src
,則會忽略
defer
特性。
async
async
特性與
defer
有些類似。它也能夠讓腳本不阻塞頁面。但是,在行為上二者有着重要的差別。
async
特性意味着腳本是完全獨立的:
- 浏覽器不會因
腳本而阻塞(與async
類似)。defer
- 其他腳本不會等待
腳本加載完成,同樣,async
腳本也不會等待其他腳本。async
-
和異步腳本不會彼此等待:DOMContentLoaded
-
可能會發生在異步腳本之前(如果異步腳本在頁面完成後才加載完成)DOMContentLoaded
-
也可能發生在異步腳本之後(如果異步腳本很短,或者是從DOMContentLoaded
HTTP
緩存中加載的)
換句話說,
腳本會在背景加載,并在加載就緒時運作。async
和其他腳本不會等待它們,它們也不會等待其它的東西。DOM
腳本就是一個會在加載完成時執行的完全獨立的腳本。就這麼簡單,現在明白了吧?async
-
下面是一個類似于我們在講
defer
時所看到的例子:
long.js
和
small.js
兩個腳本,隻是現在
defer
變成了
async
。
它們不會等待對方。先加載完成的(可能是
small.js
)—— 先執行:
<p>...content before scripts...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM ready!"));
</script>
<script async src="https://javascript.info/article/script-async-defer/long.js"></script>
<script async src="https://javascript.info/article/script-async-defer/small.js"></script>
<p>...content after scripts...</p>
複制
頁面内容立刻顯示出來:加載寫有的腳本不會阻塞頁面渲染。
DOMContentLoadedasync
較小的腳本排在第二位,但可能會比這個長腳本先加載完成,是以會先執行。雖然,可能是先加載完成,如果它被緩存了的話,那麼它就會先執行。換句話說,異步腳本以的順序執行。當我們将獨立的第三方腳本內建到頁面時,此時采用異步加載方式是非常棒的:,等,因為它們不依賴于我們的腳本,我們的腳本也不應該等待它們:
原理拆解
按照上面的原理我畫了一張圖來解釋加載順序和對HTML加載時間的影響。
可以看到,總的HTML加載時間,下載下傳腳本的時間,執行腳本的時間是固定的。不同之處在于HTML阻塞的時間以及執行腳本的次序。
- 不加任何
和async
defer
的情況,頁面總加載時間最長,是
HTML加載時間+下載下傳腳本時間+執行腳本時間
- 加了
和async
defer
的時間,在加載HTML時間足夠長的情況下,所有靜态資源總的加載時間都是
HTML加載時間+執行腳本時間
适用内容
然後,必須考慮到頁面加載時間和腳本加載順序,以及各個腳本直接存在的依賴關系。
從參考文檔來看,
-
特性除了告訴浏覽器不要阻塞頁面之外,還可以確定腳本執行的相對順序。defer
- 這個很适合使用到Vue和jquery等js架構的js腳本,給它們添加
屬性以後,可以確定HTML加載完畢,且js下載下傳完畢後,各個js腳本繼續按照引入的順序執行,進而確定不會因為依賴缺失而報錯。defer
- 這個很适合使用到Vue和jquery等js架構的js腳本,給它們添加
- 其他腳本不會等待
腳本加載完成,同樣,async
腳本也不會等待其他腳本。async
- 這個适合使用原生js,沒有引用任何js架構,自己獨立就能運作,且體量相對較小的js腳本,随頁面加載同步下載下傳執行。
使用範例
此處以我使用的
Butterfly
主題中添加的幾個js為例。
這裡首先說一下我個人的想法,不一定對,本着盡可能減少頁面阻塞資源的情況下,我建議:
- 盡可能給每個script标簽都加上
和defer
。async
- 确定為獨立腳本或原生腳本的情況下,使用
。這一條并不适用于大型js(例如busuanzi.js)。原因可以看上面的原理拆解圖,大型js的執行時間依然會造成大面積的HTML阻塞。async
- 如果實在不清楚應該添加哪個,則以
求穩,確定依賴項不會缺失。defer
- 總的來說,
加在那些非必要的,起裝飾或者優化效果的js上,async
加在那些確定頁面完整性的必要js上。defer
- 來自Heo的建議,不要給影響頁面生成的js(例如
)添加異步加載标簽(不論是util.js、main.js、lazy_load.js、vue.js、jquery.js
還是async
都不要加),不然會造成大面積頁面功能子產品失效。推測是由于部分HTML元素需要由js動态寫入,抑或部分整合在各個defer
中的pug
片段無法添加script
導緻。defer
inject配置項
bottom:
- <script defer https://cdn.jsdelivr.net/npm/[email protected]"></script>
# Vue.js作為依賴項,必須確定在所有使用到它提供的方法的諸多js之前引入,并且添加defer
- <script defer src="/live2d-widget/autoload.js"></script>
# 看闆娘使用到了jquery依賴,是以需要defer
- <script async src="/js/dytitle.js"></script>
# 動态網頁标題是原生js,且體量小,可以直接async
- <script async src="/js/mouse_snow.min.js"></script>
# 滑鼠滑動雪花是原生js,且體量小,可以直接async
- <script defer data-pjax src="/clock/js/clock.js"></script>
# 首頁電子鐘用到了vue依賴,是以需要defer
- <script defer data-pjax src="/magnet/catalogMagnet.js"></script>
# 首頁磁貼用到了vue依賴,是以需要defer
- <script defer src="/botui/botui.js"></script>
- <script defer data-pjax src="/botui/botui_init.js"></script>
# 側欄聊天窗用到了vue依賴,是以需要defer
- <script defer src="/runtime/flipcountdown.js"></script>
- <script defer data-pjax src="/runtime/runtime.js"></script>
# 頁腳計時器用到了jquery依賴,是以需要defer
- <script defer src="https://cdn.jsdelivr.net/npm/artitalk"></script>
- <script defer data-pjax src="/artitalk/artitalkkey.js"></script>
- <script defer data-pjax src="/artitalk/artitalk.js"></script>
# 側欄說說依賴于artitalk.js,必須確定其加載順序,在保證頁面引入順序的同時添加defer
- <script async src="/js/redirect.js"></script>
- <script async src="/js/mirror.js"></script>
# 404和鏡像站重定向都是原生js,可以直接async
- <script defer data-pjax src="https://cdn.jsdelivr.net/gh/Akilarlxh/[email protected]_3/gitcalendar/js/gitcalendar.js"></script>
# gitcalendar用到了vue依賴,是以需要defer
複制
CDN配置項
CDN配置項的引入先于inject,至于如何給script标簽添加
defer
和
async
屬性,則要先找到引入位置。
在
[Blogroot]\themes\butterfly\layout\includes\additional-js.pug
中修改。
div
script(src=url_for(theme.CDN.jquery))
script(src=url_for(theme.CDN.utils))
script(src=url_for(theme.CDN.main))
if theme.translate && theme.translate.enable
script(defer src=url_for(theme.CDN.translate))
if theme.medium_zoom
script(defer src=url_for(theme.CDN.medium_zoom))
else if theme.fancybox
script(defer src=url_for(theme.CDN.fancybox))
if theme.instantpage
script(src=url_for(theme.CDN.instantpage) type="module" defer)
if theme.lazyload.enable
script(src=url_for(theme.CDN.lazyload))
if (theme.snackbar && theme.snackbar.enable)
script(defer src=url_for(theme.CDN.snackbar))
if theme.pangu && theme.pangu.enable
!=partial('includes/third-party/pangu.pug', {}, {cache:theme.fragment_cache})
//- search
if theme.algolia_search.enable
script(defer src=url_for(theme.CDN.algolia_js))
else if theme.local_search.enable
script(defer src=url_for(theme.CDN.local_search))
複制
拓展内容,CSS的異步加載
頁面載入并渲染的流程可以簡單了解為以下情況:
加載HTML資源->解析HTML->加載CSS資源,同時建構DOM樹->解析CSS,同時渲染DOM樹
與js的加載執行過程十分相似,加載CSS時也會造成HTML加載阻塞。
既然js可以異步加載,那CSS理論上應該也可以。雖然不能像js一樣直接通過
async
和
defer
來定義加載順序那麼友善。
目前有效手段有兩種,一種是通過定義一個無效media,使得該CSS引入優先級最低,再用
onload="this.media='all'"
在頁面加載完成後糾正media,并加載CSS。
寫法如下:
未加入異步加載:
<link rel="stylesheet" href="/example.css">
複制
加入異步加載後:
<link rel="stylesheet" href="/example.css" media="defer" onload="this.media='all'">
複制
注意此處的media=”defer”中的defer并無意義,是個無效media(隻是友善了解才這麼寫),目的是讓浏覽器認為這個CSS檔案優先級非常低,進而在不阻塞的情況下進行加載。
加載完成以後,樣式轉為對所有裝置生效是
onload="this.media='all'"
,但是其實還有其他的裝置可供選擇。
值 | 描述 |
---|---|
screen | 計算機螢幕(預設)。 |
tty | 電傳打字機以及類似的使用等寬字元網格的媒介。 |
tv | 電視機類型裝置(低分辨率、有限的滾屏能力)。 |
projection | 放映機。 |
handheld | 手持裝置(小螢幕、有限帶寬)。 |
列印預覽模式/列印頁面。 | |
braille | 盲人點字法回報裝置。 |
aural | 語音合成器。 |
all | 适用于所有裝置。 |
還有一種利用
rel="preload"
屬性的方案,但是目前隻有Chrome浏覽器可以完美支援,等推廣還需要很長一段時間,寫法如下:
<link rel="preload" href="cssfile.css" as="style" onload="this.rel='stylesheet'">
複制
CSS整合
以下内容僅針對butterfly主題。其他主題可以了解原理後進行操作。實際上就是使用@import引入自定義樣式。
相信很多小夥伴在看了上述的CSS異步加載方案後,肯定迫不及待的去給自己部落格的魔改自定義樣式添加異步加載屬性了,就算不是,現在也給我演一下,配合我的工作,這麼做雖然可以減少HTML頁面阻塞,但是很可能會出現首屏頁面有那麼幾秒鐘存在大片無樣式的闆塊的情況。
是以我們可以确立一條原則,為了追求視覺體驗,不要給
index.css
等涉及首頁樣式的CSS檔案添加異步加載。
然而事實上,相比于給CSS添加異步加載,不如将我們的魔改樣式整合到
index.css
檔案内,減少對伺服器的請求次數。這樣更能節省加載時間。
我的做法如下:
在
[Blogroot]\themes\butterfly\source\css\
路徑下建立
_custom
檔案夾,然後把魔改樣式的
CSS
檔案拖動進去。檔案目錄層級可以表示為以下情況:
在
[[Blogroot]\themes\butterfly\source\css\index.styl
中新增一行代碼:
@import '_custom/*.css'
,表示引入
_custom
檔案夾下的所有CSS檔案。
如果是使用的外鍊css,也可以在這裡引入。同樣是修改
[Blogroot]\themes\butterfly\source\css\index.styl
代碼,使用
@import
逐行引入:
這樣一來,每個魔改方案的CSS依然可以在獨立的CSS檔案中找到并修改(如果是手動添加整合的話,隻能用注釋分割,顯然很不利于後續查找修改),而在每次送出時,運作hexo g的過程中就會将所有CSS檔案都整合到
index.css
,可以在主題配置檔案的CDN配置項裡給
index.css
加上
jsdelivr
進一步提升加載速度(注意重新整理jsdelivr的緩存)。