將webpack打包優化到極致_20180619

背景:

項目上線前是專門針對 webpack 打包作了優化的,可是在以後作網絡優化的時候經過webpack-bundle-analyzer這個插件發現一些公共的js文件重複打包進了業務代碼的js中。這些代碼體積雖然很小,可是爲了將優化作到極致仍是想要將其優化一下。這個過程最大的收穫就是讓本身對 webpack4.x 相關配置項更加的熟悉,可以使用 webpack 遊刃有餘的實現本身想要的打包方式。javascript

記得以前的一位前輩同事說過一句前端優化的話:前端優化就是權衡各類利弊後作的一種妥協。css

優化結果:

這裏先看下優化結果,由於項目是多入口打包的模式(項目腳手架點擊這裏)每個頁面一個連接且每個頁面都會有本身的一個js文件。html

結果以下:前端

  • js代碼體積減小: 20kb+
  • 網絡鏈接時長縮短: 500ms+

移動端項目可以在原有的基礎上減小20kb已經不小了,並且這20kb是屬於重複打包產生的。網絡優化方面經過咱們公司內部監控平臺看到的數據也很是明顯,項目上線後統計3天內平均數據,頁面加載時間節省了接近800ms(上面的500ms是一個保守的寫法,由於會存在網絡抖動等不可抗因素)。vue

優化前的數據統計 java

優化後的數據統計 node

優化網絡解析時長和執行時長

一、添加DNS預解析

在 html 的 head 標籤中經過 meta 標籤指定開啓DNS 預解析和添加預解析的域名,實例代碼以下:webpack

<!--告訴瀏覽器開啓DNS 預解析-->
    <meta http-equiv="x-dns-prefetch-control" content="on" />
    <!--添加須要預解析的域名-->
    <link rel="dns-prefetch" href="//tracker.didiglobal.com">
    <link rel="dns-prefetch" href="//omgup.didiglobal.com">
    <link rel="dns-prefetch" href="//static.didiglobal.com">
複製代碼

先來看下添加上面的代碼以前,頁面中靜態資源的解析時間css3

添加了DNS 預解析的代碼以後與上面的圖片比較以後能夠明顯發現tracker.didiglobal.com 這個域名在須要加載的時候已經提早完成了預解析。若是這個js文件是影響頁面渲染的(好比按需加載的頁面),能夠提升頁面的渲染性能。git

二、延時執行影響頁面渲染的代碼

平時移動端開發過程當中咱們都會引用一些第三方的js依賴,好比說調用客戶端 jsbridge 方法的 client.js和接入打點服務的console.log.js

一般的方法也是比較暴力的方法就是將這些依賴的第三方js加入到 head 標籤中,在 dom 解析到 head 標籤的時候提早下載這些js而且在以後須要的時候可以隨時的調用。可是這些放在head 標籤中的js下載時間和執行時間無疑會影響頁面的渲染時間。

下面的那張圖是咱們的項目優化前的一個現狀,淺綠色的豎線是開始渲染的時間。從下面的圖種能夠發現咱們引用客戶端方法的 fusion.js 和打點的omega.js (這兩個js都是放在head標籤中的)都影響了頁面的渲染的開始時間。

其實咱們的業務場景在頁面渲染出來以前是不須要這些js的執行結果的,那咱們爲何不能將這些js作成異步加載呢?

js的異步加載就是選擇合適的時機,使用動態建立 script標籤的方式,將須要的js加載到頁面中來。咱們的項目使用的是 vue ,咱們選擇在 vue 的生命週期 mounted 中將須要打點的js給加載進來,在咱們須要調用客戶端方法的地方去加載 fusion.js 經過異步加載庫回調的方式,當js加載完以後執行用戶的操做。

下面是一段簡單的示例代碼:

export default function executeOmegaFn(fn) {
    // 動態加載js到當前的頁面中來,而且當js加載完以後執行回調,注意須要判斷js是否已經在當前環境加載過了
    scriptLoader('//xxx.xxxx.com/static/tracker_global/2.2.2/xxx.min.js', function () {
        fn && fn();
    });
}

// 異步加載 須要加載的js
mounted() {
    executeOmegaFn();
},
複製代碼

下圖是修改以後的效果,能夠發現,頁面開始渲染的時間提早了,頁面渲染完成的時間也比上面圖種的渲染完成的時間提早了 2s 左右。能夠很明顯的看出頁面的渲染和下載執行 omega 的時間是互不影響的。

總結:

  1. 在html模板文件中添加域名 DNS 預解析代碼塊,使瀏覽器提早預解析須要加載的靜態文件的域名。當須要下載靜態資源時,加快靜態文件的下載速度。
  2. 經過將首屏渲染不須要的js文件延時加載和執行,將頁面開始渲染的時間提早,以提升首屏渲染速度。

優化webpack產出

一、優化代碼重複打包

默認的 webpack4.x 在 production 模式下會對代碼作 tree shaking。可是看完這篇文章以後會發現大多數狀況下,tree shaking 並無辦法去除重複的代碼。 你的Tree-Shaking並沒什麼卵用

在個人項目中有個lib目錄下面放着根據業務須要本身編寫的函數庫,經過 webpack-bundle-analyzer 發現它重複的打包進了咱們的業務代碼的js文件中。下面的圖是打包後業務代碼包含的js文件,能夠看到 lib 目錄下的內容重複打包了

看到這種狀況的時候就來談談本身的優化方案:

  • 將node_modules目錄下的依賴統一打包成一個vendor依賴;
  • 將lib和common目錄下的本身編寫的函數庫單獨打包成一個 common ;
  • 將依賴的第三方組件庫按需打包,若是使用了組件庫中體積比較大的組件,好比 cube-ui 中的 date-picer 和 scroll 組件。若是隻使用一次就打包進入本身引用頁面的js文件中,若是被多個頁面引用就打包進入 common 中。後面優化第三方依賴會詳細的介紹下這部分的優化和打包策略。

先來看下個人 webpack 關於拆包的配置文件,具體看註釋:

splitChunks: {
    chunks: 'all',
    automaticNameDelimiter: '.',
    name: undefined,
    cacheGroups: {
        default: false,
        vendors: false,
        common: {
            test: function (module, chunks) {
                // 這裏經過配置規則只將 common lib cube-ui 和cube-ui 組件scroll依賴的better-scroll打包進入common中
                if (/src\/common\//.test(module.context) ||
                    /src\/lib/.test(module.context) ||
                    /cube-ui/.test(module.context) ||
                    /better-scroll/.test(module.context)) {
                    return true;
                }
            },
            chunks: 'all',
            name: 'common',
            // 這裏的minchunks 很是重要,控制cube-ui使用的組件被超過幾個chunk引用以後纔打包進入該common中不然不打包進該js中
            minChunks: 2,
            priority: 20
        },
        vendor: {
            chunks: 'all',
            test: (module, chunks) => {
                // 將node_modules 目錄下的依賴統一打包進入vendor中
                if (/node_modules/.test(module.context)) {
                    return true;
                }
            },
            name: 'vendor',
            minChunks: 2,
            // 配置chunk的打包優先級,這裏的數值決定了node_modules下的 cube-ui 不會打包進入 vendor 中
            priority: 10,
            enforce: true
        }
    }
}
複製代碼

在項目中寫了一個 js 將整個項目須要使用的 cube-ui 組件統一引入。具體的代碼看這裏調用方法看這裏

這裏須要注意下,就是不要將使用頻率低的體積大的組件在這個文件中引入,具體的緣由能夠看下面代碼的註釋。

/** * @file cube ui 組件引入統一配置文件 建議這裏只引入每一個頁面使用的基礎組件,對於複雜的組件好比scroll datepicer組件 * 在頁面中單獨引入,而後在webpack中同過 minChunk 來指定當這些比較大的組件超過 x 引用數時纔打進common中不然單獨打包進頁面的js中 * @date 2019/04/02 * @author hpuhouzhiqiang@gmail.com */

/* eslint-disable */
import Vue from 'vue';
import {
    Style,
    Toast,
    Loading,
    // 這裏去除 scroll是在頁面中單獨引入,以使webpack打包時能夠根據引用chunk選擇是否將該組件打包進入頁面的js中仍是選擇打包進入common中
    // Scroll,
    createAPI
} from 'cube-ui';


export default function initCubeComponent() {
    Vue.use(Loading);
    // Vue.use(Scroll);
    createAPI(Vue, Toast, ['timeout'], true);
}

複製代碼

項目中目前只有pay_history這個頁面使用了 cube-ui 的 scroll 組件,單獨打包進業務代碼的js中因此該頁面的js較大。

當有超過兩個頁面使用了 scroll 這個組件的時候,根據 webpack 的配置會自動打包進入common中。下圖是打包結果,頁面的js大小縮小了,commonjs文件的體積變大了。

總結:

  • 優化對於第三方依賴組件的加載方式,減小沒必要要的加載和執行時間的損耗。

二、去掉沒必要要的import

有時候我在寫代碼的時候沒有注意,經過 import 引用了一個第三方依賴可是最後沒有使用它或者是上線的時候並不須要將執行的表達式註釋掉,而沒有註釋掉 import 語句 ,打包結果也會包含這個import的js的。好比如下代碼:

import VConsole from 'vconsole';
// 測試的時候咱們可能打開了下面的註釋,可是在上線的時候只是註釋了下面的代碼,webpack打包的時候仍然會將vconsole打包進目標js中。
// var vConsole = new VConsole();

複製代碼

總結:

  • 肯定無效的 import 語句,若是沒有使用 import 導入的函數或者是表達式就直接將 import 語句註釋掉或者是刪除掉。

三、babel-preset-env 和 autoprefix 配置優化

目前使用 babel + ES6 組合編寫前端代碼已經不多使用 babel-polyfill 了。 主要是它會污染全局變量,並且是全量引入 polyfill ,打包後的目標js文件會很是的大。

如今大多數狀況下都會使用 babel-preset-env 來作 polyfill。更智能或是高級的作法是使用在線的polyfill服務,參考連接

在使用 preset-env 的時候,大多數狀況都會忽略去配置兼容的 browsers 列表。或者直接從網絡上搜索到配置,不深究其產出結果直接複製、粘貼使用。其實這裏的配置會影響咱們的js文件的大小。

若是使用 autoprefix 給css自動添加廠商前綴時,也是須要配置一個 browsers 列表。這個配置列表也是會影響css文件大小的。 browserslist官方文檔

舉個例子,如今 windows phone 手機幾乎絕跡,對於如今的移動端項目是不須要考慮兼容 pc 和 wp手機的,那咱們在添加 polyfill 或是 css 廠商前綴時是否是能夠去掉 -ms- 前綴呢,那麼該怎麼配置呢?

個人配置以下:

"browsers": [
    "> 1%",
    "last 2 versions",
    "iOS >= 6.0",
    "not ie > 0",
    "not ie_mob > 0",
    "not dead"
]
複製代碼

這裏簡單提一下,正確使用css新特性的重要性。下面這段代碼是我在咱們的一個比較舊的項目中看到的。其實咋一看沒有什麼問題,可是在現代瀏覽器中瀏覽卻出現了問題?

.example {
    display: flex;
    display: -webkit-box;
}

.test {
   flex:1
}

複製代碼

這總寫法就是對 flex 佈局 解析不一致致使的問題。 在chrome 中 .example生效的是 display: -webkit-box 這個彈性盒佈局過渡期的寫法。 在 .test 中生效的是flex:1 而這個是新標準的寫法。致使佈局顯示出現問題。

autoprefix以後的代碼

.example {
    display: -ms-flexbox;
    display: flex;
    display: -webkit-box;
}

.test {
   -webkit-box-flex:1;
       -ms-flex:1;
           flex:1
}

複製代碼

一樣的也會致使上面沒有 autoprefix 以前的問題,佈局發生錯誤。

總結:

  1. 根據本身的業務場景,添加具體的polyfill配置。
  2. 若是使用了css3 的新特性,且使用了 autoprefix 作自動添加廠商前綴的處理,只須要在原始代碼中使用最新標準寫法就好了。

四、webpack runtime文件inline

使用webpack編譯代碼,當代碼生成了多個chunk時,webpack是怎麼加載這些chunk呢?

webpack 本身實現了一個模塊加載器來維護不一樣模塊間的關係(webpack 文章中稱它爲 runtime 模塊)。標識不一樣模塊是經過一串數字來作標識的(具體的能夠寫個簡單的demo來看下編譯結果)。

當修改了一個文件的代碼,在 runtime 模塊中這串數字會發生變化,若是在webpack 打包時對這部分代碼不作處理,它會默認的產出到咱們的 vendor 代碼中。致使只要修改代碼,生成的 vendor 文件的hash就會發生變化,沒辦法充分利用瀏覽器緩存。

webpack 已經提供了配置能夠將這部分代碼單獨抽離出來生成一個文件。由於這部分代碼常常發生變化,並且代碼體積很小,爲了減小 http 請求能夠在打包的時候選擇將這部分代碼內聯進html模板文件中。

在webpack4.x中能夠經過如下配置實現optimization.runtimeChunk: 'single'。若是想要將生產的runtime代碼內聯進入html,可使用這個webpack插件inline-manifest-webpack-plugin

五、 去除沒必要要的async語句

async和await語法糖可以很好的解決異步編程問題。在編寫前端代碼的過程當中也可使用該語法糖。無論是使用 babel 和 typescript 編譯代碼其實都是將 async和 await 編譯成了 generator。

若是對代碼體積有極致的需求,我是不太建議在前端代碼中使用 async 和await的。由於如今不少第三方依賴處理異步的方式都是使用 Promise ,咱們使用的 node_modules依賴通常也都是編譯後的 ES5 的源文件,都是對 Promise 作了 Polyfill 的。並且咱們本身的 babel 配置也會對 Promise 作 Polyfill, 若是混合使用 async 和 await ,babel又會增長相關 generator run time 代碼。

看一個真實的代碼案例:如下代碼中出現了一個 async 表達式,可是在任何調用這個方法地方的時候都沒有使用await ,經過閱讀代碼也肯定這裏不須要使用 async 表達式

添加了一個async表達式後,編譯結果以下圖。能夠發如今產出的目標文件中多了一個generaotr runtime 的代碼,並且這部分代碼的體積仍是比較大的

這是編譯前的文件大小

去掉這個沒必要要的 async 表達式後,下圖能夠看到編譯後的文件大小,代碼體積縮小了將近 3KB

六、優化第三方依賴

在第一小節中已經簡單了介紹了優化第三方依賴的打包方法了,這裏再作下總結:

  • 若是第三方依賴支持後編譯,使用後編譯,且按需加載的方式使用第三方依賴,不要將組件庫全量引入;
  • 若是第三方依賴某個組件體積較大,且在項目中使用次數較少,頁面又是按需加載,能夠選擇配置規則,當引用次數超過多少次以後纔將該組件打包進入公共的 common 中,不然將該組件直接打包進入業務代碼中;
  • 經過script標籤和連接引入第三方依賴時,這些連接儘可能不要寫入 head 標籤中,能夠選擇按需加載引入;

後編譯,就是在使用的時候編譯,能夠解決代碼重複打包的問題。按需引入是指,假如我使用的cube-ui有20個組件可是我只使用了其中的一個 alert 組件,避免所有引入,增長產出文件的體積;

七、lodash按需引入

lodash這個庫確實挺好用的,但它有個缺點,全量引入打包後體積較大。那麼lodash能不能按需引入呢?

固然是能夠的,能夠在 npm上搜索 lodash-es這個模塊,而後根據文檔執行命令能夠將 lodash 導出爲 es6 modules。 而後就能夠經過 import 方式單獨導入某個函數的方式使用。

其實lodash到底怎麼優化,有沒有必要優化,這個也是有一些爭議的,具體的能夠閱讀下百度高T灰大的這篇文章 lodash在webpack中的各項優化的嘗試。灰大的這篇文章也論證了文章開頭所說的,優化就是根據業務需求作了各類權衡後的一種妥協。

webpack 重要知識總結

一、hash、contenthash、chunkhash的區別

hash 是跟整個項目的構建相關,只要項目裏有文件更改,整個項目構建的hash值都會更改,而且所有文件都共用相同的hash值;

chunkhash 採用hash計算的話,每一次構建後生成的哈希值都不同,即便文件內容壓根沒有改變。這樣子是沒辦法實現緩存效果,咱們須要換另外一種哈希值計算方式,即chunkhash。chunkhash和hash不同,它根據不一樣的入口文件(Entry)進行依賴文件解析、構建對應的chunk,生成對應的哈希值。咱們在生產環境裏把一些公共庫和程序入口文件區分開,單獨打包構建,接着咱們採用chunkhash的方式生成哈希值,那麼只要咱們不改動公共庫的代碼,就能夠保證其哈希值不會受影響。

contenthash 使用 webpack 編譯代碼時,咱們能夠在js文件裏面引用css文件的。因此這兩個文件應該共用相同的chunkhash值。可是有個問題,若是js更改了代碼,css文件就算內容沒有任何改變,因爲是該模塊發生了改變,致使css文件會重複構建。這個時候,咱們可使用 extra-text-webpack-plugin 裏的 contenthash 值,保證即便css文件所處的模塊裏其它文件內容改變,只要css文件內容不變,那麼就不會重複構建。

二、splitChunks詳解

目前網絡上可以查詢到的 webpack4.x 的文檔對於 splitChunks 並無完整的中文翻譯,若是對於英文閱讀沒有障礙,能夠直接去閱讀官方文檔,若是英文很差能夠參考下面的參數和中文釋義:

首先 Webpack4.x 會根據下述條件自動進行代碼塊分割:

  • 新代碼塊能夠被共享引用或者這些模塊都是來自node_modules文件夾裏面
  • 新代碼塊大於30kb(min + gziped以前的體積)
  • 按需加載的代碼塊,最大數量應該小於或者等於5
  • 初始加載的代碼塊,最大數量應該小於或等於3
// 配置項解釋以下
splitChunks: {
    // 默認做用於異步chunk,值爲all
    //initial模式下會分開優化打包異步和非異步模塊。而all會把異步和非異步同時進行優化打包。也就是說moduleA在indexA中異步引入,indexB中同步引入,initial下moduleA會出如今兩個打包塊中,而all只會出現一個。
    // all 全部chunk代碼(同步加載和異步加載的模塊均可以使用)的公共部分分離出來成爲一個單獨的文件
    // async 將異步加載模塊代碼公共部分抽離出來一個單獨的文件
    chunks: 'async',
    // 默認值是30kb 當文件體積 >= minsize 時將會被拆分爲兩個文件 某則不生成新的chunk
    minSize: 30000,
    // 共享該module的最小chunk數 (當>= minchunks時纔會被拆分爲新的chunk)
    minChunks: 1,
    // 最多有5個異步加載請求該module
    maxAsyncRequests: 5,
    // 初始話時最多有3個請求該module
    maxInitialRequests: 3,
    // 名字中間的間隔符
    automaticNameDelimiter: '~',
    // 打包後的名稱,若是設置爲 truw 默認是chunk的名字經過分隔符(默認是~)分隔開,如vendor~ 也能夠本身手動指定
    name: true,
    // 設置緩存組用來抽取知足不一樣規則的chunk, 切割成的每個新的chunk就是一個cache group
    cacheGroups: {
        common: {
            // 抽取的chunk的名字
            name: 'common',
            // 同外層的參數配置,覆蓋外層的chunks,以chunk爲維度進行抽取
            chunks: 'all',
            // 能夠爲字符串,正則表達式,函數,以module爲維度進行抽取,
            // 只要是知足條件的module都會被抽取到該common的chunk中,爲函數時第一個參數
            // 是遍歷到的每個模塊,第二個參數是每個引用到該模塊的chunks數組
            test(module, chunks) {
                // module.context 當前文件模塊所屬的目錄 該目錄下包含多個文件
                // module.resource 當前模塊文件的絕對路徑

                if (/scroll/.test(module.context)) {
                    let chunkName = ''; // 引用該chunk的模塊名字

                    chunks.forEach(item => {
                        chunkName += item.name + ',';
                    });
                    console.log(`module-scroll`, module.context, chunkName, chunks.length);
                }
            },
            // 優先級,一個chunk極可能知足多個緩存組,會被抽取到優先級高的緩存組中, 數值打的優先被選擇
            priority: 10,
            // 最少被幾個chunk引用
            minChunks: 2,
            // 若是該chunk中引用了已經被抽取的chunk,直接引用該chunk,不會重複打包代碼 (當module未發生變化時是否使用以前的Module)
            reuseExistingChunk: true,
            // 若是cacheGroup中沒有設置minSize,則據此判斷是否使用上層的minSize,true:則使用0,false:使用上層minSize
            enforce: true
        }
    }
};


複製代碼

參考文章

相關文章
相關標籤/搜索