webpack性能優化

為什麼要優化?

如果你的項目很小,構建很快,其實不用特別在意性能方面的問題。但是隨着項目涉及到的頁面越來越多,功能和業務代碼也會越來越複雜,相應的 webpack 的構建時間也會越來越久,打包後的體積也會越來越大,這個時候我們就不得不考慮性能優化的事情了。

image

分析工具

在動手優化之前,我們需要有一個量化的指標,得知影響構建時間的問題究竟出在哪裡,是某個 chunk 的體積太大,還是某個loader或者 plugin的耗時太久了等等。

我們可以通過一些工具對項目的 體積速度 進行分析,讓後對症下藥。

體積分析

  • 初級分析

可以通過官方提供的 stat.json 幫助我們分析打包結果, stat.json 可以通過下面的語句快速生成

npx webpack --profile --json > stats.json

接下來我們就可以通過官方提供的 stat.json分析工具 進行分析,目前已無法打開

  • 使用第三方工具

webpack-bundle-analyzer 是一個打包分析神器,它的界面也清晰,而且能很直觀的給出每一個打包出來的文件的大小以及各自的依賴,能夠更加方便的幫助我們對項目進行分析。

使用如下:

// npm install --save-dev webpack-bundle-analyzer
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  // ...
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerPort: 8889, // 指定端口號
      openAnalyzer: false,
    })
  ],
  // ...
}

image

webpack-bundle-analyzer 其底層也是依賴 stat.json 文件的,通過對 stat.json 的分析,得出最後的分析頁面

通過分析工具的分析,我們可以知道哪些文件打包出來的體積比較大,從而對有問題的文件進行優化。

速度分析

我們可以通過 speed-measure-webpack-plugin 這個插件幫助我們分析整個打包的總耗時,以及每一個loader 和每一個 plugins 構建所耗費的時間,從而幫助我們快速定位到可以優化 Webpack 的配置。

// 安裝 npm install --save-dev speed-measure-webpack-plugin

// 使用
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

const webpackConfig = smp.wrap({
  plugins: [
    new MyPlugin(),
    new MyOtherPlugin()
  ]
});

image

如上圖所示,耗時比較長的都會以紅色標出

注意:speed-measure-webpack-plugin 對於 webpack 的升級還不夠完善,暫時還無法與你自己編寫的掛載在 html-webpack-plugin 提供的 hooks 上的自定義 Pluginadd-asset-html-webpack-plugin 就是此類)共存,有人已經在 github 上提了 issue 了,但是貌似還是沒有解決。

優化策略

經過相應的體積分析和速度分析之後,我們便可以着手進行優化了。

使用新版本

這個是 webpack 性能優化的萬能膏藥,升級版本必定能帶來性能提升,而且提升很明顯。
image

image

從上圖中我們可以看到,webpack4.0 的構建速度遠遠快於 webpack3.0,官方也說升級版本之後,構建時間可以降低 60% - 98% 左右。

在每一個版本的更新,webpack 內部肯定會做很多優化,而 webpack 是依賴 Nodejs 運行環境,升級他們對應的版本,webpack 的速度肯定也能夠獲得提升。

如果要遷移到 Webpack 4 也只需要檢查一下 checklist,看看這些點是否都覆蓋到了,就可以了。

