經常使用的webpack優化方法

1. 前言

關於webpack,相信如今的前端開發人員必定不會陌生,由於它已經成爲前端開發人員必不可少的一項技能,它的官方介紹以下:javascript

webpack 是一個模塊打包器。webpack的主要目標是將 JavaScript 文件打包在一塊兒,打包後的文件用於在瀏覽器中使用,但它也可以勝任轉換(transform)、打包(bundle)或包裹(package)任何資源(resource or asset)。css

在平常開發工做中,咱們除了會使用webpack以及會編寫它的配置文件以外,咱們還須要瞭解一些關於webpack性能優化的方法,這樣在實際工做就可以如虎添翼,加強自身的競爭力。html

關於webpack優化的方法我將其分爲兩大類,以下:前端

  • 能夠提升webpack打包速度,減小打包時間的優化方法
  • 可讓 Webpack 打出來的包體積更小的優化方法

OK,廢話很少說,接下來咱們就來分別瞭解一下優化方法。vue

2. 提升 Webpack 打包速度

2.1 優化Loader搜索範圍

對於 Loader 來講,影響打包效率首當其衝必屬 Babel 了。由於 Babel 會將代碼轉爲字符串生成 AST,而後對 AST 繼續進行轉變最後再生成新的代碼,項目越大,轉換代碼越多,效率就越低。固然了,咱們是有辦法優化的。java

首先咱們能夠優化 Loader 的文件搜索範圍,在使用loader時,咱們能夠指定哪些文件不經過loader處理,或者指定哪些文件經過loader處理。node

module.exports = {
  module: {
    rules: [
      {
        // js 文件才使用 babel
        test: /\.js$/,
        use: ['babel-loader'],
        // 只處理src文件夾下面的文件
        include: path.resolve('src'),
        // 不處理node_modules下面的文件
        exclude: /node_modules/
      }
    ]
  }
}

對於 Babel 來講,咱們確定是但願只做用在 JS 代碼上的,而後 node_modules 中使用的代碼都是編譯過的,因此咱們也徹底沒有必要再去處理一遍。react

另外,對於babel-loader,咱們還能夠將 Babel 編譯過的文件緩存起來,下次只須要編譯更改過的代碼文件便可,這樣能夠大幅度加快打包時間。jquery

loader: 'babel-loader?cacheDirectory=true'

2.2 cache-loader緩存loader處理結果

在一些性能開銷較大的 loader 以前添加 cache-loader,以將處理結果緩存到磁盤裏,這樣下次打包能夠直接使用緩存結果而不須要從新打包。webpack

module.exports = {
  module: {
    rules: [
      {
        // js 文件才使用 babel
        test: /\.js$/,
        use: [
          'cache-loader',
          ...loaders
        ],
      }
    ]
  }
}

那這麼說的話,我給每一個loder前面都加上cache-loader,然而凡事物極必反,保存和讀取這些緩存文件會有一些時間開銷,因此請只對性能開銷較大的 loader 使用 cache-loader。關於這個cache-loader更詳細的使用方法請參照這裏cache-loader

2.3 使用多線程處理打包

受限於Node是單線程運行的,因此 Webpack 在打包的過程當中也是單線程的,特別是在執行 Loader 的時候,長時間編譯的任務不少,這樣就會致使等待的狀況。那麼咱們可使用一些方法將 Loader 的同步執行轉換爲並行,這樣就能充分利用系統資源來提升打包速度了。

2.3.1 HappyPack

happypack ,快樂的打包。人如其名,就是可以讓Webpack把打包任務分解給多個子線程去併發的執行,子線程處理完後再把結果發送給主線程。

module: {
  rules: [
    {
        test: /\.js$/,
        // 把對 .js 文件的處理轉交給 id 爲 babel 的 HappyPack 實例
        use: ['happypack/loader?id=babel'],
        exclude: path.resolve(__dirname, 'node_modules'),
    },
    {
        test: /\.css$/,
        // 把對 .css 文件的處理轉交給 id 爲 css 的 HappyPack 實例
        use: ['happypack/loader?id=css']
    }
  ]
},
plugins: [
    new HappyPack({
        id: 'js', //ID是標識符的意思,ID用來代理當前的happypack是用來處理一類特定的文件的
        threads: 4, //你要開啓多少個子進程去處理這一類型的文件
        loaders: [ 'babel-loader' ]
    }),
    new HappyPack({
        id: 'css',
        threads: 2,
        loaders: [ 'style-loader', 'css-loader' ]
    })
]

