webpack優化實踐(遇到很奇葩的問題)

一、先上打包先後的優化效果圖

二、優化背景

  • webpack 的構建過程太花時間
  • webpack 打包的結果體積太大
  • 做者的項目是webpack 2的老項目(項目已上線好久,不太敢忽然的升級webpack版本,以防有意想不到的問題)

三、優化操做

1. 用include或exclude來避免沒必要要的轉義

module: {
    rules: [{
        test: /\.js$/,
        use: {
             loader: 'babel-loader',
        },
        // 規避了對龐大的 node_modules 文件夾或者 bower_components 文件夾的處理
        exclude: /(node_modules|bower_components)/,
        include: resolve('source')
    }]
}
複製代碼

2. 開啓緩存將轉譯結果緩存至文件系統

module: {
    rules: [{
        test: /\.js$/,
        use: {
            // 開啓緩存
            loader: 'babel-loader?cacheDirectory=true'
        },
        exclude: /(node_modules|bower_components)/,
        include: resolve('source')
    }]
}
複製代碼

3. DllPlugin/DLLReferencePlugin優化處理

處理第三方庫有多種方式,externals、CommonsChunkPlugin、splitChunks、DllPlugin/DLLReferencePlugin 。咱們先說說各類的優劣勢html

3.1 externals

防止將某些 import 的包(package)打包到 bundle 中,而是在運行時(runtime)再去從外部獲取這些擴展依賴(external dependencies)。vue

<script src="https://unpkg.com/element-ui@2.0.8/lib/index.js"></script>

module.exports= {
    externals: {
        ElementUi: 'ELEMENT' // 主要element-ui保留出來的全局變量是ELEMENT
    }
}

import ElementUi from 'ElementUi';
複製代碼
  • 優勢:操做比較簡單,支持多種設置方式,也能夠自定義行爲。
  • 缺點:操做不是很靈活,須要設置多步,而且會出現意想不到的狀況。下面我會介紹到。

3.2 CommonsChunkPlugin

經過將公共模塊拆出來,最終合成的文件可以在最開始的時候加載一次,便存到緩存中供後續使用。這個帶來速度上的提高,由於瀏覽器會迅速將公共的代碼從緩存中取出來, 而不是每次訪問一個新頁面時,再去加載一個更大的文件。(可是新的webpack中已經被移除,使用的是SplitChunksPlugin)node

new webpack.optimize.CommonsChunlPlugin({
      name: string, // or
      names: string[],
      // 這是 common chunk 的名稱。已經存在的 chunk 能夠經過傳入一個已存在的 chunk 名稱而被選擇。
      // 若是一個字符串數組被傳入,這至關於插件針對每一個 chunk 名被屢次調用
      // 若是該選項被忽略,同時 `options.async` 或者 `options.children` 被設置,全部的 chunk 都會被使用,
      // 不然 `options.filename` 會用於做爲 chunk 名。
      // When using `options.async` to create common chunks from other async chunks you must specify an entry-point
      // chunk name here instead of omitting the `option.name`.
    
      filename: string,
      // common chunk 的文件名模板。能夠包含與 `output.filename` 相同的佔位符。
      // 若是被忽略,本來的文件名不會被修改(一般是 `output.filename` 或者 `output.chunkFilename`)。
      // This option is not permitted if you're using `options.async` as well, see below for more details. minChunks: number|Infinity|function(module, count) -> boolean, // 在傳入 公共chunk(commons chunk) 以前所須要包含的最少數量的 chunks 。 // 數量必須大於等於2,或者少於等於 chunks的數量 // 傳入 `Infinity` 會立刻生成 公共chunk,但裏面沒有模塊。 // 你能夠傳入一個 `function` ,以添加定製的邏輯(默認是 chunk 的數量) chunks: string[], // 經過 chunk name 去選擇 chunks 的來源。chunk 必須是 公共chunk 的子模塊。 // 若是被忽略,全部的,全部的 入口chunk (entry chunk) 都會被選擇。 children: boolean, // 若是設置爲 `true`,全部公共 chunk 的子模塊都會被選擇 deepChildren: boolean, // 若是設置爲 `true`,全部公共 chunk 的後代模塊都會被選擇 async: boolean|string, // 若是設置爲 `true`,一個異步的 公共chunk 會做爲 `options.name` 的子模塊,和 `options.chunks` 的兄弟模塊被建立。 // 它會與 `options.chunks` 並行被加載。 // Instead of using `option.filename`, it is possible to change the name of the output file by providing // the desired string here instead of `true`. minSize: number, // 在 公共chunk 被建立立以前,全部 公共模塊 (common module) 的最少大小。 }); 複製代碼
  • 優勢:最終合成的文件可以在最開始的時候加載一次,便存到緩存中供後續使用。這個帶來速度上的提高。
  • 缺點:每次構建時都會從新構建一次 vendor。

3.3 splitChunks

webpack4中支持了零配置的特性,同時對塊打包也作了優化,CommonsChunkPlugin已經被移除了,如今是使用optimization.splitChunks代替。webpack

// webpack.config.js
splitChunks: {
    chunks: "async",
    minSize: 30000,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    automaticNameDelimiter: '~',
    name: true,
    cacheGroups: {
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
        },
    	default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
        }
    }
}
複製代碼
  • 優勢:使用SplitChunksPlugin是Webpack 4的默認行爲,可能你設置一下chunks: 'all'就足夠了,打包速度更快了。
  • 缺點:SplitChunksPlugin是Webpack 4的特性,Webpack 4以前不能操做,好比我當前優化項目,webpack 2。

3.4 DllPlugin/DLLReferencePlugin

DllPlugin:是基於 Windows 動態連接庫(dll)的思想被創做出來的。這個插件會把第三方庫單獨打包到一個文件中,這個文件就是一個單純的依賴庫。這個依賴庫不會跟着你的業務代碼一塊兒被 從新打包,只有當依賴自身發生版本變化時纔會從新打包。ios

DLLReferencePlugin:是基於 Windows 動態連接庫(dll)的思想被創做出來的。這個插件會把第三方庫單獨打包到一個文件中,這個文件就是一個單純的依賴庫。這個依賴庫不會跟着你的業務代碼。web

一塊兒被從新打包,只有當依賴自身發生版本變化時纔會從新打包。配置步驟以下:vue-router

  1. 新增一個配置文件webpack.dll.config.js
const path = require('path');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
    entry: {
        // 依賴的庫數組
        vendor: [
            'vue/dist/vue.esm.js',
            'axios',
            // 'element-ui',
            'vue-router',
            'vuex',
            'v-viewer',
            'vue-photo-preview',
            'mockjs'
        ]
    },
    output: {
        path: path.join(__dirname, 'htdocs/dist'),
        filename: '[name].js',
        library: '[name]_[hash]',
    },
    plugins: [
        new CleanWebpackPlugin(['htdocs/dist/vendor-manifest.json', 'htdocs/dist/vendor.js']),
        new webpack.DllPlugin({
            // DllPlugin的name屬性須要和libary保持一致
            name: '[name]_[hash]',
            path: path.join(__dirname, 'htdocs/dist', '[name]-manifest.json'),
            // context須要和webpack.config.js保持一致
            context: __dirname,
        }),
    ],
}
複製代碼
  1. 配置package.json命令,npm run dll 這個命令就會生成兩個文件,一個是vendor-manifest.json,一個是vendor.js。 vendor.js 沒必要解釋,是咱們第三方庫打包的結果。這個多出來的 vendor-manifest.json,則用於描述每一個第三方庫對應的具體路徑下。
/ package.json
"scripts": {
    "dll": "webpack -p --progress --config ./webpack.dll.config.js" 
    },
複製代碼
  1. 配置webpack.config.js
module.exports = {
    plugins: [
         new webpack.DllReferencePlugin({
            context: __dirname,
            // manifest就是咱們第一步中打包出來的json文件
            manifest: require('./htdocs/dist/vendor-manifest.json'),
        })       
    ]
}
複製代碼
  1. 引入vendor.js,引入vendor.js有兩種方式
  • 第一種:直接在index.html。,這種方式簡單粗暴,針對沒有vendor.js沒有hash值的,徹底ok,可是若是你打包的vendor.js有hash,每次從新 npm run dll以後若是生成了新的hash,就要手動改這個路徑了。vuex

  • 第二種:add-asset-html-webpack-plugin,該插件會將給定的JS或CSS文件添加到Webpack知道的文件中,並將其放入html-webpack-plugin注入到生成的html 的資產列表中。將插件添加 到您的配置中。(可是須要注意的是此插件須要html-webpack-plugin@^2.10.0。在webpack 4+以後,須要先註冊AddAssetHtmlPlugin後才 使用內部使用的鉤子,而先前版本的webpack對此 並不關心。HtmlWebpackPluginhtml-webpack-plugin-before-html-generation。)npm

可是我在操做的時候遇到一個很奇葩的坑,element-ui被dllPlugins以後,el-table不生效,是的不生效,頁面也麼有報錯,樣式都還存在,可是就是頁面展現不出來table。編程

我看到element-ui的issues上也有人遇到這樣的狀況,可是如今尚未一個解決方案,很難受。

若是不能dll elemenet-ui,打包出來的index文件,仍是很大的,怎麼辦。

