webpack入門必備(二):優化配置

以前寫的《webpack入門必備(一):基礎配置》主要介紹了webpack基礎解析所需的loader/plugin。而隨着平常webpack的使用,咱們會更多關注如何構建更快、構建產物更小、構建產物符合規範...但願這篇文章可讓你找到答案。

1、webpack4的構建優化

1. 加快構建速度

1.1 優化配置

這裏介紹的主要的幾種優化配置以下所示:javascript

  1. 縮小構建範圍css

    • exclude、include範圍
    • noParse
    • IgnorePlugin
  2. 多進程html

    • thread-loader/happypack
  3. 緩存前端

    • cache-loader/cacheDirectory,把loader的處理結果緩存到本地
    • Dll緩存,把一些不常變動的模塊構建產物緩存在本地

若是你有沒用過的配置能夠接着看下面的具體使用方法,若是你已經很熟悉了則能夠跳過此節~java

1. exclude、include範圍

配置來確保轉譯儘量少的文件(exclude 的優先級高於 include)
const rootDir = process.cwd();

{
        test: /\.(j|t)sx?$/,
        include: [path.resolve(rootDir, 'src')],
        exclude: [
          /(.|_)min\.js$/
        ],
}

PS. 相比exclude能夠多用includenode

2. noParse

若是一些庫不依賴其它庫的庫,不須要解析他們,能夠引入來加快編譯速度。
noParse: /node_modules\/(moment|chart\.js)/

3. IgnorePlugin

忽略第三方包指定目錄。 (他是webpack 內置的插件)

例如: moment (2.24.0版本) 會將全部本地化內容和核心功能一塊兒打包,咱們就可使用 IgnorePlugin 在打包時忽略本地化內容(語言包),見下圖。react

plugins: [
  // 表示忽略moment下的locale文件夾內容
  new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]

image

4.1 thread-loader

把 thread-loader 放置在其它 loader 以前,那麼它以後的 loader 就會在一個單獨的 worker 池中運行。
// 項目中babel-loader通常耗時比較長,因此能夠配置thread-loader 
rules: [ 
    { 
        test: /\.jsx?$/, 
        use: ['thread-loader', 'cache-loader', 'babel-loader'] 
    } 
]

4.2 happypack

運行在Node.js上的webpack是單線程,將文件解析的任務拆分由多個子進程併發進行,而後子進程處理完任務後再將結果發送給主進程,提高項目構件速度。
(可是由於進程的分配和管理也須要時間,因此使用後不必定快,須要項目接入實驗一下)
const Happypack = require("happypack");
module.exports = {
  module: {
    rules: [
      {
        test: /\.js[x]?$/,
        use: "Happypack/loader?id=js",
        include: [path.resolve(__dirname, "src")],
      },
      {
        test: /\.css$/,
        use: "Happypack/loader?id=css",
        include: [
          path.resolve(__dirname, "src"),
          path.resolve(__dirname, "node_modules", "bootstrap", "dist"),
        ],
      },
      {
        test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2|.gexf)$/,
        use: "Happypack/loader?id=file",
        include: [
          path.resolve(__dirname, "src"),
          path.resolve(__dirname, "public"),
          path.resolve(__dirname, "node_modules", "bootstrap", "dist"),
        ],
      },
    ],
  },
  plugins: [
    new Happypack({
      id: "js", //和rule中的id=js對應
      //將以前 rule 中的 loader 在此配置
      use: ["babel-loader"], //必須是數組
    }),
    new Happypack({
      id: "css", //和rule中的id=css對應
      use: ["style-loader", "css-loader", "postcss-loader"],
    }),
    new Happypack({
      id: "file", //和rule中的id=file對應
      use: [
        {
          loader: "url-loader",
          options: {
            limit: 10240, //10K
          },
        },
      ],
    }),
  ],
};

5. cache-loader/cacheDirectory

在性能開銷較大的loader處使用,將構建結果緩存中磁盤中。
(默認存在node_modueles/.cache/cache-loader目錄下。 )

cacheDirectory例子:webpack