2.3.2 thread-loader

thread-loader ,在worker 池(worker pool)中運行加載器loader。把thread-loader 放置在其餘 loader 以前, 放置在這個 thread-loader 以後的 loader 就會在一個單獨的 worker 池(worker pool)中運行。

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve('src'),
        use: [
          {
              loader: "thread-loader",
              // 有一樣配置的 loader 會共享一個 worker 池(worker pool)
              options: {
                  // 產生的 worker 的數量,默認是 cpu 的核心數
                  workers: 2,

                  // 一個 worker 進程中並行執行工做的數量
                  // 默認爲 20
                  workerParallelJobs: 50,

                  // 額外的 node.js 參數
                  workerNodeArgs: ['--max-old-space-size', '1024'],

                  // 閒置時定時刪除 worker 進程
                  // 默認爲 500ms
                  // 能夠設置爲無窮大, 這樣在監視模式(--watch)下能夠保持 worker 持續存在
                  poolTimeout: 2000,

                  // 池(pool)分配給 worker 的工做數量
                  // 默認爲 200
                  // 下降這個數值會下降整體的效率,可是會提高工做分佈更均一
                  poolParallelJobs: 50,

                  // 池(pool)的名稱
                  // 能夠修更名稱來建立其他選項都同樣的池(pool)
                  name: "my-pool"
              }
          }, 
          {
              loader:'babel-loader'
          }
        ]
      }
    ]
  }
}

一樣,thread-loader也不是越多越好,也請只在耗時的loader 上使用。

2.3.3 webpack-parallel-uglify-plugin

Webpack3 中,咱們通常使用 UglifyJS 來壓縮代碼,可是這個是單線程運行的,也就是說多個js文件須要被壓縮,它須要一個個文件進行壓縮。因此說在正式環境打包壓縮代碼速度很是慢(由於壓縮JS代碼須要先把代碼解析成AST語法樹,再去應用各類規則分析和處理AST,致使這個過程耗時很是大)。爲了加快效率,咱們可使用 webpack-parallel-uglify-plugin 插件,該插件會開啓多個子進程,把對多個文件壓縮的工做分別給多個子進程去完成,可是每一個子進程仍是經過UglifyJS去壓縮代碼。無非就是變成了並行處理該壓縮了,並行處理多個子任務,提升打包效率。來並行運行 UglifyJS,從而提升效率。

Webpack4 中,咱們就不須要以上這些操做了,只須要將 mode 設置爲 production 就能夠默認開啓以上功能。代碼壓縮也是咱們必作的性能優化方案,固然咱們不止能夠壓縮JS 代碼,還能夠壓縮HTMLCSS 代碼,而且在壓縮 JS 代碼的過程當中,咱們還能夠經過配置實現好比刪除 console.log 這類代碼的功能。

let ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
    module: {},
    plugins: [
        new ParallelUglifyPlugin({
            workerCount:3,//開啓幾個子進程去併發的執行壓縮。默認是當前運行電腦的cPU核數減去1
            uglifyJs:{
                output:{
                    beautify:false,//不須要格式化
                    comments:false,//不保留註釋
                },
                compress:{
                    warnings:false,//在Uglify]s除沒有用到的代碼時不輸出警告
                    drop_console:true,//刪除全部的console語句,能夠兼容ie瀏覽器
                    collapse_vars:true,//內嵌定義了可是隻用到一次的變量
                    reduce_vars:true,//取出出現屢次可是沒有定義成變量去引用的靜態值
                }
            },
        })
    ]
}

關於該插件更加詳細的用法請參照這裏webpack-parallel-uglify-plugin

2.4 DllPlugin&DllReferencePlugin