webpack 4.0 帶來的優化

  • v8 引擎帶來的優化(for of 替代 forEachMapSet 替代 Objectincludes 替代 indexOf
  • 默認使用更快的 md4 hash 算法
  • webpack AST 可以直接從 loader 傳遞給 AST,減少解析時間
  • 使用字符串方法替代正則表達式

我們可以在 github 上的 webpack 庫的 releases 版本迭代 頁面中查看其帶來的性能優化:

image

一個V8性能的例子

我們可以來看一個例子,比較使用 includes 替代 indexOf 之後帶來的速度提升,創建 compare-includes-indexof.js 文件,在這個文件中建一個 10000000 長度的數組,記錄兩個函數分別消耗的時間:

const ARR_SIZE = 10000000;
const hugeArr = new Array(ARR_SIZE).fill(1);

// includes
const includesTest = () => {
  const arrCopy = [];
  console.time('includes')
  let i = 0;
  while (i < hugeArr.length) {
    arrCopy.includes(i++);
  }
  console.timeEnd('includes');
}

// indexOf
const indexOfTest = () => {
  const arrCopy = [];
  console.time('indexOf');
  for (let item of hugeArr) {
    arrCopy.indexOf(item);
  }
  console.timeEnd('indexOf');
}

includesTest();
indexOfTest();

image

所以在項目上儘可能使用比較新的 webpackNodeNpmYarn 版本,是我們提升打包速度的第一步。

體積優化

webpack 是個項目打包工具,一般項目打完包以後,需要發佈到服務器上供用戶使用,為了用戶體驗,我們的項目體積需要越小越好,所以 webpack 中打包的體積是 webpack 中重要的一環。

js壓縮

webpack4.0 默認在生產環境的時候是支持代碼壓縮的,即 mode=production 模式下

實際上 webpack4.0 默認是使用 terser-webpack-plugin 這個壓縮插件,在此之前是使用 uglifyjs-webpack-plugin,兩者的區別是後者對 ES6 的壓縮不是很好,同時我們可以開啟 parallel 參數,使用多進程壓縮,加快壓縮。

// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  // ...
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: 4, // 開啟幾個進程來處理壓縮,默認是 os.cpus().length - 1
      }),
    ],
  },
  // ...
}

css壓縮

我們可以藉助 optimize-css-assets-webpack-plugin 插件來壓縮 css,其默認使用的壓縮引擎是 cssnano。 具體使用如下:

// npm install --save-dev optimize-css-assets-webpack-plugin
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
// ...
const prodConfig = {
  // ...
  optimization: {
    minimizer: [
      new OptimizeCSSAssetsPlugin({
        assetNameRegExp: /\.optimize\.css$/g, // 一個正則表達式,指示應優化\最小化的資產的名稱。提供的正則表達式針對配置中
        cssProcessor: require('cssnano'), // 用於優化/最小化CSS的CSS處理器,默認為cssnano。
        cssProcessorPluginOptions: { // 傳遞給cssProcessor的插件選項,默認為 {}
          preset: ['default', { discardComments: { removeAll: true } }],
        },
        canPrint: true, // 一個布爾值,指示插件是否可以將消息打印到控制台,默認為 true
      })
    ]
  },
}

擦除無用的css

使用 PurgeCSS 來完成對無用 css 的擦除,它需要和 mini-css-extract-plugin 配合使用。

// npm install --save-dev mini-css-extract-plugin purgecss-webpack-plugin

const path = require('path');
const glob = require('glob');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const PurgecssPlugin = require('purgecss-webpack-plugin');
// ...
const PATHS = {
  src: path.join(__dirname, 'src')
}

const commonConfig = {
  // ...
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
    }),
    new PurgecssPlugin({
      paths: glob.sync(`${PATHS.src}/**/*`,  { nodir: true }),
    }),
  ]
  // ...
}

在未使用此插件之前,比如我只用到了bootsrap 裏面的 btn btn-primary 這個類,其他的都沒用到,但是在打包之後發現未用到的css也會被打包進去

image

引入插件後,重新打包,發現沒有用到的css都被擦除了

image

更多的使用方法可以參考: PurgeCSS 文檔

圖片壓縮

一般來說在打包之後,一些圖片文件的大小是遠遠要比 js 或者 css 文件大很多,所以我們首先要做的就是對於圖片的優化,我們可以手動的去通過線上的圖片壓縮工具,如 tiny png 幫我們來壓縮圖片。

但是這個比較繁瑣,在項目中我們希望能夠更加自動化一點,自動幫我們做好圖片壓縮,這個時候我們就可以藉助 image-webpack-loader 幫助我們來實現。它是基於 imagemin 這個 Node 庫來實現圖片壓縮的。

使用很簡單,我們只要在 file-loader 之後加入 image-webpack-loader 即可:

// npm install --save-dev file-loader image-webpack-loader

// ...
module: {
  rules: [
    {
      test: /\.(png|jpeg|jpg|gif)$/,
      use: [
        {
          loader: 'file-loader',
          options: {
            name: '[name]_[hash].[ext]',
            outputPath: 'images/',
          }
        },
        {
          loader: 'image-webpack-loader',
          options: {
            // 壓縮 jpeg 的配置
            mozjpeg: {
              progressive: true,
              quality: 65
            },
            // 使用 imagemin**-optipng 壓縮 png,enable: false 為關閉
            optipng: {
              enabled: false,
            },
            // 使用 imagemin-pngquant 壓縮 png
            pngquant: {
              quality: '65-90',
              speed: 4
            },
            // 壓縮 gif 的配置
            gifsicle: {
              interlaced: false,
            },
            // 開啟 webp,會把 jpg 和 png 圖片壓縮為 webp 格式
            webp: {
              quality: 75
            }
          }
        }
      ]
    },
  ]
}         
// ...

我們先不用這個loader打包,圖片大小是100K

image

使用 image-webpack-loader 之後,圖片大小是 55KB

image

gzip壓縮

我們可以使用 compression-webpack-plugin 插件對靜態文件做一個gzip壓縮,能極大的減小文件體積,是節省帶寬和加快站點速度的有效方法,但是這種方式需要服務端支持,比如Nginx需要開啟gzip 才能正常使用,Nginx的gzip設置

// npm install --save-dev [email protected]  注意webpack4使用5.x版本
const CompressionWebpackPlugin = require('compression-webpack-plugin');

module.exports = {
  // ...
  plugins: [
    new CompressionWebpackPlugin({
      test: new RegExp('\\.(js|css)$'), // 需要壓縮的文件,正則匹配
      threshold: 1024, // 只處理比這個值大的資源。按位元組計算
      deleteOriginalAssets: false // 是否刪除原資源
    })
  ]
}

打包之後的結果:

image

本地預先生成.gz文件,這樣服務器端就不用自己去生成.gz了,從而降低第一次服務器生成.gz的壓力。關於gzip的更多配置可以參考 官方文檔

Tree-shaking

有時候我們寫的某些模塊根本沒有使用,但是還是被打包了,這樣實際上會拖累 webpack 的打包速度,而且也會增加打包文件的體積,所以我們可以使用 tree-shaking 將這些代碼剔除掉。這個是webpack自帶的功能,默認支持,注意必須是 ES6 的語法,CJS 的方式不支持。

代碼拆分

從 webpack v4 開始,移除了 CommonsChunkPlugin,取而代之的是 optimization.splitChunks。它可以把一個大的文件分割成幾個小的文件,這樣也可以有效的提升 webpack 的打包速度。

webpack 將根據以下條件自動拆分 chunks:

  • 新的 chunk 可以被共享,或者模塊來自於 node_modules 文件夾
  • 新的 chunk 體積大於 20kb(在進行 min+gz 之前的體積)
  • 當按需加載 chunks 時,並行請求的最大數量小於或等於 30
  • 當加載初始化頁面時,並發請求的最大數量小於或等於 30

optimization.splitChunks 官方給了默認配置:

splitChunks: {
  chunks: "async", // "initial" | "all"(推薦) | "async" (默認就是async) | 函數
  minSize: 20000, // 生成 chunk 的最小體積(以 bytes 為單位)
  minChunks: 1, // 最小 chunk ,默認1
  maxAsyncRequests: 30, // 最大異步請求數, 默認30
  maxInitialRequests: 30, // 最大初始化請求書,默認30
  automaticNameDelimiter: '~', // 打包分隔符, 例如 vendors~main.js
  name: true, // 打包後的名稱,此選項可接收 function
  cacheGroups: { // 這裡開始設置緩存的 chunks ,緩存組
    vendors: {
      test: /[\\/]node_modules[\\/]/,
      priority: -10,
    },
    default: {
      minChunks: 2,
      priority: -20,
      reuseExistingChunk: true, // 可設置是否重用該chunk
    }
  }
}

可以先通過 webpack-bundle-analyzer 分析出哪些文件佔用比較大,再使用splitChunks進行合理的拆分。

下面看是一個之前老項目的例子,在沒有使用此插件拆分之前:

image

我們對項目進行一些配置,拆分佔用較大的文件

optimization: {
          splitChunks: {
            maxInitialRequests: 4,
            automaticNameDelimiter: '_',
            cacheGroups: {
              swiper: {
                name: 'chunk-swiper',
                test: /[\\/]node_modules[\\/]swiper[\\/]/,
                chunks: 'all',
                priority: 5,
                reuseExistingChunk: true,
                enforce: true
              },
              vant: {
                name: 'chunk-vant',
                test: /[\\/]node_modules[\\/]vant[\\/]/,
                chunks: 'all',
                priority: 5,
                reuseExistingChunk: true,
                enforce: true
              },
              videojs: {
                name: 'chunk-videojs',
                test: /[\\/]node_modules[\\/](video\.js)|(videojs-contrib-hls)[\\/]/,
                chunks: 'all',
                priority: 5,
                reuseExistingChunk: true,
                enforce: true
              },
              utils: {
                name: 'chunk-utils',
                test: /[\\/]node_modules[\\/](crypto-js)|(md5\.js)|(core-js)|(axios)[\\/]/,
                chunks: 'all',
                priority: 5,
                reuseExistingChunk: true,
                enforce: true
              }
            }
          }
        }

再次打包之後:

image

更多的配置項,可以參考 split-chunks-plugin

html-webpack-externals-plugin

html-webpack-externals-plugin插件可以將一些公用包提取出來使用 cdn 引入,不打入 bundle

下面我們先來寫一個vue的例子:

import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

const Foo = { template: '<div>foo</div>' };
const Bar = { template: '<div>bar</div>' };

const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
];

const router = new VueRouter({
  routes
});

const app = new Vue({
  router
}).$mount('#app');

我們打包看到main.bundle.js大小是 97.7K

image

接下來我們配置一下 html-webpack-externals-plugin

// npm i --save-dev html-webpack-externals-plugin [email protected]
// 此插件需要依賴html-webpack-plugin,注意webpack4安裝4.x版本

const htmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');

module.exports = {
  //...
  plugins: [
    new htmlWebpackPlugin({
      filename: './index.html',
    }),
    new HtmlWebpackExternalsPlugin({
      externals: [
        {
          module: 'vue',
          entry: '//unpkg.com/vue/dist/vue.js',
          global: 'Vue',
        }, {
          module: 'vue-router',
          entry: '//unpkg.com/vue-router/dist/vue-router.js',
          global: 'VueRouter'
        }
      ]
    })
  ]
}

再重新打包,可以看到 main.bundle.js 大小已經變成了1.21K ,同時在html中也自動幫我們引入了cdn腳本

image

速度優化

上面列舉了一些常用的體積優化方案,接下來我們介紹一些速度優化方案

分離配置文件

一般情況下,我們需要區分開發和生產兩套環境,各司其職。

在開發階段:我們需要 webpack-dev-server 來幫我們進行快速的開發,同時需要 HMR 熱更新 幫我們進行頁面的無刷新改動,而這些在 生產環境 中都是不需要的。

在生產階段:我們需要進行 代碼壓縮目錄清理計算 hash提取 CSS 等等;

在實現起來也非常簡單,可以參考vue-cli2.9的配置,將文件拆分為三個:

  • webpack.dev.js 開發環境的配置文件
  • webpack.prod.js 生產環境的配置文件
  • webpack.common.js 公共配置文件

通過 webpack-merge 來整合兩個配置文件共同的配置 webpack.common.js

減少查找過程

webpackresolve 參數進行合理配置,使用 resolve 字段告訴 webpack 怎麼去搜索文件

  • 合理使用 resolve.extensions

很多時候我們在引入模塊的時候,都是不帶後綴的,webpack 會自動帶上後綴後去嘗試詢問文件是否存在,查詢的順序是按照我們配置 的 resolve.extensions 順序從前到後查找,webpack 默認支持的後綴是 jsjson。所以我們應該把常用到的文件後綴寫在前面,或者 我們導入模塊時,盡量帶上文件後綴名。

