彻底解决Webpack打包慢的问题:npmrunbuild:dll
问题分析
我们先来看⼀下完全没有任何优化的时候,Webpack 的打包速度(使⽤了jsx和babel的loader)。下⾯是我们的⽂件:
//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");
运⾏
webpack test.js
在我的2015款RMBP13,i5处理器,全SSD下,性能是这样的:
没错你没有看错,这⼏个第三⽅轮⼦加起来有整整668个模块,全部打包需要20多秒。
这意味着什么呢?你每次对业务代码的修改,gulp 或者 Webpack 监测到后都会重新打包,你要⾜⾜等20秒才能看到⾃⼰的修改结果。
但是需要重新打包的只有你的业务代码,这些第三⽅库是完全不⽤重新打包的,它们的存在只会拖累打包性能。所以我们要⼀些⽅法来优化这个过程。
配置externals
Webpack 可以配置 externals 来将依赖的库指向全局变量,从⽽不再打包这个库,⽐如对于这样⼀个⽂件:
import React from 'react';
console.log(React);
如果你在 fig.js 中配置了externals:
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.js
这个动画模块就只有⼀⾏代码,唯⼀的作⽤就是指向 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');
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。使⽤这个功能需要把打包过程分成两步:
1. 打包ddl包
2. 引⽤ddl包,打包业务代码
⾸先我们来打包ddl包,⾸先配置⼀个这样的fig.js:
const webpack = require('webpack');
const vendors = [
'react',
'react-dom',
'react-router',
// ...其它库
];
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是解析包路径的上下⽂,这个要跟接下来配置的 fig.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,
// ............
}
}
接下来我们就可以快乐地打包业务代码啦,⾸先写好打包配置⽂件fig.js:
const webpack = require('webpack');
output: {
path: 'build',
react router6filename: '[name].js',
},
entry: {
app: './src/index.js',
},
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./manifest.json'),
}),
],
};
webpack.DllReferencePlugin 的选项中:
context需要跟之前保持⼀致,这个⽤来指导 Webpack 匹配manifest中库的路径;
manifest⽤来引⼊刚才输出的manifest.json⽂件。
DllPlugin 本质上的做法和我们⼿动分离这些第三⽅库是⼀样的,但是对于包极多的应⽤来说,⾃动化明显加快了⽣产效率。
PS:
其实还有⼀个速度的优化点,就是配置babel,让它排除⼀些⽂件,当loader这些⽂件时不进⾏转换,⾃动跳过;可在.babelrc⽂件中配置,⽰例:
{
"presets": [
"es2015"
],
"ignore":[
"jquery.js",
"jquery.min.js",
"angular.js",
"angular.min.js",        "bootstrap.js",
"bootstrap.min.js"    ]
}