DllPlugin能夠將特定的類庫提早打包成動態連接庫,在一個動態連接庫中能夠包含給其餘模塊調用的函數和數據,把基礎模塊獨立出來打包到單獨的動態鏈接庫裏,當須要導入的模塊在動態鏈接庫裏的時候,模塊不用再次被打包,而是去動態鏈接庫裏獲取。這種方式能夠極大的減小打包類庫的次數,只有當類庫更新版本纔有須要從新打包,而且也實現了將公共代碼抽離成單獨文件的優化方案。

這裏咱們能夠先將reactreact-dom單獨打包成動態連接庫,首先新建一個新的webpack配置文件:webpack.dll.js

const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {
    // 想統一打包的類庫
    entry:['react','react-dom'],
    output:{
        filename: '[name].dll.js',  //輸出的動態連接庫的文件名稱,[name] 表明當前動態連接庫的名稱
        path:path.resolve(__dirname,'dll'),  // 輸出的文件都放到 dll 目錄下
        library: '_dll_[name]',//存放動態連接庫的全局變量名稱,例如對應 react 來講就是 _dll_react
    },
    plugins:[
        new DllPlugin({
            // 動態連接庫的全局變量名稱,須要和 output.library 中保持一致
            // 該字段的值也就是輸出的 manifest.json 文件 中 name 字段的值
            // 例如 react.manifest.json 中就有 "name": "_dll_react"
            name: '_dll_[name]',
            // 描述動態連接庫的 manifest.json 文件輸出時的文件名稱
            path: path.join(__dirname, 'dll', '[name].manifest.json')
        })
    ]
}

而後咱們須要執行這個配置文件生成依賴文件:

webpack --config webpack.dll.js --mode development

接下來咱們須要使用 DllReferencePlugin 將依賴文件引入項目中

const DllReferencePlugin = require('webpack/lib/DllReferencePlugin')
module.exports = {
  // ...省略其餘配置
  plugins: [
    new DllReferencePlugin({
      // manifest 就是以前打包出來的 json 文件
      manifest:path.join(__dirname, 'dll', 'react.manifest.json')
    })
  ]
}

2.5 noParse

module.noParse 屬性,能夠用於配置那些模塊文件的內容不須要進行解析(即無依賴) 的第三方大型類庫(例如jquery,lodash)等,使用該屬性讓 Webpack不掃描該文件,以提升總體的構建速度。

module.exports = {
    module: {
      noParse: /jquery|lodash/, // 正則表達式
      // 或者使用函數
      noParse(content) {
        return /jquery|lodash/.test(content)
      }
    }
}

2.6 IgnorePlugin

IgnorePlugin用於忽略某些特定的模塊,讓webpack 不把這些指定的模塊打包進去。

module.exports = {
  // ...省略其餘配置
  plugins: [
    new webpack.IgnorePlugin(/^\.\/locale/,/moment$/)
  ]
}

webpack.IgnorePlugin()參數中第一個參數是匹配引入模塊路徑的正則表達式,第二個參數是匹配模塊的對應上下文,即所在目錄名。

2.7 打包文件分析工具

webpack-bundle-analyzer插件的功能是能夠生成代碼分析報告,幫助提高代碼質量和網站性能。

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports={
      plugins: [
          new BundleAnalyzerPlugin({
            generateStatsFile: true, // 是否生成stats.json文件
          })  
        // 默認配置的具體配置項
        // new BundleAnalyzerPlugin({
        //   analyzerMode: 'server',
        //   analyzerHost: '127.0.0.1',
        //   analyzerPort: '8888',
        //   reportFilename: 'report.html',
        //   defaultSizes: 'parsed',
        //   openAnalyzer: true,
        //   generateStatsFile: false,
        //   statsFilename: 'stats.json', 
        //   statsOptions: null,
        //   excludeAssets: null,
        //   logLevel: info
        // })
  ]
}

使用方式:

"generateAnalyzFile": "webpack --profile --json > stats.json", // 生成分析文件
"analyz": "webpack-bundle-analyzer --port 8888 ./dist/stats.json" // 啓動展現打包報告的http服務器

2.8 費時分析

speed-measure-webpack-plugin,打包速度測量插件。這個插件能夠測量webpack構建速度,能夠測量打包過程當中每一步所消耗的時間,而後讓咱們能夠有針對的去優化代碼。

