原文地址: Modern Script Loading, 文章做者是Preact做者Jason Millerhtml
先簡單介紹一下模塊script(Module script)
, 它指的是現代瀏覽器支持經過<script type=module src=main.js></script>
來加載現代的ES6模塊. 現代瀏覽器對ES6現代語法有良好的支持,這意味着咱們能夠給這些現代瀏覽器提供更緊湊的‘現代代碼’,一方面能夠減少打包的體積,減小網絡傳輸的帶寬,另外還能夠提升腳本解析的效率和運行效率.前端
下圖來源於module/nomodule pattern
, 對比了模塊script和傳統(legacy) script的性能:react
體積對比:webpack
Version | Size (minified) | Size (minified + gzipped) |
---|---|---|
ES2015+ (main.mjs) | 80K | 21K |
ES5 (main.es5.js) | 175K | 43K |
解析效率:git
Version | Parse/eval time (individual runs) | Parse/eval time (avg) |
---|---|---|
ES2015+ (main.mjs) | 184ms, 164ms, 166ms | 172ms |
ES5 (main.es5.js) | 389ms, 351ms, 360ms | 367ms |
Ok,爲了兼容舊瀏覽器, module/nomodule pattern這篇文章介紹了一種module/nomodule 模式, 簡單說就是同時提供兩個script, 由瀏覽器來決定加載哪一個文件:es6
<script type="module" src="main.mjs"></script> <script nomodule src="main.es5.js"></script>
看起來很美好是吧? 現實是:中間存在一些瀏覽器,它們能夠識別模塊script
可是不認識nomodule
屬性, 這就致使了這些瀏覽器會同時加載這兩個文件(下文統一稱爲‘雙重加載’(over-fetching)).github
OK,正式進入正文. 給正確的瀏覽器交付正確代碼是一件棘手的事情。本文會介紹幾種方式, 來解決上述的問題:
給現代瀏覽器伺服'現代的代碼'對性能有很大的幫助。因此你應該針對現代瀏覽器提供包含更緊湊和優化的現代語法的Javascript包,同時又能夠保持對舊瀏覽器的支持web
現有的工具鏈的生態系統基本都是在module/nomodule模式
上整合的,它聲明式加載現代和傳統代碼(legacy code),即給瀏覽器提供兩個源代碼,讓它來本身來決定用哪一個:瀏覽器
<script type="module" src="/modern.js"></script> <script nomodule src="/legacy.js"></script>
然而現實老是給你當頭一棒,它沒咱們指望的那麼簡單直接。上述基於HTML的加載方式在Edge和Safari中會被同時加載!緩存
怎麼辦?咱們想依賴瀏覽器來交付不一樣的編譯目標,可是一些舊瀏覽器並不能優雅地支持這種簡潔的寫法。
首先,Safari 在10.1開始支持JS模塊, 但不支持nomodule屬性。值得慶幸的是,Sam找到了一種方法,能夠經過Safari 10和11中非標準的beforeload事件來模擬 nomodule, 也就是能夠認爲Safari 10.1開始是能夠支持module/nomodule模式
咱們能夠實現一個小型script加載器來規避這個問題,工做原理相似於LoadCSS。只不過這裏須要依靠瀏覽器的來實現ES模塊和nomodule屬性.
咱們首先嚐試執行一個模塊script進行'石蕊試驗'(litmus test), 而後由這個試驗的結果來決定加載現代代碼仍是傳統代碼:
<script type=module> self.modern = true </script> <script> addEventListener('load', function() { var s = document.createElement('script') if (self.modern) { s.src = '/modern.js' s.type = 'module' } else { s.src = '/legacy.js' } document.head.appendChild(s) }) </script>
然而,這個解決方案必須等待進行‘石蕊試驗’模塊script執行完成, 才能開始注入script。這是由於<script type=module>
始終是異步的,因此別無它法(延遲到load事件後)。
另外一種實現方式是檢查瀏覽器是否支持nomodule
, 這是方式能夠避免上述的延遲加載問題, 只不過這意味着像Safari 10.1這些支持模塊, 卻不支持nomodule的瀏覽器也會被當作傳統瀏覽器,這也許可能是好事(相對於兩個腳本都加載以及有一些bug),代碼以下:
var s = document.createElement('script') if ('noModule' in s) { // 注意這裏的大小寫 s.type = 'module' s.src = '/modern.js' } else s.src = '/legacy.js' } document.head.appendChild(s)
如今把它們封裝成函數,並確保兩種方式都統一使用異步的方式加載(上文提到模塊script是異步的,而傳統script不是):
<script> $loadjs("/modern.js","/legacy.js") function $loadjs(src,fallback,s) { s = document.createElement('script') if ('noModule' in s) s.type = 'module', s.src = src else s.async = true, s.src = fallback // 統一使用異步方式加載 document.head.appendChild(s) } </script>
看起來已經很完美了,還有什麼問題呢?咱們還沒考慮預加載(preloading)
這個有點蛋疼, 由於通常瀏覽器只會靜態地掃描HTML,而後查找它能夠預加載的資源。 咱們上面介紹的模塊加載器是徹底動態的,因此瀏覽器在沒有運行咱們的代碼以前,是沒辦法發現咱們要預加載現代仍是傳統的Javascript資源的。
不過有一個解決辦法,就是不完美:就是使用<link rel=modulepreload>
來預加載現代版本的包, 舊瀏覽器會忽略這條規則,然而目前只有Chrome支持這麼作:
<link rel="modulepreload" href="/modern.js"> <script type=module>self.modern=1</script>
其實預加載這種技術是否有效,取決於嵌入你的腳本的HTML文檔的大小。
若是你的HTML載荷很小, 好比只是一個啓動屏或者只是簡單啓動客戶端應用,那麼放棄預加載掃描對你的應用性能影響很小。
若是你的應用使用服務器渲染大量有意義的HTML, 並以流(stream)的方式傳輸給瀏覽器,那麼預加載掃描就是你的朋友,但這也未必是最佳方法。
譯註: 現代瀏覽器都支持分塊編碼傳輸,等服務端徹底輸出html可能有一段空閒時間,這時候能夠經過預加載技術,讓瀏覽器預先去請求資源
大概代碼以下:
<link rel="modulepreload" href="/modern.js"> <script type=module>self.modern=1</script> <script> $loadjs("/modern.js","/legacy.js") function $loadjs(e,d,c){c=document.createElement("script"),self.modern?(c.src=e,c.type="module"):c.src=d,document.head.appendChild(c)} </script>
還要指出的是,支持JS模塊的瀏覽器通常也支持<link rel = preload>
。對於某些網站,相比依靠modulepreload
, 使用<link rel=preload as=script crossorigin>
可能更有意義。不過性能上面可能欠點,由於傳統的腳本預加載不會像modulepreload
同樣隨着時間的推移而去展開解析工做(rel=preload
只是下載,不會嘗試去解析腳本)。
我辦法拿出一個簡潔的代碼示例,由於用戶代理檢測不在本文的範圍以內,推薦閱讀這篇Smashing Magazine文章
本質上,這種技術在每一個瀏覽器上都使用<script src=bundle.js>
來加載代碼,當bundle.js
被請求時,服務器會解析瀏覽器的用戶代理,並選擇返回現代代碼仍是傳統代碼,取決於瀏覽器是否能被識別爲現代瀏覽器.
儘管這種方法比較通用,但它也有一些嚴重的缺點:
解決這些限制的一種方法就是將module/nomodule模式
與'用戶代理區分'結合起來,首先這能夠避免單純的module/nomodule模式
須要發送多個軟件包問題,儘管這種方法仍然會下降頁面(這時候指HTML,而不是Javascript包)的可緩存性,可是它能夠有效地觸發預加載,由於生成HTML的服務器根據用戶代理知道應該使用modulepreload
仍是preload
:
function renderPage(request, response) { let html = `<html><head>...`; const agent = request.headers.userAgent; const isModern = userAgent.isModern(agent); if (isModern) { html += ` <link rel=modulepreload href=modern.mjs> <script type=module src=modern.mjs></script> `; } else { html += ` <link rel=preload as=script href=legacy.js> <script src=legacy.js></script> `; } response.end(html); }
對於那些已經在使用服務端渲染的網站來講,用戶代理嗅探是一個比較有效的解決方案
注意這裏的‘舊版本瀏覽器’特指那些出現雙重加載的瀏覽器. 對於module/nomodule模式
支持比較差(即雙重加載)的主要是一些舊版本的Chrome、Firefox和Safari. 幸運的是這部分瀏覽器的市場範圍一般是比較窄,由於用戶會自動升級到最新的版本。Edge 16-18是例外, 但還有但願: 新版本的Edge會使用基於Chromium的渲染器,能夠不受該問題的影響.
對於某些應用程序來講,接受這一點妥協是徹底合理的:你能夠給90%的瀏覽器中提供現代代碼,讓他們得到更好的體驗,而極少數舊瀏覽器不得不拋棄它們,它們只是付出的額外帶寬(即雙重加載),並不影響功能。值得注意的是,佔據移動端主要市場份額的用戶代理不會有雙重加載問題,因此這些流量不太可能來自於低速或者高昂流量費的手機。
若是你的網站用戶主要使用移動設備或較新版本的瀏覽器,那麼最簡單的module/nomodule
模式將適用於你的絕大多數用戶, 其餘用戶就不考慮了,反正也是能夠跑起來的, 優先考慮大多數用戶的體驗。
<script type=module> !function(e,t,n){!("noModule"in(t=e.createElement("script")))&&"onbeforeload"in t&&(n=!1,e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove())}(document) </script> <script src=modern.js type=module></script> <script src=legacy.js nomodule async defer></script>
nomodule
能夠巧妙地用來條件加載那些現代瀏覽器不須要的代碼, 例如polyfills。經過這種方法,最壞的狀況就是polyfill和bundle都會被加載(例如Safari 10.1),但這畢竟是少數。鑑於目前通行的作法就是在全部瀏覽器中一致同仁地加載polyfills,相比而言, 條件polyfills可讓大部分現代瀏覽器用戶避免加載polyfill代碼。
<script nomodule src="polyfills.js"></script> <script src="/bundle.js"></script>
Angular CLI支持配置這種方式來加載polyfill, 查看Minko Gechev的代碼示例.
瞭解了這種方式以後,我決定在preact-cli中支持自動polyfill注入,你能夠查看這個PR
若是你使用Webpack,這裏有一個html-webpack-plugin插件能夠方便地爲polyfill包添加nomodule
屬性.
答案取決於你的使用場景, 選擇和大家的架構匹配的選項:
若是你的應用只是客戶端渲染, 並且你的HTML不超過一個<script>
,選項1比較合適;
若是你的應用使用服務端渲染,並且能夠接受緩存問題,那麼能夠選擇選項2;
若是你開發的是同構應用,預加載的功能可能對你很重要,這時你能夠考慮選項3和4.
就我我的而言,相比考慮桌面端瀏覽器資源下載成本,我更傾向於優化移動設備解析時間. 移動用戶體驗會受到數據解析、流量費用,電池消耗等因素的影響,而桌面用戶每每不須要考慮這些因素。
另外這些優化適用於90%的用戶,好比我工做面對的大部分用戶都是使用現代或移動瀏覽器的。
有興趣繼續深刻?能夠從下面的文章開始挖掘: