徹底解決Webpack打包性能問題
這幾天寫騰訊實習生 Mini 項目的時候用上了 react 全家桶,當然同時引入了 Webpack 作為打包工具。但是開發過程中遇到一個很棘手的問題就是,react 加上 react-router、material-ui、superagent、eventproxy 這些第三方輪子一共有好幾百個 module,Webpack 的打包速度極慢。這對於開發是非常不好的體驗,同時效率也極低。
問題分析
我們先來看一下完全沒有任何優化的時候,Webpack 的打包速度(使用了jsx和babel的loader)。下面是我們的測試文件:
//test.jsvar react = require("react");var ReactAddonsCssTransitionGroup = require("react-addons-css-transition-group");var reactDOM = require("react-dom");var reactRouter = require("react-router");var superagent = require("superagent");var eventproxy = require("eventproxy");
運行
webpack test.js
在我的2015款RMBP13,i5處理器,全SSD下,性能是這樣的:
沒錯你沒有看錯,這幾個第三方輪子加起來有整整668個模塊,全部打包需要20多秒。
這意味著什麼呢?你每次對業務代碼的修改,gulp 或者 Webpack 監測到後都會重新打包,你要足足等20秒才能看到自己的修改結果。
但是需要重新打包的只有你的業務代碼,這些第三方庫是完全不用重新打包的,它們的存在只會拖累打包性能。所以我們要找一些方法來優化這個過程。
配置externals
Webpack 可以配置 externals 來將依賴的庫指向全局變數,從而不再打包這個庫,比如對於這樣一個文件:
import React from "react";console.log(React);
如果你在 Webpack.config.js 中配置了externals:
module.exports = { externals: { "react": "window.React" } //其它配置忽略...... };
等於讓 Webpack 知道,對於 react 這個模塊就不要打包啦,直接指向 window.React 就好。不過別忘了載入 react.min.js,讓全局中有 React 這個變數。
我們來看看性能,因為不用打包 React 了所以速度自然很快,包也很小:
配置externals的缺陷
問題如果就這麼簡單地解決了的話,那我就沒必要寫這篇文章了,下面我們加一個 react 的動畫庫 react-addons-css-transition-group 來試一試:
import React from "react";import ReactAddonsCssTransitionGroup from "react-addons-css-transition-group";console.log(React);
對,你沒有看錯,我也沒有截錯圖,新加了一個很小很小的動畫庫之後,性能又爆炸了。從模塊數來看,一定是 Webpack 又把 react 重新打包了一遍。
我們來看一下為什麼一個很小很小的動畫庫會導致 Webpack 又傻傻地把 react 重新打包了一遍。找到 react-addons-css-transition-group 這個模塊,然後看看它是怎麼寫的:
// react-addons-css-transition-group模塊// 入口文件 index.jsmodule.exports = require("react/lib/ReactCSSTransitionGroup");
這個動畫模塊就只有一行代碼,唯一的作用就是指向 react 下面的一個子模塊,我們再來看看這個子模塊是怎麼寫的:
// react模塊// react/lib/ReactCSSTransitionGroup.jsvar React = require("./React");var ReactTransitionGroup = require("./ReactTransitionGroup");var ReactCSSTransitionGroupChild = require("./ReactCSSTransitionGroupChild");//....剩餘代碼忽略
這個子模塊又反回去依賴了 react 整個庫的入口,這就是拖累 Webpack 的罪魁禍首。
總而言之,問題是這樣產生的:
- Webpack 發現我們依賴了 react-addons-css-transition-group;
- Webpack 去打包 react-addons-css-transition-group 的時候發現它依賴了 react 模塊下的一個叫 ReactTransitionGroup.js 的文件,於是 Webpack 去打包這個文件;
- ReactTransitionGroup.js 依賴了整個 react 的入口文件 React.js,雖然我們設置了 externals ,但是 Webpack 不知道這個入口文件等效於 react 模塊本身,於是我們可愛又敬業的 Webpack 就把整個 react 又重新打包了一遍。
讀到這裡你可能會有疑問,為什麼不能把這個動畫庫也設置到 externals 里,這樣不是就不用打包了嗎?
問題就在於,這個動畫庫並沒有提供生產環境的文件,或者說這個庫根本沒有提供 react-addons-css-transition-group.min.js 這個文件。
這個問題不只存在於 react-addons-css-transition-group 中,對於 react 的大多數現有庫來說都有這個依賴關係複雜的問題。
初級解決方法
所以對於這個問題的解決方法就是,手工打包這些 module,然後設置 externals ,讓 Webpack 不再打包它們。
我們需要這樣一個 lib-bundle.js 文件:
window.__LIB["react"] = require("react");window.__LIB["react-addons-css-transition-group"] = require("react-addons-css-transition-group");// ...其它依賴包
我們在這裡把一些第三方庫註冊到了 window.__LIB 下,這些庫可以作為底層的基礎庫,免於重複打包。
然後執行
webpack lib-bundle.js lib.js
得到打包好的 lib.js。然後去設置我們的 externals :
var webpack = require("webpack");module.exports = {externals: { "react": "window.__LIB["react"]", "react-addons-css-transition-group": "window.__LIB["react-addons-css-transition-group"]", // 其它庫 } //其它配置忽略...... };
這時由於 externals 的存在,Webpack 打包的時候就會避開這些模塊超多,依賴關係複雜的庫,把這些第三方 module 的入口指向預先打包好的 lib.js 的入口 window.__LIB,從而只打包我們的業務代碼。
終極解決方法
上面我們提到的方法本質上就是一種「動態鏈接庫(dll)」的思想,這在 windows 系統下面是一種很常見的思想。一個 dll 包,就是一個很純凈的依賴庫,它本身不能運行,是用來給你的 app 或者業務代碼引用的。
同樣的 Webpack 最近也新加入了這個功能:webpack.DllPlugin。使用這個功能需要把打包過程分成兩步:
- 打包ddl包
- 引用ddl包,打包業務代碼
首先我們來打包ddl包,首先配置一個這樣的 ddl.config.js:
const webpack = require("webpack");const vendors = [ "react", "react-dom", "react-router", // ...其它庫];module.exports = { output: { path: "build", filename: "[name].js", library: "[name]", }, entry: { "lib": vendors, }, plugins: [ new webpack.DllPlugin({ path: "manifest.json", name: "[name]", context: __dirname, }), ],};
webpack.DllPlugin 的選項中:
- path 是 manifest.json 文件的輸出路徑,這個文件會用於後續的業務代碼打包;
- name 是 dll 暴露的對象名,要跟 output.library 保持一致;
- context 是解析包路徑的上下文,這個要跟接下來配置的 webpack.config.js 一致。
運行Webpack,會輸出兩個文件一個是打包好的 lib.js,一個就是 manifest.json,它裡面的內容大概是這樣的:
{ "name": "vendor_ac51ba426d4f259b8b18", "content": { "./node_modules/react/react.js": 1, "./node_modules/react/lib/React.js": 2, "./node_modules/react/node_modules/object-assign/index.js": 3, "./node_modules/react/lib/ReactChildren.js": 4, "./node_modules/react/lib/PooledClass.js": 5, "./node_modules/react/lib/reactProdInvariant.js": 6, // ............ }}
接下來我們就可以快樂地打包業務代碼啦,首先寫好打包配置文件 webpack.config.js:
const webpack = require("webpack");module.exports = { output: { path: "build", filename: "[name].js", }, entry: { app: "./src/index.js", }, plugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require("./manifest.json"), }), ],};
webpack.DllReferencePlugin 的選項中:
- context 需要跟之前保持一致,這個用來指導 Webpack 匹配 manifest.json 中庫的路徑;
- manifest 用來引入剛才輸出的 manifest.json 文件。
DllPlugin 本質上的做法和我們手動分離這些第三方庫是一樣的,但是對於包極多的應用來說,自動化明顯加快了生產效率。
推薦閱讀:
※webpack技術講解及入門
※webpack源碼學習系列之一:如何實現一個簡單的webpack
※React學習資源匯總
TAG:webpack | JavaScript | 前端开发 |