雖然 extensions 會優先查找數組內的值,但是我們不要一股腦兒的把所有後綴都往裏面塞,這會調用多次文件的查找,這樣就會減慢打包速度。

  • 優化 resolve.modules

這個屬性告訴 webpack 解析模塊時應該搜索的目錄,絕對路徑和相對路徑都能使用。使用絕對路徑之後,將只在給定目錄中搜索,從而減少模塊的搜索層級:

  • 使用 resolve.alias 減少查找過程

alias 的意思為 別名,能把原導入路徑映射成一個新的導入路徑。這種寫法不僅在編寫時更方便,也能讓 webpack 減少查找過程,比如以下配置:

module.exports = {
  // ...
  resolve: {
    extensions: ['.vue', '.js'],
    mainFiles: ['index', 'list'],
    alias: {
      alias: path.resolve(__dirname, '../src/alias'),
    },
    modules: [
      path.resolve(__dirname, 'node_modules'), // 指定當前目錄下的 node_modules 優先查找
      'node_modules', // 將默認寫法放在後面
    ]
  },
  //...
}

縮小構建目標

排除 Webpack 不需要解析的模塊,即使用 loader 的時候,在盡量少的模塊中去使用。

我們可以藉助 includeexclude 這兩個參數,規定 loader 只在那些模塊應用和在哪些模塊不應用。

module.exports = {
  // ...
  module: {
    rules: [
      { 
        test: /\.js|jsx$/, 
        exclude: /node_modules/,
        include: path.resolve(__dirname, '../src'),
        use: ['babel-loader']
      },
      // ...
    ]
  },
  // ...
}

首先我們不加 excludeinclude 兩個參數,打包一下 npm run build,打包時間 3280ms 左右:

image

接着我們加上這兩個參數,意思分別是:

  • exclude: /node_modules/:排除 node_modules 下面的文件
  • include: path.resolve(__dirname, '../src'):只對 src 下面的文件使用

重新打包,結果變成了1021ms

image

利用多線程提升構建速度

由於運行在 Node.js 之上的 webpack 是單線程模型的,所以 webpack 需要處理的事情需要一件一件的做,不能多件事一起做

HappyPack

原理:每次 webapck 解析一個模塊,HappyPack 會將它及它的依賴分配給 worker 線程中。處理完成之後,再將處理好的資源返回給 HappyPack 的主進程,從而加快打包速度。

image

webpack4.0 中使用 happypack 需要使用其 5.0 版本

我們將 HappyPack 引入公共配置文件,他的用法就是將相應的 loader 替換成 happypack/loader,同時將替換的 loader 放入其插件的 loaders 選項,我們暫且替換一下 babel-loader

const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
const HappyPack = require('happypack');

module.exports = {
  mode: 'production',
  entry: {
    index: './src/index.js',
    entry1: './src/entry1.js',
    entry2: './src/entry2.js',
    entry3: './src/entry3.js',
    entry4: './src/entry4.js',
    entry5: './src/entry5.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.join(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: [
          // 'babel-loader'
          'happypack/loader?id=happyBabel'
        ],
        exclude: /node_modules/,
        include: path.resolve(__dirname, 'src'),
      }
    ]
  },
  plugins: [
    new htmlWebpackPlugin({
      filename: './index.html',
    }),
    new HappyPack({
        id: 'happyBabel',
        loaders: ['babel-loader'],
        verbose: true,
    })
  ]
}

為了讓打包更明顯,在入口多增加了幾個頁面,首先是在使用 happypack 的情況下打包,耗時是5.7s左右

image

開啟 happypack 之後,我們可以從控制台中看到,happypack 默認幫我們開啟了 3 個進程,打包時間變成了1.1 左右:

image

注意:HappyPack 的作者現在基本上也不維護這個插件了,因為作者對此項目的興趣正在減弱。他也推薦我們使用 webpack 官方 thread-loader

更多參數大家可以參考 HappyPack 官網

thread-loader

webpack 官方推出的一個多進程方案,用來替代 HappyPack

原理和 HappyPack 類似,webpack 每次解析一個模塊,thread-loader 會將它及它的依賴分配給 worker 線程中,從而達到多進程打包的目的。