因此我想到了一個辦法就是,利用externals來處理element-ui(操做步驟如上),index有80%的大小是element-ui打包以後佔據的。可是externals來處理卻發現了另一個問題。index.js打包的文件是小了不少 可是,有一個相應的js文件的大小反而變的很大,這樣操做得不償失。

網上有不少解決辦法,可是都沒有根治我這個問題,最後的解決方案是,單獨的使用table,項目重寫table。這樣的方法也有一個弊端,就是當前只是發現了table不能正常使用,可是若是還有其餘 組件不行,也須要重寫,這樣的成本就很好了。因此,最後項目選擇是不打包element-ui(無奈之舉)。期待這個問題被解決。

4. happypack-將laoder轉換爲多線程

webpack 是單線程的,就算此刻存在多個任務,你也只能排隊一個接一個地等待處理。這是 webpack 的缺點,好在咱們的 CPU 是多核的,Happypack 會充分釋放 CPU 在多核併發方面的優點, 幫咱們把任務分解給多個子進程去併發執行,大大提高打包效率。

HappyPack 的使用方法也很是簡單,只須要咱們把對 loader 的配置轉移到 HappyPack 中去就好,咱們能夠手動告訴 HappyPack 咱們須要多少個併發的進程:

const HappyPack = require("happypack");
// 手動建立進程池
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
module.exports = {
    plugins: [
        new HappyPack({
            // 這個HappyPack的「名字」就叫作happyBabel,和樓上的查詢參數遙相呼應
            id: 'happyBabel',
            // 指定進程池
            threadPool: happyThreadPool,
            // 輸出日誌
            verbose: true,
            loaders: ['babel-loader?cacheDirectory']
        }),
    ]
};
複製代碼

5. webpack-parallel-uglify-plugin替換uglifyjs-webpack-plugin

這個插件能夠幫助具備許多入口點的項目加速構建。隨Webpack提供的uglifyjs插件在每一個輸出文件上按順序運行。這個插件與每一個可用CPU的一個線程並行運行uglify。這可能會致使顯著減小構 建時間,由於最小化是CPU密集型的。簡單點來講就是順序運行改成並行,因此大大提高了構建速度。

// const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
    plugins: [
        new ParallelUglifyPlugin({
            cacheDir: 'cache_dir',//用做緩存的可選絕對路徑。若是未提供,則不使用緩存。
            sourceMap: config.build.productionSourceMap, //可選布爾值。是否爲壓縮後的代碼生成對應的Source Map(瀏覽器能夠在調試代碼時定位到源碼位置了),這會減慢編譯速度。默認爲false
            uglifyJS: { 
               output: { 
                   comments: false,//是否保留代碼中的註釋,默認爲保留 
               }, 
               warnings: true,//是否在UglifyJS刪除沒有用到的代碼時輸出警告信息,默認爲false 
               compress: { 
                   drop_console: true,//是否刪除代碼中全部的console語句,默認爲false 
                   collapse_vars: true,//是否內嵌雖然已經定義了,可是隻用到一次的變量, 默認值false 
                   reduce_vars: true,//是否提取出現了屢次可是沒有定義成變量去引用的靜態值,默認爲false 
               } 
            },
        })
    ]
};
複製代碼

6. 路由按需加載

做者優化這個項目以前,開發人員也是分組打包,可是是一個路由打包一個js文件,致使最後的結果就是,生產了60+的js文件,在進行批量發佈時,哪速度叫一個感人,做者進行了一個簡單的操做優化, 就是根據名稱分組打包,最後打包下來只有10+的js,而且我發現,60+文件打包的大小比10+文件的整體大小要大一些。哈哈哈,無心間發現的。

7. 多使用函數式編程

這個優化其實就是一個平常須要注意的問題,可是能間接的提高webpack的打包效率、打包大小的優化。

  • 爲何? 由於在webpack 2開始,是集成了tree-shaking。在Vue的3.0版本,有一個重大的改變就是Vue採用了函數式編程。 函數式更方便tree-shaking(刪除無用的方法,例如:100個方法,只使用了1個方法,打包的時候的時候只打包一個方法,刪除沒有使用的方法),在Vue2.0暴露出去的是一個類, 不利於tree-shaking。在Vue3.0中,使用函數式,所有暴露的是函數,使用哪一個函數就是import哪一個函數,更方便tree-shaking。減小了代碼體積。在簡單項目中Vue的打包體積能夠下降到10KB左右。

因此咱們在平常的操做中多使用函數式編程,來提高webpack的打包效率和打包大小。

四、遺留問題

element-ui被dllPlugins以後,el-table不生效。求大神解決。項目依賴版本以下:

相關文章
相關標籤/搜索