webpack2 終極優化

  • 2019 年 12 月 5 日
  • 筆記

本文作者:IMWeb 吳浩麟 原文出處:IMWeb社區 未經同意,禁止轉載

webpack是當下最流行的js打包工具,這得益於網頁應用日益複雜和js模組化的流行。webpack2增加了一些新特性也正式發布了一段時間,是時候告訴大家如何用webpack2優化你的構建讓它構建出更小的文件尺寸和更好的開發體驗。

優化輸出

打包結果更小可以讓網頁打開速度更快以及簡約寬頻。可以通過這以下幾點做到

壓縮css

css-loader 在webpack2里默認是沒有開啟壓縮的,最後生成的css文件里有很多空格和tab,通過配置 css-loader?minimize參數可以開啟壓縮輸出最小的css。css的壓縮實際是是通過cssnano實現的。

tree-shaking

tree-shaking 是指藉助es6 import export 語法靜態性的特點來刪掉export但是沒有import過的東西。要讓tree-shaking工作需要注意以下幾點:

  • 配置babel讓它在編譯轉化es6程式碼時不把import export轉換為cmd的module.export,配置如下:"presets": [ [ "es2015", { "modules": false } ] ]
  • 大多數分布到npm的庫里的程式碼都是es5的,但是也有部分庫(redux,react-router等等)開始支援tree-shaking。這些庫發布到npm里的程式碼即包含es5的又包含全採用了es6 import export 語法的程式碼。 拿redux庫來說,npm下載到的目錄結構如下:├── es │ └── utils ├── lib │ └── utils 其中lib目錄里是編譯出的es5程式碼,es目錄里是編譯出的採用import export 語法的es5程式碼,在redux的package.json文件里有這兩個配置:"main": "lib/index.js", "jsnext:main": "es/index.js", 這是指這個庫的入口文件的位置,所以要讓webpack去讀取es目錄下的程式碼需要使用jsnext:main欄位配置的入口,要做到這點webpack需要這樣配置:module.exports = { resolve: { mainFields: ['jsnext:main','main'], } }; 這會讓webpack先使用jsnext:main欄位,在沒有時使用main欄位。這樣就可以優化支援tree-shaking的庫。

優化 UglifyJsPlugin

webpack --optimize-minimize 選項會開啟 UglifyJsPlugin來壓縮輸出的js,但是默認的UglifyJsPlugin配置並沒有把程式碼壓縮到最小輸出的js里還是有注釋和空格,需要覆蓋默認的配置:

new UglifyJsPlugin({      // 最緊湊的輸出      beautify: false,      // 刪除所有的注釋      comments: false,      compress: {        // 在UglifyJs刪除沒有用到的程式碼時不輸出警告        warnings: false,        // 刪除所有的 `console` 語句        // 還可以兼容ie瀏覽器        drop_console: true,        // 內嵌定義了但是只用到一次的變數        collapse_vars: true,        // 提取出出現多次但是沒有定義成變數去引用的靜態值        reduce_vars: true,      }  })

定義環境變數 NODE_ENV=production

很多庫里(比如react)有部分程式碼是這樣的:

if(process.env.NODE_ENV !== 'production'){  // 不是生產環境才需要用到的程式碼,比如控制台里看到的警告  }

在環境變數 NODE_ENV 等於 production 的時候UglifyJs會認為if語句里的是死程式碼在壓縮程式碼時刪掉。

使用 CommonsChunkPlugin 抽取公共程式碼

CommonsChunkPlugin可以提取出多個程式碼塊都依賴的模組形成一個單獨的模組。要發揮CommonsChunkPlugin的作用還需要瀏覽器快取機制的配合。在應用有多個頁面的場景下提取出所有頁面公共的程式碼減少單個頁面的程式碼,在不同頁面之間切換時所有頁面公共的程式碼之前被載入過而不必重新載入。這個方法可以非常有效的提升應用性能。

在生產環境按照文件內容md5打hash

webpack編譯在生產環境出來的js、css、圖片、字體這些文件應該放到CDN上,再根據文件內容的md5命名文件,利用快取機制用戶只需要載入一次,第二次載入時就直接訪問快取。如果你之後有修改就會為對應的文件生產新的md5值。做到以上你需要這樣配置:

{    output: {      publicPath: CND_URL,      filename: '[name]_[chunkhash].js',    },  }

知道以上原理後我們還可以進一步優化:利用CommonsChunkPlugin提取出使用頁面都依賴的基礎運行環境。比如對於最常見的react體系你可以抽出基礎庫react react-dom redux react-redux到一個單獨的文件而不是和其它文件放在一起打包為一個文件,這樣做的好處是只要你不升級他們的版本這個文件永遠不會被刷新。如果你把這些基礎庫和業務程式碼打包在一個文件里每次改動業務程式碼都會導致瀏覽器重複下載這些保護基礎庫的程式碼。以上的配置為:

// vender.js 文件抽離基礎庫到單獨的一個文件里防止跟隨業務程式碼被刷新  // 所有頁面都依賴的第三方庫  // react基礎  import 'react';  import 'react-dom';  import 'react-redux';  // redux基礎  import 'redux';  import 'redux-thunk';
// webpack配置  {    entry: {      vendor: './path/to/vendor.js',    },  }

DedupePlugin 和 OccurrenceOrderPlugin

在webpack1里經常會使用 DedupePlugin 插件來消除重複的模組以及使用 OccurrenceOrderPlugin 插件讓被依賴次數更高的模組靠前分到更小的id 來達到輸出更少的程式碼,在webpack2里這些已經這兩個插件已經被移除了因為這些功能已經被內置了。

除了壓縮文本程式碼外還可以:

以上優化點只需要在構建用於生產環境程式碼的時候才使用,在開發環境時最好關閉因為它們很耗時。

優化開發體驗

優化開發體驗主要從更快的構建和更方便的功能入手。

更快的構建

縮小文件搜索範圍

webpack的resolve.modules配置模組庫(通常是指node_modules)所在的位置,在js里出現import 'redux'這樣不是相對也不是絕對路徑的寫法時會去node_modules目錄下找。但是默認的配置會採用向上遞歸搜索的方式去尋找node_modules,但通常項目目錄里只有一個node_modules在項目根目錄,為了減少搜索我們直接寫明node_modules的全路徑:

module.exports = {      resolve: {          modules: [path.resolve(__dirname, 'node_modules')]      }  };

除此之外webpack配置loader時也可以縮小文件搜索範圍。

  • loader的test正則表達式也應該儘可能的簡單,比如在你的項目里只有.js文件時就不要把test寫成/.jsx?$/
  • loader使用include命中只需要處理的文件,比如babel-loader的這兩個配置:

只對項目目錄下src目錄里的程式碼進行babel編譯

{      test: /.js$/,      loader: 'babel-loader',      include: path.resolve(__dirname, 'src')  }

項目目錄下的所有js都會進行babel編譯,包括龐大的node_modules下的js

{      test: /.js$/,      loader: 'babel-loader'  }

開啟 babel-loader 快取

babel編譯過程很耗時,好在babel-loader提供快取編譯結果選項,在重啟webpack時不需要創新編譯而是復用快取結果減少編譯流程。babel-loader快取機制默認是關閉的,打開的配置如下:

module.exports = {      module: {           loaders: [{                  test: /.js$/,                  loader: 'babel-loader?cacheDirectory',           }]    }  };

使用 alias

resolve.alias 配置路徑映射。 發布到npm的庫大多數都包含兩個目錄,一個是放著cmd模組化的lib目錄,一個是把所有文件合成一個文件的dist目錄,多數的入口文件是指向lib裡面下的。 默認情況下webpack會去讀lib目錄下的入口文件再去遞歸載入其它依賴的文件這個過程很耗時,alias配置可以讓webpack直接使用dist目錄的整體文件減少文件遞歸解析。配置如下:

module.exports = {    resolve: {      alias: {        'moment': 'moment/min/moment.min.js',        'react': 'react/dist/react.js',        'react-dom': 'react-dom/dist/react-dom.js'      }    }  };

使用 noParse

module.noParse 配置哪些文件可以脫離webpack的解析。 有些庫是自成一體不依賴其他庫的沒有使用模組化的,比如jquey、momentjs、chart.js,要使用它們必須整體全部引入。 webpack是模組化打包工具完全沒有必要去解析這些文件的依賴,因為它們都不依賴其它文件體積也很龐大,要忽略它們配置如下:

module.exports = {    module: {      noParse: /node_modules/(jquey|moment|chart.js)/    }  };

除此以外還有很多可以加速的方法:

更方便的功能

模組熱替換

模組熱替換是指在開發的過程中修改程式碼後不用刷新頁面直接把變化的模組替換到老模組讓頁面呈現出最新的效果。 webpack-dev-server內置模組熱替換,配置起來也很方便,下面以react應用為例,步驟如下:

  • 在啟動webpack-dev-server的時候帶上--hot參數開啟模組熱替換,在開啟--hot後針對css的變化是會自動熱替換的,但是js涉及到複雜的邏輯還需要進一步配置。
  • 配置頁面入口文件
import App from './app';    function run(){      render(<App/>,document.getElementById('app'));  }  run();    // 只在開發模式下配置模組熱替換  if (process.env.NODE_ENV !== 'production') {    module.hot.accept('./app', run);  }

當./app發生變化或者當./app依賴的文件發生變化時會把./app編譯成一個模組去替換老的,替換完畢後重新執行run函數渲染出最新的效果。

自動生成html

webpack只做了資源打包的工作還缺少把這些載入到html里運行的功能,在龐大的app裏手寫html去載入這些資源是很繁瑣易錯的,我們需要自動正確的載入打包出的資源。 webpack原生不支援這個功能於是我做了一個插件 web-webpack-plugin 具體使用點開鏈接看詳細文檔,使用大概如下:

demo

webpack配置

module.exports = {      entry: {          A: './a',          B: './b',      },      plugins: [          new WebPlugin({              // 輸出的html文件名稱,必填,注意不要重名,重名會覆蓋相互文件。              filename: 'index.html',              // 該html文件依賴的entry,必須是一個數組。依賴的資源的注入順序按照數組的順序。              requires: ['A', 'B'],          }),      ]  };

將會輸出一個index.html文件,這個文件將會自動引入 entry AB 生成的js文件,

輸出的html:

<!DOCTYPE html>  <html>  <head>      <meta charset="UTF-8">  </head>  <body>  <script src="A.js"></script>  <script src="B.js"></script>  </body>  </html>

輸出的目錄結構

├── A.js  ├── B.js  └── index.html

管理多頁面

雖然webpack適用於單頁應用,但複雜的系統經常是由多個單頁應用組成,每個頁面一個功能模組。webpack給出了js打包方案但缺少管理多個頁面的功能。 web-webpack-pluginAutoWebPlugin會自動的為你的系統里每個單頁應用生成一個html入口頁,這個入口會自動的注入當前單頁應用依賴的資源,使用它你只需如下幾行程式碼:

plugins: [      // ./src/pages/ 代表存放所有頁面的根目錄,這個目錄下的每一個目錄被看著是一個單頁應用      // 會為裡面的每一個目錄生成一個html入口      new AutoWebPlugin('./src/pages/', {        //使用單頁應用的html模版文件,這裡你可以自定義配置        template: './src/assets/template.html',      }),    ],

查看web-webpack-plugin的文檔了解更多

分析輸出結果

如果你對當前的配置輸出或者構建速度不滿意,webpack有一個工具叫做webpack analyze 以可視化的方式直觀的分析構建,來進一步優化構建結果和速度。要使用它你需要在執行webpack的時候帶上--json --profile2個參數,這代表讓webpack把構建結果以json輸出並帶上構建性能資訊,使用如下:

webpack --json --profile > stats.json

會生產一個stats.json文件,再打開webpack analyze 上傳這個文件開始分析。

最後附上這篇文章所講到的webpack整體的配置,分為開發環境的webpack.config.js和生產環境的webpack-dist.config.js

原文:https://github.com/gwuhaolin/blog/issues/2