rules: [
      {
            test: /\.(j|t)sx?$/,
            use: [
              {
                loader: 'babel-loader',
                options: {
                  cacheDirectory: true,
                },
              }
       }
 ]

cache-loader例子:es6

rules: [
    {
        test: /\.(css)$/,
        use: [
          { loader: 'style-loader' },
          { loader: 'cache-loader' },
          { loader: 'css-loader' },
          { loader: 'postcss-loader' }
        ]
      }
]

6. Dll緩存(動態連接庫)

將複用性較高的第三方模塊打包到DLL中,再次構建時直接複用,這樣只需從新打包業務代碼。
(注意是DLL緩存是大大縮短了 首次構建時間,像以前的cache-loader優化都是縮短 rebuild時間

使用相關插件:web

  • DllPlugin 插件:用於打包出一個個單獨的動態連接庫文件。
  • DllReferencePlugin 插件:用於在主要配置文件中去引入 DllPlugin 插件打包好的動態連接庫文件。

具體步驟:
(1) 新增一個webpack配置去編譯DLL文件([name].dll.js[name].manifest.json

// 新增一個webpack-dll.config.js配置文件

const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
const distPath = path.resolve(__dirname, 'dll');
module.exports = {
  entry: {
    // 把 React 相關模塊的放到一個單獨的動態連接庫
    react: ['react', 'react-dom'],
    // 把項目須要全部的 polyfill 放到一個單獨的動態連接庫
    polyfill: [
      'core-js/fn/object/assign',
      'core-js/fn/object/entries',
      ...
    ],
  },
  output: {
    // 輸出的動態連接庫的文件名稱,[name] 表明當前動態連接庫的名稱(react 和 polyfill)
    filename: '[name].dll.js',
    path: distPath,
    // 存放動態連接庫的全局變量名稱,例如對應 react 來講就是 _dll_react
    // 之因此在前面加上 _dll_ 是爲了防止全局變量衝突
    library: '_dll_[name]',
  },
  plugins: [
    // 接入 DllPlugin
    new DllPlugin({
      // 動態連接庫的全局變量名稱,須要和 output.library 中保持一致
      // 該字段的值也就是輸出的 manifest.json 文件 中 name 字段的值(_dll_react)
      name: '_dll_[name]',
      context: process.cwd(),
      // 描述動態連接庫的 manifest.json 文件輸出時的文件名稱
      path: path.join(__dirname, 'dll', '[name].manifest.json'),
    }),
  ],
};
// package.json裏新增dll的構建命令
"scripts": {
    "dll": "webpack --config webpack-dll.config.js",
}

(2) dev構建時,告訴 Webpack 使用了哪些動態連接庫

// webpack.config.js文件

const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');

plugins: [
    // 使用的動態連接庫(react和polyfill的)
    new DllReferencePlugin({
      context: process.cwd(),
      manifest: path.join(rootDir, 'dll', 'react.manifest.json'),
    }),
    new DllReferencePlugin({
      context: process.cwd(),
      manifest: path.join(rootDir, 'dll', 'polyfill.manifest.json'),
    }),
    ...
]

(3) html template裏引入文件

由於我這裏只是本地構建加速,因此就以dev的方式引入

<script src="./dll/polyfill.dll.js?_dev"></script>
<script src="./dll/react.dll.js?_dev"></script>

到這DLL就配好了。有些人可能比較好奇react.dll.jsreact.manifast.js究竟是什麼文件,作了什麼事?你看看他兩個文件就知道啦~

  • react.dll.js其實主要就是所引用模塊的代碼集合
  • react.manifast.js則寫明包含哪些模塊、模塊路徑
// react.dll.js文件部份內容以下所示。
var _dll_react = (function(modules) {
  // ... 此處省略 webpackBootstrap 函數代碼
}([
  function(module, exports, __webpack_require__) {
    // 模塊 ID 爲 0 的模塊對應的代碼
  },
  function(module, exports, __webpack_require__) {
    // 模塊 ID 爲 1 的模塊對應的代碼
  },
  // ... 此處省略剩下的模塊對應的代碼 
]));


// react.manifast.js文件部份內容以下所示。
{
  // 描述該動態連接庫文件暴露在全局的變量名稱
  "name": "_dll_react",
  "content": {
    "./node_modules/process/browser.js": {
      "id": 0,
      "meta": {}
    },
    // ... 此處省略部分模塊
    "./node_modules/react-dom/lib/ReactBrowserEventEmitter.js": {
      "id": 42,
      "meta": {}
    },
     ...
}

1.2 檢測工具

經常使用工具:speed-measure-webpack-plugin
使用方法:用其來包裹 Webpack 的配置

image

2. 構建產物方面

2.1 減少構建產物大小、提升複用率

這裏介紹的主要的幾種優化配置以下所示:

  • optimization.splitChunks分包
  • babel配置@babel/plugin-transform-runtime
  • tree-shaking

具體使用:

1. optimization.splitChunks分包

將業務代碼和第三方依賴庫進行分包,減少index.js的大小;
抽離多頁應用的公共模塊,單獨打包。公共代碼只須要下載一次就緩存起來了,避免了重複下載。
optimization: {
    minimize: false,
    moduleIds: 'named',
    splitChunks: {
      chunks: 'all',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 6,
      maxInitialRequests: 6,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        polyfill: {
          test: /[\\/]node_modules[\\/](core-js|@babel|regenerator-runtime)/,
          name: 'polyfill',
          priority: 70,
          minChunks: 1,
          reuseExistingChunk: true
        },
        lib: {
            test: /[\\/]node_modules[\\/]/,
            name: 'lib',
            chunks: 'initial',
            priority: 3,
            minChunks: 1,
         },
       ...
      }
    }
 }

2. babel配置

提取全部頁面所需的helper函數到一個包裏,避免重複注入

"plugins": [
    "@babel/plugin-transform-runtime"
    ...
]

3. tree-shaking

若是使用ES6的import 語法,那麼在生產環境下,會自動移除沒有使用到的代碼。

(1) 具體配置

const TerserPlugin = require('terser-webpack-plugin');

const config = {
 // 生產模式下tree-shaking才生效
 mode: 'production',
 optimization: {
  // Webpack 將識別出它認爲沒有被使用的代碼,並在最初的打包步驟中給它作標記。
  usedExports: true,
  minimizer: [
   // 刪除死代碼的壓縮器
   new TerserPlugin({...})
  ]
 }
};

(2) 哪類代碼會被shake掉?如下有一些事例

// no tree-shaking
import Stuff from './stuff';
doSomething(Stuff);

// tree-shaking
import Stuff from './stuff';
doSomething();

// tree-shaking
import './stuff';
doSomething();

// no tree-shaking
import 'my-lib';
doSomething();

// 所有導入 no tree-shaking
import _ from 'lodash';

// 具名導入 tree-shaking
import { debounce } from 'lodash';

// 直接導入具體的模塊  tree-shaking
import debounce from 'lodash/lib/debounce';

(3) 什麼叫有反作用的代碼?
`只要被引入,就會對應用程序產生重要的影響。
(一個很好的例子就是全局樣式表,或者設置全局配置的js文件。)`

(4) 有反作用的代碼咱們不但願被shake,咱們能夠配置以下

// 全部文件都有反作用,全都不可 tree-shaking
{
 "sideEffects": true
}
// 沒有文件有反作用,全均可以 tree-shaking
{
 "sideEffects": false
}
// 只有這些文件有反作用,全部其餘文件均可以 tree-shaking,但會保留這些文件
{
 "sideEffects": [
  "./src/file1.js",
  "./src/file2.js"
 ]
}

(5) 注意,babel配置須要配modules: false,忽略import/export代碼編譯

const config = {
 presets: [
  [
   '@babel/preset-env',
   {
     // commonjs代碼不能被tree-shaking
     // 因此babel保留咱們現有的 es2015 import/export 語句,不進行編譯
    modules: false
   }
  ]
 ]
};

2.2 檢測工具

經常使用工具:webpack-bundle-analyzer
使用方法:用其來包裹 Webpack 的配置

image

3. 產物檢查

ES check

生產環境構建時,會檢查構建產物裏是否存在es6語法。有則拋出錯誤並提示你去進行babel編譯,這樣避免了構建產物不合要求的狀況。

image
image

具體使用例子:

// package.json 命令里加上es-check檢查
"dist:basic": "rimraf public && cross-env NODE_ENV=production webpack --config webpack-dist.config.js && es-check es5 ./public/**/*.js"

2、webpack5的構建優化

1. 速度優化

1.1 編譯緩存

編譯緩存就是在首次編譯後把結果緩存起來,在後續編譯時複用緩存,從而達到加速編譯的效果。
webpack5默認開啓編譯緩存,緩存默認是在內存裏,你能夠自定義。
module.exports = {
    cache: {
        // 將緩存類型設置爲文件系統
        type: "filesystem", 
        // 緩存的位置(默認是node_modules/.cache/webpack)
        cacheDirectory: path.resolve(__dirname, '.temp_cache'), 
     
        // 指定構建過程當中的代碼依賴。webpack將使用這些項目以及全部依賴項的哈希值來使文件系統緩存無效。
        buildDependencies: {
     
            // 當配置文件內容或配置文件依賴的模塊文件發生變化時,當前的構建緩存即失效。 
            config: [__filename], 

            // webpack.config、loader和全部從你的配置中require的模塊都會被自動添加。若是有其餘的東西被構建依賴,你能夠在這裏添加它們
      },
      
      // 指定緩存的版本。當須要更新配置緩存時,經過設置此版本使緩存失效。
      version: '1.0'  
    }
}

一些參數註解

  • cache: true 就是 cache: { type: 'memory' } 的別名
  • type: 'filesystem'|'memory'。

若是設置'memory'則緩存在內存且不能配置其餘信息,設置成'filesystem'就能夠配置更多信息。默認開發模式使用的是'memory',生產模式是false。

  • version: 當配置文件和代碼都沒有發生變化,可是構建的外部依賴(如環境變量)發生變化時,預期的構建產物代碼也可能不一樣。這時就可使用 version 配置來防止在外部依賴不一樣的狀況下混用了相同的緩存。例如,能夠傳入 cache: {version: process.env.NODE_ENV},達到當不一樣環境切換時彼此不共用緩存的效果。

1.2 長效緩存 Long-term caching

長效緩存指的是能充分利用瀏覽器緩存,儘可能減小因爲模塊變動致使的構建文件hash值的改變,從而致使文件緩存失效。
(因爲moduleId和chunkId肯定了,構建的文件的hash值也會肯定。)

1.2.1 引子

  1. chunk、module都是什麼?

    • module:每個可被導入導出的源碼js、css文件就是一個module。
    • chunk:module經webpack依賴分析、打包生成的單獨文件塊。如:入口entry裏的文件、SplitChunks抽離的公共代碼
    • bundle:chunk後面通過編譯/壓縮打包等處理後就變成了bundle,bundle文件直接被html文件引用。

image

  1. webpack提供瞭如下3種哈希值,分別是什麼意思?有啥優缺點?
  • hash 全部bundle文件都是同一個hash。(【缺點】不修改文件的狀況下rebuild後hash會更新)
  • chunkhash 同一個entry/及entry引用的chunk文件都是同一個hash。(【缺點】修改chunk文件內容後,這個hash不變)
  • contenthash 一個文件一個hash,修改哪一個文件哪一個文件的hash就改變。(【缺點】若是刪除一個entry裏的chunk,entry和chunk及好多個文件的hash都變了,不利於長效緩存。

好比只是jsx刪除引用的一個css文件 好多bundle文件的hash就都變了。)

1.2.2 webpack4實現長效緩存

以前須要經過以下配置達到長效緩存:

plugins: [
- new webpack.NamedModulesPlugin(),
+ new webpack.HashedModuleIdsPlugin(),

或者配置

optimization.moduleIds = 'hashed’ 
optimization.chunkIds = 'named'

配置說明:

  • 在開發環境下使用 NamedModulesPlugin 來固化 module id,在生產環境下使用 HashedModuleIdsPlugin 來固化 module id(由於構建結果文件會更小)
  • 使用 NamedChunksPlugin 來固化 runtime 內以及在使用動態加載時分離出的 chunk 的 chunk id

(NamedChunksPlugin 只能對普通的 Webpack 模塊起做用,異步模塊(異步模塊能夠在 import 的時候加上 chunkName 的註釋,好比這樣:import(/ webpackChunkName: 「lodash」 / ‘lodash’).then() 這樣就有 Name 了),external 模塊是不會起做用的。)

1.2.3 Webpack5默認啓用deterministic實現長效緩存

Webpack5採用新的算法,生產模式下默認啓用以下配置不只實現長效緩存,還減小了文件打包大小:

optimization.chunkIds: "deterministic"
optimization.moduleIds: "deterministic"
mangleExports: 「deterministic"

PS.具體採用的算法還須要進一步深刻研究~

2. 包構建大小優化

2.1 Node Polyfill腳本被移除

Webpack 4版本附帶了大多數Node.js核心模塊的polyfill,一旦前端使用了任何核心模塊,這些模塊就會自動應用,致使polyfill文件很大,可是其實有些polyfill是沒必要要的。
而如今webpack5將不會自動爲Node.js模塊添加Polyfills,須要開發者手動添加合適的Polyfills。

升級遷移至webpack5須要注意:

  • 儘量嘗試使用與前端兼容的模塊。
  • 能夠爲 Node.js 核心模塊手動添加 polyfill。錯誤消息將提示實現方法。
  • 包做者:使用 package.json 中的 browser 字段使包與前端兼容。提供瀏覽器的替代實現 / 依賴。

2.2 tree-shaking

1.嵌套tree-shaking
可以跟蹤對export的嵌套屬性的訪問,分析模塊的export和import的依賴關係,去掉未被使用的模塊

// inner.js
export const a = 1;
export const b = 2;

// module.js
export * as inner from './inner';
// or import * as inner from './inner'; export { inner };

// user.js
import * as module from './module';
console.log(module.inner.a); // 在此示例中,能夠在生產模式下移除導出 b。

2.內部模塊tree-shaking(深度做用域分析)
新屬性optimization.innerGraph分析模塊導出和導入之間的依賴關係,在生產模式下默認啓用。

import { something } from './something';
function usingSomething() {
  return something;
}
export function test() {
  return usingSomething();
}
// 在使用 test 導出時才使用 something。

能夠分析如下符號:

  • 函數聲明
  • 類聲明
  • 帶有如下內容的 export default 或變量聲明:函數表達式,類表達式,序列表達式,/#_PURE_/ 表達式,局部變量,導入綁定

3.package.json 中的「sideEffects」標誌容許將模塊手動標記爲無反作用,從而在不使用它們時將其移除。
webpack 5 還能夠根據對源代碼的靜態分析,自動將模塊標記爲無反作用。

更多Webpack5的內容推薦閱讀:

相關文章
相關標籤/搜索