摘要:鑒于目前通行的做法就是在所有瀏覽器中一致同仁地加載,相比而言條件可以讓大部分現(xiàn)代瀏覽器用戶避免加載代碼。
原文地址: Modern Script Loading, 文章作者是Preact作者Jason Miller
背景知識先簡單介紹一下模塊script(Module script), 它指的是現(xiàn)代瀏覽器支持通過來加載現(xiàn)代的ES6模塊. 現(xiàn)代瀏覽器對ES6現(xiàn)代語法有良好的支持,這意味著我們可以給這些現(xiàn)代瀏覽器提供更緊湊的‘現(xiàn)代代碼’,一方面可以減小打包的體積,減少網絡傳輸的帶寬,另外還可以提高腳本解析的效率和運行效率.
下圖來源于module/nomodule pattern, 對比了模塊script和傳統(tǒng)(legacy) script的性能:
體積對比:
Version | Size (minified) | Size (minified + gzipped) |
---|---|---|
ES2015+ (main.mjs) | 80K | 21K |
ES5 (main.es5.js) | 175K | 43K |
解析效率:
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, 由瀏覽器來決定加載哪個文件:
看起來很美好是吧? 現(xiàn)實是:中間存在一些瀏覽器,它們可以識別模塊script但是不認識nomodule屬性, 這就導致了這些瀏覽器會同時加載這兩個文件(下文統(tǒng)一稱為‘雙重加載’(over-fetching)).
OK,正式進入正文. 給正確的瀏覽器交付正確代碼是一件棘手的事情。本文會介紹幾種方式, 來解決上述的問題:
給現(xiàn)代瀏覽器伺服"現(xiàn)代的代碼"對性能有很大的幫助。所以你應該針對現(xiàn)代瀏覽器提供包含更緊湊和優(yōu)化的現(xiàn)代語法的Javascript包,同時又可以保持對舊瀏覽器的支持
現(xiàn)有的工具鏈的生態(tài)系統(tǒng)基本都是在module/nomodule模式上整合的,它聲明式加載現(xiàn)代和傳統(tǒng)代碼(legacy code),即給瀏覽器提供兩個源代碼,讓它來自己來決定用哪個:
然而現(xiàn)實總是給你當頭一棒,它沒我們期望的那么簡單直接。上述基于HTML的加載方式在Edge和Safari中會被同時加載!
怎么辦?怎么辦?我們想依賴瀏覽器來交付不同的編譯目標,但是一些舊瀏覽器并不能優(yōu)雅地支持這種簡潔的寫法。
首先,Safari 在10.1開始支持JS模塊, 但不支持nomodule屬性。值得慶幸的是,Sam找到了一種方法,可以通過Safari 10和11中非標準的beforeload事件來模擬 nomodule, 也就是可以認為Safari 10.1開始是可以支持module/nomodule模式
選項1: 動態(tài)加載我們可以實現(xiàn)一個小型script加載器來規(guī)避這個問題,工作原理類似于LoadCSS。只不過這里需要依靠瀏覽器的來實現(xiàn)ES模塊和nomodule屬性.
我們首先嘗試執(zhí)行一個模塊script進行"石蕊試驗"(litmus test), 然后由這個試驗的結果來決定加載現(xiàn)代代碼還是傳統(tǒng)代碼:
然而,這個解決方案必須等待進行‘石蕊試驗’模塊script執(zhí)行完成, 才能開始注入script。這是因為始終是異步的,所以別無它法(延遲到load事件后)。
另一種實現(xiàn)方式是檢查瀏覽器是否支持nomodule, 這是方式可以避免上述的延遲加載問題, 只不過這意味著像Safari 10.1這些支持模塊, 卻不支持nomodule的瀏覽器也會被當做傳統(tǒng)瀏覽器,這也許可能是好事(相對于兩個腳本都加載以及有一些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)
現(xiàn)在把它們封裝成函數,并確保兩種方式都統(tǒng)一使用異步的方式加載(上文提到模塊script是異步的,而傳統(tǒng)script不是):
看起來已經很完美了,還有什么問題呢?我們還沒考慮預加載(preloading)
這個有點蛋疼, 因為一般瀏覽器只會靜態(tài)地掃描HTML,然后查找它可以預加載的資源。 我們上面介紹的模塊加載器是完全動態(tài)的,所以瀏覽器在沒有運行我們的代碼之前,是沒辦法發(fā)現(xiàn)我們要預加載現(xiàn)代還是傳統(tǒng)的Javascript資源的。
不過有一個解決辦法,就是不完美:就是使用來預加載現(xiàn)代版本的包, 舊瀏覽器會忽略這條規(guī)則,然而目前只有Chrome支持這么做:
其實預加載這種技術是否有效,取決于嵌入你的腳本的HTML文檔的大小。
如果你的HTML載荷很小, 比如只是一個啟動屏或者只是簡單啟動客戶端應用,那么放棄預加載掃描對你的應用性能影響很小。
如果你的應用使用服務器渲染大量有意義的HTML, 并以流(stream)的方式傳輸給瀏覽器,那么預加載掃描就是你的朋友,但這也未必是最佳方法。
譯注: 現(xiàn)代瀏覽器都支持分塊編碼傳輸,等服務端完全輸出html可能有一段空閑時間,這時候可以通過預加載技術,讓瀏覽器預先去請求資源
大概代碼如下:
還要指出的是,支持JS模塊的瀏覽器一般也支持。對于某些網站,相比依靠modulepreload, 使用可能更有意義。不過性能上面可能欠點,因為傳統(tǒng)的腳本預加載不會像modulepreload一樣隨著時間的推移而去展開解析工作(rel=preload只是下載,不會嘗試去解析腳本)。
選項2: 用戶代理嗅探我辦法拿出一個簡潔的代碼示例,因為用戶代理檢測不在本文的范圍之內,推薦閱讀這篇Smashing Magazine文章
本質上,這種技術在每個瀏覽器上都使用來加載代碼,當bundle.js被請求時,服務器會解析瀏覽器的用戶代理,并選擇返回現(xiàn)代代碼還是傳統(tǒng)代碼,取決于瀏覽器是否能被識別為現(xiàn)代瀏覽器.
盡管這種方法比較通用,但它也有一些嚴重的缺點:
因為依賴于服務端實現(xiàn),所以前端資源不能被靜態(tài)部署(例如靜態(tài)網站生成器(如github page),Netlify等等)
很難進行有效的緩存. 現(xiàn)在這些JavaScript URL的緩存會因用戶代理而異,這是非常不穩(wěn)定的, 而很多緩存機制只是將URL作為緩存鍵,現(xiàn)在這些緩存中間件可能就沒辦法工作了。
UA檢測很難,容易出現(xiàn)誤報
用戶代理字符串容易被篡改,而且每天都有新的UA出現(xiàn)
解決這些限制的一種方法就是將module/nomodule模式與"用戶代理區(qū)分"結合起來,首先這可以避免單純的module/nomodule模式需要發(fā)送多個軟件包問題,盡管這種方法仍然會降低頁面(這時候指HTML,而不是Javascript包)的可緩存性,但是它可以有效地觸發(fā)預加載,因為生成HTML的服務器根據用戶代理知道應該使用modulepreload還是preload:
function renderPage(request, response) { let html = `...`; const agent = request.headers.userAgent; const isModern = userAgent.isModern(agent); if (isModern) { html += ` `; } else { html += ` `; } response.end(html); }
對于那些已經在使用服務端渲染的網站來說,用戶代理嗅探是一個比較有效的解決方案
選項 3:不考慮舊版本瀏覽器注意這里的‘舊版本瀏覽器’特指那些出現(xiàn)雙重加載的瀏覽器. 對于module/nomodule模式支持比較差(即雙重加載)的主要是一些舊版本的Chrome、Firefox和Safari. 幸運的是這部分瀏覽器的市場范圍通常是比較窄,因為用戶會自動升級到最新的版本。Edge 16-18是例外, 但還有希望: 新版本的Edge會使用基于Chromium的渲染器,可以不受該問題的影響.
對于某些應用程序來說,接受這一點妥協(xié)是完全合理的:你可以給90%的瀏覽器中提供現(xiàn)代代碼,讓他們獲得更好的體驗,而極少數舊瀏覽器不得不拋棄它們,它們只是付出的額外帶寬(即雙重加載),并不影響功能。值得注意的是,占據移動端主要市場份額的用戶代理不會有雙重加載問題,所以這些流量不太可能來自于低速或者高昂流量費的手機。
如果你的網站用戶主要使用移動設備或較新版本的瀏覽器,那么最簡單的module/nomodule模式將適用于你的絕大多數用戶, 其他用戶就不考慮了,反正也是可以跑起來的, 優(yōu)先考慮大多數用戶的體驗。
選項 4: 使用條件包
nomodule可以巧妙地用來條件加載那些現(xiàn)代瀏覽器不需要的代碼, 例如polyfills。通過這種方法,最壞的情況就是polyfill和bundle都會被加載(例如Safari 10.1),但這畢竟是少數。鑒于目前通行的做法就是在所有瀏覽器中一致同仁地加載polyfills,相比而言, 條件polyfills可以讓大部分現(xiàn)代瀏覽器用戶避免加載polyfill代碼。
Angular CLI支持配置這種方式來加載polyfill, 查看Minko Gechev的代碼示例.
了解了這種方式之后,我決定在preact-cli中支持自動polyfill注入,你可以查看這個PR
如果你使用Webpack,這里有一個html-webpack-plugin插件可以方便地為polyfill包添加nomodule屬性.
你應該怎么做?答案取決于你的使用場景, 選擇和你們的架構匹配的選項:
如果你的應用只是客戶端渲染, 而且你的HTML不超過一個,選項1比較合適;
如果你的應用使用服務端渲染,而且可以接受緩存問題,那么可以選擇選項2;
如果你開發(fā)的是同構應用,預加載的功能可能對你很重要,這時你可以考慮選項3和4.
就我個人而言,相比考慮桌面端瀏覽器資源下載成本,我更傾向于優(yōu)化移動設備解析時間. 移動用戶體驗會受到數據解析、流量費用,電池消耗等因素的影響,而桌面用戶往往不需要考慮這些因素。
另外這些優(yōu)化適用于90%的用戶,比如我工作面對的大部分用戶都是使用現(xiàn)代或移動瀏覽器的。
有興趣繼續(xù)深入?可以從下面的文章開始挖掘:
Phil的webpack-esnext-boilerplate的一些附加的背景.
Ralph在Next.js中實現(xiàn)了module/nomodule, 并努力解決了上面的問題.
感謝Phil, Shubhie, Alex, Houssein, Ralph 以及 Addy 的反饋.
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/105816.html
摘要:異步請求線程在在連接后是通過瀏覽器新開一個線程請求,將檢測到狀態(tài)變更時,如果設置有回調函數,異步線程就產生狀態(tài)變更事件放到引擎的處理隊列中等待處理。 瀏覽器的主要功能是將用戶選擇的 web 資源呈現(xiàn)出來,它需要從服務器請求資源,并將其顯示在瀏覽器窗口中,資源的格式通常是 HTML,也包括 PDF、image 及其他格式。 瀏覽器的線程 瀏覽器是多線程的,它們在內核制控下相互配合以保持同...
摘要:異步請求線程在在連接后是通過瀏覽器新開一個線程請求,將檢測到狀態(tài)變更時,如果設置有回調函數,異步線程就產生狀態(tài)變更事件放到引擎的處理隊列中等待處理。 瀏覽器的主要功能是將用戶選擇的 web 資源呈現(xiàn)出來,它需要從服務器請求資源,并將其顯示在瀏覽器窗口中,資源的格式通常是 HTML,也包括 PDF、image 及其他格式。 瀏覽器的線程 瀏覽器是多線程的,它們在內核制控下相互配合以保持同...
摘要:啟動性能瓶頸分析與解決方案翻譯自的,從屬于筆者的前端入門與工程實踐。我們必須要清醒地認識到全面評測以挖掘出真正性能瓶頸的重要性。這可能是最佳的方式了,類似于這樣的模式鼓勵基于路由的分組,目前被與廣泛使用。 JavaScript 啟動性能瓶頸分析與解決方案 翻譯自 Addy Osmani 的 JavaScript Start-up Performance,從屬于筆者的Web 前端入門與工...
摘要:而且默認帶有執(zhí)行的順序是,,即便是內聯(lián)的,依然具有屬性。模塊腳本只會執(zhí)行一次必須符合同源策略模塊腳本在跨域的時候默認是不帶的。通常被用作腳本被禁用的回退方案。最后標簽真的令人感到興奮。 窺探 Script 標簽 0x01 什么是 script 標簽? script 標簽允許你包含一些動態(tài)腳本或數據塊到文檔中,script 標簽是非閉合的,你也可以將動態(tài)腳本或數據塊當做 script 的...
摘要:而且默認帶有執(zhí)行的順序是,,即便是內聯(lián)的,依然具有屬性。模塊腳本只會執(zhí)行一次必須符合同源策略模塊腳本在跨域的時候默認是不帶的。通常被用作腳本被禁用的回退方案。最后標簽真的令人感到興奮。 窺探 Script 標簽 0x01 什么是 script 標簽? script 標簽允許你包含一些動態(tài)腳本或數據塊到文檔中,script 標簽是非閉合的,你也可以將動態(tài)腳本或數據塊當做 script 的...
閱讀 3175·2021-09-10 10:51
閱讀 3359·2021-08-31 09:38
閱讀 1652·2019-08-30 15:54
閱讀 3138·2019-08-29 17:22
閱讀 3219·2019-08-26 13:53
閱讀 1969·2019-08-26 11:59
閱讀 3290·2019-08-26 11:37
閱讀 3317·2019-08-26 10:47