使用很簡單,直接在我們使用的 loader 之前加上 thread-loader 就行,我們需要先注釋掉 HappyPack 代碼:

// npm install --save-dev thread-loader
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: [
          {
            loader: 'thread-loader',
            options: {
              workers: 3, // 開啟幾個 worker 進程來處理打包,默認是 os.cpus().length - 1
            }
          },
          'babel-loader'
          // 'happypack/loader?id=happyBabel'
        ],
        exclude: /node_modules/,
        include: path.resolve(__dirname, 'src'),
      }
    ]
  },
  // ...
}

重新打包之後和 happypack 打包的結果差不多

image

預先編譯資源模塊(DllPlugin)

我們在打包的時候,一般來說第三方模塊是不會變化的,所以我們想只要在第一次打包的時候去打包一下第三方模塊,並將第三方模塊打包到一個特定的文件中,當第二次 webpack 進行打包的時候,就不需要去 node_modules 中去引入第三方模塊,而是直接使用我們第一次打包的第三方模塊的文件就行。

webpack.DllPlugin 就是來解決這個問題的插件,使用它可以在第一次編譯打包後就生成一份不變的代碼供其他模塊引用,這樣下一次構建的時候就可以節省開發時編譯打包的時間。

DllPluginwebpack內置的插件,不需要額外安裝,直接配置webpack.dll.config.js文件:

const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    // 需要打包的第三方庫
    react: ['react', 'react-dom']
  },
  output: {
    // 輸出的動態鏈接庫的文件名稱,[name] 代表當前動態鏈接庫的名稱,
    filename: '[name].dll.js',
    path: path.resolve(__dirname, 'dist/dll'),
    // library必須和後面dllplugin中的name一致 後面會說明
    library: '[name]_dll_[hash]'
  },
  plugins: [
    // 接入 DllPlugin
    new webpack.DllPlugin({
      // 動態鏈接庫的全局變量名稱,需要和 output.library 中保持一致
      // 該字段的值也就是輸出的 manifest.json 文件 中 name 字段的值
      name: '[name]_dll_[hash]',
      // 描述動態鏈接庫的 manifest.json 文件輸出時的文件名稱
      path: path.join(__dirname, 'dist/dll', '[name].manifest.json')
    }),
  ]
}
  • 上面的 library 的意思其實就是將 dll 文件以一個全局變量的形式導出出去,便於接下來引用,如下圖:
  • mainfest.json 文件是一個映射關係,它的作用就是幫助 webpack 使用我們之前打包好的 ***.dll.js 文件,而不是重新再去 node_modules 中去尋找。

我們在 package.json 中配置打包的命令,進行dll打包,可以看到根目錄生成了一個 dll 文件夾,並且在下面生成了相應的文件

{
	"scripts": {
    "build": "webpack",
    "dll": "webpack --config webpack.dll.config"
  },
}

image

接着我們需要去修改公共配置文件 webpack.config.js,將我們之前生成的 dll 文件導入到 html 中去,如果我們不想自己手動向 html 文件去添加 dll 文件的時候,我們可以藉助一個插件 add-asset-html-webpack-plugin,此插件顧名思義,就是將一些文件加到 html 中去。

// npm install --save-dev add-asset-html-webpack-plugin

const path = require('path');
const webpack = require('webpack');
const htmlWebpackPlugin = require('html-webpack-plugin');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');

module.exports = {
  // ...
  plugins: [
    new htmlWebpackPlugin({
      filename: './index.html'
    }),
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, 'dist/dll/react.dll.js')
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, 'dist/dll/react.manifest.json')
    })
  ]
}

我們再進行一次打包,可以看到打包耗時為 697ms,縮短了打包時間

image

緩存 Cache 相關

我們可以開啟相應 loader 或者 plugin 的緩存,來提升二次構建的速度。一般我們可以通過下面幾項來完成:

如果項目中有緩存的話,在 node_modules 下會有相應的 .cache 目錄來存放相應的緩存。

babel-loader

