在開發 web 應用程序時候,性能都是必不可少的話題。而大部分的前端優化機制都已經被集成到前端打包工具 webpack 中去了,固然,事實上仍舊會有一些有趣的機制能夠幫助 web 應用進行性能提高,在這裏咱們來聊一聊可以優化 web 應用程序的一些機制,同時也談一談這些機制背後的原理。css
在講解這些機制前,先來談一個 Chrome 工具 Corverage。該工具能夠幫助查找在當前頁面使用或者未使用的 JavaScript 和 CSS 代碼。html
工具的打開流程爲:前端
webpackjsvue
這裏以淘寶網爲例子,介紹一下如何使用node
上面兩張分別爲 reload 與 record 點擊後的分析。webpack
其中從左到右分別爲git
左下角有一份總述。說明在當前頁面加載的資源大小以及沒有使用的百分比。能夠看到淘寶網對於首頁代碼的未使用率僅僅只有 36%。github
介紹該功能的目的並非要求各位重構代碼庫以便於每一個頁面僅僅只包含所需的 js 與 css。這個是難以作到的甚至是不可能的。可是這種指標能夠提高咱們對當前項目的認知以便於性能提高。web
提高代碼覆蓋率的收益是全部性能優化機制中最高的,這意味着能夠加載更少的代碼,執行更少的代碼,消耗更少的資源,緩存更少的資源。vue-router
通常來講,咱們基本上都會使用 Vue,React 以及相對應的組件庫來搭建 SPA 單頁面項目。可是在構建時候,把這些框架代碼直接打包到項目中,並不是是一個十分明智的選擇。
咱們能夠直接在項目的 index.html 中添加以下代碼
<script src="//cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.runtime.min.js" crossorigin="anonymous"></script> <script src="//https://cdn.jsdelivr.net/npm/vue-router@3.1.3/dist/vue-router.min.js" crossorigin="anonymous"></script>
而後能夠在 webpack.config.js 中這樣配置
module.exports = { //... externals: { 'vue': 'Vue', 'vue-router': 'VueRouter', } };
webpack externals 的做用是 不會在構建時將 Vue 打包到最終項目中去,而是在運行時獲取這些外部依賴項。這對於項目初期沒有實力搭建自身而又須要使用 CDN 服務的團隊有着不錯的效果。
這些項目被打包成爲第三方庫的時候,同時還會以全局變量的形式導出。從而能夠直接在瀏覽器的 window 對象上獲得與使用。便是
window.Vue // ƒ bn(t){this._init(t)}
這也就是爲何咱們直接能夠在 html 頁面中直接使用
<div id="app"> {{ message }} </div> // Vue 就是 掛載到 window 上的,因此能夠直接在頁面使用 var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } })
此時咱們能夠經過 webpack Authoring Libraries 來了解如何利用 webpack 開發第三方包。
對於這種既沒法進行代碼分割又沒法進行 Tree Shaking 的依賴庫而言,把這些需求的依賴庫放置到公用 cdn 中,收益是很是大的。
對於相似 Vue React 此類庫而言,CDN 服務出現問題意味着徹底沒法使用項目。須要常常瀏覽所使用 CDN 服務商的公告(再也不提供服務等公告),以及在代碼中添加相似的出錯彌補方案。
<script src="//cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.runtime.min.js" crossorigin="anonymous"></script> <script>window.Vue || ...其餘處理 </script>
咱們能夠利用 webpack 動態導入,能夠在須要利用代碼時候調用 getComponent。在此以前,須要對 webpack 進行配置。具體參考 webpack dynamic-imports。
在配置完成以後,咱們就能夠寫以下代碼。
async function getComponent() { const element = document.createElement('div'); /** webpackChunkName,相同的名稱會打包到一個 chunk 中 */ const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; } getComponent().then(component => { document.body.appendChild(component); });
經過動態導入配置,能夠搞定多個 chunk,在須要時候纔會加載然後執行。對於該用戶不會使用的資源(路由控制,權限控制)不會進行加載,從而直接提高了代碼的覆蓋率。
Tree Shaking,能夠理解爲死代碼消除,即不須要的代碼不進行構建與打包。但當咱們使用動態導入時候,沒法使用 Tree Shaking 優化,由於二者直接按存在着兼容性問題。由於 webpack 沒法假設用戶如何使用動態導入的狀況。
基礎代碼X 模塊A 模塊B ----------------------------------- 業務代碼A 業務代碼B 業務代碼...
當在業務中使用多個異步塊時後,業務代碼A 需求 模塊A,業務代碼 B 需求 模塊B,可是 webpack 沒法去假設用戶在代碼中 A 與 B 這兩個模塊在同一時間是互斥仍是互補。因此必然會假設同時能夠加載模塊 A 與 B,此時基礎代碼 X 出現兩個導出狀態,這個是作不到的!從這方面來講,動態導入和 Tree Shaking 很難兼容。具體能夠參考 Document why tree shaking is not performed on async chunks 。
固然,利用動態導入,也會有必定的性能下降,畢竟一個是本地函數調用,另外一個涉及網絡請求與編譯。可是與其說這是一種缺陷,倒不如說是一種決策。到底是哪種對自身的項目幫助更大?
在普通的業務代碼咱們可使用動態導入,在當今的前端項目中,總有一些庫是咱們必需而又使用率很低的庫,好比在只會在統計模塊出現的 ECharts 數據圖表庫,或者只會在文檔或者網頁編輯時候出現的富文本編輯器庫。
對於這些苦庫其實咱們可使用頁面或組件掛載時候 loadjs 加載。由於使用動態導入這些第三方庫沒有 Tree shaking 加強,因此其實效果差很少,可是 loadjs 能夠去取公用 CDN 資源。具體能夠參考 github loadjs 來進行使用。由於該庫較爲簡單,這裏暫時就不進行深刻探討。
由於不管是使用 webpack externals 或者 loadjs 來使用公用 cdn 都是一種折衷方案。若是公司能夠花錢購買 oss + cdn 服務的話,就能夠直接將打包的資源託管上去。
module.exports = { //... output: { // 每一個塊的前綴 publicPath: 'https://xx/', chunkFilename: '[id].chunk.js' } }; // 此時打包出來的數據前綴會變爲 <script src=https://xx/js/app.a74ade86.js></script>
此時業務服務器僅僅只須要加載 index.html。
若是不須要在瀏覽器的首屏中使用腳本。能夠利用瀏覽器新增的 prefetch 延時獲取腳本。
下面這段代碼告訴瀏覽器,echarts 將會在將來某個導航或者功能中要使用到,可是資源的下載順序權重比較低。也就是說prefetch一般用於加速下一次導航。被標記爲 prefetch 的資源,將會被瀏覽器在空閒時間加載。
<link rel="prefetch" href="https://cdn.jsdelivr.net/npm/echarts@4.3.0/dist/echarts.min.js"></link>
該功能也適用於 html 以及 css 資源的預請求。
instant.page 是一個較新的功能庫,該庫小而美。而且無侵入式。
只要在項目的 </body> 以前加入如下代碼,便會獲得收益。
<script src="//instant.page/2.0.1" type="module" defer integrity="sha384-4Duao6N1ACKAViTLji8I/8e8H5Po/i/04h4rS5f9fQD6bXBBZhqv5am3/Bf/xalr"></script>
該方案不適合單頁面應用,可是該庫很棒的運用了 prefetch,是在你懸停於連接超過65ms 時候,把已經放入的 head 最後的 link 改成懸停連接的 href。
下面代碼是主要代碼
// 加載 prefetcher const prefetcher = document.createElement('link') // 查看是否支持 prefetcher const isSupported = prefetcher.relList && prefetcher.relList.supports && prefetcher.relList.supports('prefetch') // 懸停時間 65 ms let delayOnHover = 65 // 讀取設定在 腳本上的 instantIntensity, 若是有 修改懸停時間 const milliseconds = parseInt(document.body.dataset.instantIntensity) if (!isNaN(milliseconds)) { delayOnHover = milliseconds } // 支持 prefetch 且 沒有開啓數據保護模式 if (isSupported && !isDataSaverEnabled) { prefetcher.rel = 'prefetch' document.head.appendChild(prefetcher) ... // 鼠標懸停超過 instantIntensit ms || 65ms 改變 href 以便預先獲取 html mouseoverTimer = setTimeout(() => { preload(linkElement.href) mouseoverTimer = undefined }, delayOnHover) ... function preload(url) { prefetcher.href = url }
延時 prefetch ? 仍是在鼠標停留的時候去加載。不得不說,該庫利用了不少瀏覽器新的的機制。包括使用 type=module 來拒絕舊的瀏覽器執行,利用 dataset 讀取 instantIntensity 來控制延遲時間。
認識到這個庫是在 v8 關於新版本的文章中,在 github 中被標記爲 UNMAINTAINED 再也不維護,可是瞭解與學習該庫仍舊有其的價值與意義。該庫的用法十分簡單粗暴。竟然只是把函數改成 IIFE(當即執行函數表達式)。
用法以下:
optimize-js input.js > output.js
Example input:
!function (){}() function runIt(fun){ fun() } runIt(function (){})
Example output:
!(function (){})() function runIt(fun){ fun() } runIt((function (){}))
在 v8 引擎內部(不只僅是 V8,在這裏以 v8 爲例子),位於各個編譯器的前置Parse 被分爲 Pre-Parse 與 Full-Parse,Pre-Parse 會對整個 Js 代碼進行檢查,經過檢查能夠直接斷定存在語法錯誤,直接中斷後續的解析,在此階段,Parse 不會生成源代碼的AST結構。
// This is the top-level scope. function outer() { // preparsed 這裏會預分析 function inner() { // preparsed 這裏會預分析 可是不會 全分析和編譯 } } outer(); // Fully parses and compiles `outer`, but not `inner`.
可是若是使用 IIFE,v8 引擎直接不會進行 Pre-Parsing 操做,而是當即徹底解析並編譯函數。能夠參考Blazingly fast parsing, part 2: lazy parsing
快!即便在較新的 v8 引擎上,咱們能夠看到 optimize-js 的速度依然是最快的。更不用說在國內瀏覽器的版本遠遠小於 v8 當前版本。與後端 node 不一樣,前端的頁面生命週期很短,越快執行越好。
可是一樣的,任何技術都不是銀彈,直接徹底解析和編譯也會形成內存壓力,而且該庫也不是 js 引擎推薦的用法。相信在不遠的將來,該庫的收益也會逐漸變小,可是對於某些特殊需求,該庫的確會又必定的助力。
此時咱們在談一次代碼覆蓋率。若是咱們能夠在首屏記載的時候能夠達到很高的代碼覆蓋率。直接執行即是更好的方式。在項目中代碼覆蓋率越高,越過 Pre-Parsing 讓代碼儘快執行的收益也就越大。
若是寫過前端,就不可能不知道 polyfill。各個瀏覽器版本不一樣,所須要的 polyfill 也不一樣,
Polyfill.io是一項服務,可經過選擇性地填充瀏覽器所需的內容來減小 Web 開發的煩惱。Polyfill.io讀取每一個請求的User-Agent 標頭,並返回適合於請求瀏覽器的polyfill。
若是是最新的瀏覽器且具備 Array.prototype.filter
https://polyfill.io/v3/polyfill.min.js?features=Array.prototype.filter /* Disable minification (remove `.min` from URL path) for more info */
若是沒有 就會在 正文下面添加有關的 polyfill。
國內的阿里巴巴也搭建了一個服務,能夠考慮使用,網址爲 https://polyfill.alicdn.com/p...
使用新的 DOM API,能夠有條件地加載polyfill,由於能夠在運行時檢測。可是,使用新的 JavaScript 語法,這會很是棘手,由於任何未知的語法都會致使解析錯誤,而後全部代碼都不會運行。
該問題的解決方法是
<script type="module">。
早在 2017 年,我便知道 type=module 能夠直接在瀏覽器原生支持模塊的功能。具體能夠參考 JavaScript modules 模塊。可是當時感受只是這個功能很強大,並無對這個功能產生什麼解讀。可是卻沒有想到能夠利用該功能識別你的瀏覽器是否支持 ES2015。
每一個支持 type="module" 的瀏覽器都支持你所熟知的大部分 ES2015+ 語法!!!!!
例如
所以,利用該特性,徹底能夠去作優雅降級。在支持 type=module 提供所屬的 js,而在 不支持的狀況下 提供另外一個js。具體能夠參考 Phillip Walton 精彩的博文,這裏也有翻譯版本 https://jdc.jd.com/archives/4911.
若是當前項目已經開始從 webpack 陣營轉到 Vue CLI 陣營的話,那麼恭喜你,上述解決方案已經被內置到 Vue CLI 當中去了。只須要使用以下指令,項目便會產生兩個版本的包。
vue-cli-service build --modern
具體能夠參考 Vue CLI 現代模式
提高代碼覆蓋率,直接使用原生的 await 等語法,直接減小大量代碼。
提高代碼性能。以前 v8 用的時 Crankshaft 編譯器,隨着時間的推移,該編譯器由於沒法優化現代語言特性而被拋棄,以後 v8 引入了新的 Turbofan 編譯器來對新語言特性進行支持與優化,以前在社區中談論的 try catch, await,JSON 正則等性能都有了很大的提高。具體能夠時常瀏覽 v8 blog 來查看功能優化。
Writing ES2015 code is a win for developers, and deploying ES2015 code is a win for users.
無,實在考慮不出有什麼很差。
若是你以爲這篇文章不錯,但願能夠給與我一些鼓勵,在個人 github 博客下幫忙 star 一下。
博客地址
webpackjs 中文文檔
Blazingly fast parsing, part 2: lazy parsing
Polyfill.io
JavaScript modules 模塊
deploying-es2015-code-in-production-today
Vue CLI 現代模式
v8 blog