const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');
const smw = new SpeedMeasureWebpackPlugin();
// 用smw.wrap()包裹webpack的全部配置項
module.exports =smw.wrap({
    module: {},
    plugins: []
});

2.9 一些小的優化點

咱們還能夠經過一些小的優化點來加快打包速度

  • resolve.extensions:用來代表文件後綴列表,默認查找順序是 ['.js', '.json'],若是你的導入文件沒有添加後綴就會按照這個順序查找文件。咱們應該儘量減小後綴列表長度,而後將出現頻率高的後綴排在前面
  • resolve.alias:能夠經過別名的方式來映射一個路徑,能讓 Webpack 更快找到路徑
module.exports ={
    // ...省略其餘配置
    resolve: {
        extensions: [".js",".jsx",".json",".css"],
        alias:{
            "jquery":jquery
        }
    }
};

3. 減小 Webpack 打包後的文件體積

3.1 對圖片進行壓縮和優化

image-webpack-loader這個loder能夠幫助咱們對打包後的圖片進行壓縮和優化,例如下降圖片分辨率,壓縮圖片體積等。

module.exports ={
    // ...省略其餘配置
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif|jpeg|ico)$/,
                use: [
                    'file-loader',
                    {
                        loader: 'image-webpack-loader',
                        options: {
                            mozjpeg: {
                                progressive: true,
                                quality: 65
                            },
                            optipng: {
                                enabled: false,
                            },
                            pngquant: {
                                quality: '65-90',
                                speed: 4
                            },
                            gifsicle: {
                                interlaced: false,
                            },
                            webp: {
                                quality: 75
                            }
                        }
                    }
                ]
            }
        ]
    }
};

3.2 刪除無用的CSS樣式

有時候一些時間久遠的項目,可能會存在一些CSS樣式被迭代廢棄,須要將其剔除掉,此時就可使用purgecss-webpack-plugin插件,該插件能夠去除未使用的CSS,通常與 globglob-all 配合使用。

注意:此插件必須和CSS代碼抽離插件mini-css-extract-plugin配合使用。

例如咱們有樣式文件style.css

body{
    background: red
}
.class1{
    background: red
}

這裏的.class1顯然是無用的,咱們能夠搜索src目錄下的文件,刪除無用的樣式。

const glob = require('glob');
const PurgecssPlugin = require('purgecss-webpack-plugin');

