轉載javascript
這幾天寫騰訊實習生 Mini 項目的時候用上了 React 全家桶,固然同時引入了 Webpack 做爲打包工具。可是開發過程當中遇到一個很棘手的問題就是,React 加上 React-Router、superagent、eventproxy 這些第三方輪子一共有好幾百個 module,Webpack 的打包速度極慢。這對於開發是很是很差的體驗,同時效率也極低。css
咱們先來看一下徹底沒有任何優化的時候,Webpack 的打包速度(使用了jsx和babel的loader)。下面是咱們的測試文件:html
//test.js
var 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");
運行java
webpack test.js
在個人2015款RMBP13,i5處理器,全SSD下,性能是這樣的:node
沒錯你沒有看錯,這幾個第三方輪子加起來有整整668個模塊,所有打包須要20多秒。react
這意味着什麼呢?你每次對業務代碼的修改,gulp 或者 Webpack 監測到後都會從新打包,你要足足等20秒才能看到本身的修改結果。jquery
可是須要從新打包的只有你的業務代碼,這些第三方庫是徹底不用從新打包的,它們的存在只會拖累打包性能。因此咱們要找一些方法來優化這個過程。webpack
Webpack 能夠配置 externals 來將依賴的庫指向全局變量,從而再也不打包這個庫,好比對於這樣一個文件:web
import React from 'react';
console.log(React);
若是你在 Webpack.config.js 中配置了externals:json
module.exports = {
externals: {
'react': 'window.React'
}
//其它配置忽略......
};
等於讓 Webpack 知道,對於 react 這個模塊就不要打包啦,直接指向 window.React 就好。不過別忘了加載 react.min.js,讓全局中有 React 這個變量。
咱們來看看性能,由於不用打包 React 了因此速度天然超級快,包也很小:
問題若是就這麼簡單地解決了的話,那我就不必寫這篇文章了,下面咱們加一個 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.js
module.exports = require('react/lib/ReactCSSTransitionGroup');
這個動畫模塊就只有一行代碼,惟一的做用就是指向 react 下面的一個子模塊,咱們再來看看這個子模塊是怎麼寫的:
// react模塊
// react/lib/ReactCSSTransitionGroup.js
var React = require('./React');
var ReactTransitionGroup = require('./ReactTransitionGroup');
var ReactCSSTransitionGroupChild = require('./ReactCSSTransitionGroupChild');
//....剩餘代碼忽略
這個子模塊又反回去依賴了 react 整個庫的入口,這就是拖累 Webpack 的罪魁禍首。
總而言之,問題是這樣產生的:
1.Webpack 發現咱們依賴了 react-addons-css-transition-group
2.Webpack 去打包 react-addons-css-transition-group 的時候發現它依賴了 react 模塊下的一個叫 ReactTransitionGroup.js 的文件,因而 Webpack 去打包這個文件。
3.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.config.js:
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 的選項中:
運行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.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 的選項中:
配置babel,讓它排除一些文件,當loader這些文件時不進行轉換,自動跳過;可在.babelrc文件中配置,示例:
{
"presets": [
"es2015"
],
"ignore":[
"jquery.js",
"jquery.min.js",
"angular.js",
"angular.min.js",
"bootstrap.js",
"bootstrap.min.js"
]
}