首先我們開啟 babel-loader 的緩存,我們修改 babel-loader 的參數,將參數 cacheDirectory 設置為 true即可

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true,
            }
          },
        ],
        // exclude: /node_modules/,
        // include: path.resolve(__dirname, 'src'),
      }
    ]
  }
}

首次打包時間為 3.3s 左右,打包完成之後,我們可以發現在 node_modules 下生成了一個 .cache 目錄,裏面存放了 babel 的緩存文件:

image

image

再重新打包一次,時間變成了 774ms

image

TerserPlugin

我們通過將 TerserPlugin 中的 cache 設為 true,就可以開啟緩存:

// npm install --save-dev [email protected]  注意webpack4 要使用4.x版本
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
  // ...
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: 4, // 開啟幾個進程來處理壓縮,默認是 os.cpus().length - 1
        cache: true,
      }),
    ],
  },
  // ...
}

和上面一樣,也會在 node_modules 目錄下生成 .cache 文件,下面包含 terser-webpack-plugin 目錄

HardSourceWebpackPlugin

這個插件其實就是用於給模塊提供一個中間的緩存。

使用如下,我們直接在插件中引入就 ok 了:

// npm install --save-dev hard-source-webpack-plugin
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

module.exports = {
  // ...
  plugins: [
    new HardSourceWebpackPlugin()
  ],
  // ...
}

具體的一些其他配置,參考 hard-source-webpack-plugin

合理使用 sourceMap

打包生成 sourceMap 的時候,如果信息越詳細,打包速度就會越慢

eval: 生成代碼 每個模塊都被eval執行,並且存在@sourceURL

cheap-eval-source-map: 轉換代碼(行內) 每個模塊被eval執行,並且sourcemap作為eval的一個dataurl

cheap-module-eval-source-map: 原始代碼(只有行內) 同樣道理,但是更高的質量和更低的性能

eval-source-map: 原始代碼 同樣道理,但是最高的質量和最低的性能

cheap-source-map: 轉換代碼(行內) 生成的sourcemap沒有列映射,從loaders生成的sourcemap沒有被使用

cheap-module-source-map: 原始代碼(只有行內) 與上面一樣除了每行特點的從loader中進行映射

source-map: 原始代碼 最好的sourcemap質量有完整的結果,但是會很慢

對於打包後的sourceMapwebpack提供多種類型的配置

image

其他優化

  • 盡量都使用ES6 Modules 語法,以保證 Tree-Shaking 起作用

因為 tree-shaking 只對 ES6 Modules 靜態引入生效,對於類似於 CommonJs 的動態引入方式是無效的

  • 合理使用 Ployfill

如果我們對於引入的 polyfill 不做處理的話,Webpack 會把所有的 Polyfill 都加載進來,導致產出文件過大。推薦使用 @babel/preset-envuseBuiltIns='usage' 方案,此配置項會根據瀏覽器的兼容性幫助我們按需引入所需的墊片;此外我們也可以使用動態 polyfill 服務,每次根據瀏覽器的 User Agent,下發不同的 Polyfill,具體可以參考 polyfill.io

  • 預加載資源 webpackPrefetch

使用 webpackPrefetch 來提前預加載一些資源,意思就是 將來可能需要一些模塊資源,在核心代碼加載完成之後帶寬空閑的時候再去加載需要用到的模塊代碼。

  • icon 類圖片使用 css Sprite 來合併圖片

如果 icon 類圖片太多的話,就使用雪碧圖合成一張圖片,減少網絡請求,或者使用字體文件 iconfont

  • 合理配置 chunk 的哈希值

在生產環境打包,一定要配置文件的 hash,這樣有助於瀏覽器緩存我們的文件,當我們的代碼文件沒變化的時候,用戶就只需要讀取瀏覽器緩存的文件即可。一般來說 javascript 文件使用 [chunkhash]css 文件使用 [contenthash]、其他資源(例如圖片、字體等)使用 [hash]

  • vue-router 路由懶加載

  • 動態組件

demo地址:

//github.com/Shenjieping/webpack-optimization/tree/main/webpack

參考文章:

//github.com/darrell0904/webpack-doc

//webpack.docschina.org/