module.exports ={
    // ...
    plugins: [
        // 須要配合mini-css-extract-plugin插件
        new PurgecssPlugin({
            paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`, 
                  {nodir: true}), // 不匹配目錄,只匹配文件
            })
        }),
    ]
}

3.3 以CDN方式加載資源

咱們知道,通常經常使用的類庫都會發布在CDN上,所以,咱們能夠在項目中以CDN的方式加載資源,這樣咱們就不用對資源進行打包,能夠大大減小打包後的文件體積。

CDN方式加載資源須要使用到add-asset-html-cdn-webpack-plugin插件。咱們以CDN方式加載jquery爲例:

const AddAssetHtmlCdnPlugin = require('add-asset-html-cdn-webpack-plugin')

module.exports ={
    // ...
    plugins: [
        new AddAssetHtmlCdnPlugin(true,{
            'jquery':'https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js'
        })
    ],
    //在配置文件中標註jquery是外部的,這樣打包時就不會將jquery進行打包了
    externals:{
      'jquery':'$'
    }
}

3.4 開啓Tree Shaking

Tree-shaking,搖晃樹。顧名思義就是當咱們搖晃樹的時候,樹上乾枯的沒用的葉子就會掉下來。類比到咱們的代碼中就是將沒用的代碼搖晃下來,從而實現刪除代碼中未被引用的代碼。

這個功能在webpack4中,當咱們將mode設置爲production時,會自動進行tree-shaking

來看下面代碼:

main.js

import { minus } from "./calc";
console.log(minus(1,1));

calc.js

import {test} from './test';
export const sum = (a, b) => {
  return a + b + 'sum';
};
export const minus = (a, b) => {
  return a - b + 'minus';
};

test.js

export const test = ()=>{
    console.log('hello')
}
console.log(test());

觀察上述代碼其實咱們主要使用minus方法,test.js代碼是有反作用的!所謂"反作用",官方文檔以下解釋:

「反作用」的定義是,在導入時會執行特殊行爲的代碼,而不是僅僅暴露一個 export 或多個 export。舉例說明,例如 polyfill,它影響全局做用域,而且一般不提供 export。

對上述代碼進行打包後發現'hello'依然會被打印出來,這時候咱們須要在package.json中配置配置不使用反作用:

{
  "sideEffects": false
}

若是這樣設置,默認就不會導入css文件啦,由於咱們引入css也是經過import './style.css'

這裏重點就來了,tree-shaking主要針對es6模塊,咱們可使用require語法導入css,可是這樣用起來有點格格不入,因此咱們能夠配置css文件不是反作用,以下:

{
    "sideEffects":[
        "**/*.css"
    ]
}

3.5 開啓Scope Hoisting

Scope Hoisting 可讓 Webpack 打包出來的代碼文件更小、運行的更快, 它又譯做 "做用域提高",是在 Webpack3 中新推出的功能。

因爲最初的webpack轉換後的模塊會包裹上一層函數,import會轉換成require,由於函數會產生大量的做用域,運行時建立的函數做用域越多,內存開銷越大。而Scope Hoisting 會分析出模塊之間的依賴關係,儘量的把打包出來的模塊合併到一個函數中去,而後適當地重命名一些變量以防止命名衝突。這個功能在webpack4中,當咱們將mode設置爲production時會自動開啓。

好比咱們但願打包兩個文件

let a = 1;
let b = 2;
let c = 3;
let d = a+b+c
export default d;
// 引入d
import d from './d';
console.log(d)

最終打包後的結果會變成 console.log(6),這樣的打包方式生成的代碼明顯比以前的少多了,而且減小多個函數後內存佔用也將減小。若是你但願在開發模式development中開啓這個功能,只須要使用插件 webpack.optimize.ModuleConcatenationPlugin() 就能夠了。

module.exports = {
  // ...
  plugins: [
    // 開啓 Scope Hoisting
    new webpack.optimize.ModuleConcatenationPlugin(),
  ]
}

3.6 按需加載&動態加載

必你們在開發單頁面應用項目的時候,項目中都會存在十幾甚至更多的路由頁面。若是咱們將這些頁面所有打包進一個文件的話,雖然將多個請求合併了,可是一樣也加載了不少並不須要的代碼,耗費了更長的時間。那麼爲了首頁能更快地呈現給用戶,咱們確定是但願首頁能加載的文件體積越小越好,這時候咱們就可使用按需加載,將每一個路由頁面單獨打包爲一個文件。在給單頁應用作按需加載優化時,通常採用如下原則:

  • 對網站功能進行劃分,每一類一個chunk
  • 對於首次打開頁面須要的功能直接加載,儘快展現給用戶,某些依賴大量代碼的功能點能夠按需加載
  • 被分割出去的代碼須要一個按需加載的時機

動態加載目前並無原生支持,須要babel的插件:plugin-syntax-dynamic-import。安裝此插件而且在.babelrc中配置:

{
  // 添加
  "plugins": ["transform-vue-jsx", "transform-runtime"],
  
}

例如以下示例:

index.js

let btn = document.createElement('button');
btn.innerHTML = '點擊加載視頻';
btn.addEventListener('click',()=>{
    import(/* webpackChunkName: "video" */'./video').then(res=>{
        console.log(res.default);
    });
});
document.body.appendChild(btn);

webpack.config.js

module.exports = {
    // ...
    output:{
      chunkFilename:'[name].min.js'
    }
}

這樣打包後的結果最終的文件就是 video.min.js,而且剛啓動項目時不會加載該文件,只有當用戶點擊了按鈕時纔會動態加載該文件。

4. 總結

以上就是一些經常使用的webpack優化手段,固然webpack優化手段還有不少,而且用法也有不少。須要的話能夠閱讀官方文檔來深刻學習。

(完)

相關文章
相關標籤/搜索