Webpack打包慢问题的完美解决方法


Posted in Javascript onMarch 16, 2017

前言

这几天写腾讯实习生 Mini 项目的时候用上了 React 全家桶,当然同时引入了 Webpack 作为打包工具。但是开发过程中遇到一个很棘手的问题就是,React 加上 React-Router、superagent、eventproxy 这些第三方轮子一共有好几百个 module,Webpack 的打包速度极慢。这对于开发是非常不好的体验,同时效率也极低。

问题分析

我们先来看一下完全没有任何优化的时候,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下,性能是这样的:

Webpack打包慢问题的完美解决方法

没错你没有看错,这几个第三方轮子加起来有整整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 了所以速度自然超级快,包也很小:

Webpack打包慢问题的完美解决方法

配置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
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。使用这个功能需要把打包过程分成两步:

  1. 打包ddl包
  2. 引用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 的选项中:

  1. path 是 manifest.json 文件的输出路径,这个文件会用于后续的业务代码打包;
  2. name 是dll暴露的对象名,要跟 output.library 保持一致;
  3. 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 的选项中:

  1. context 需要跟之前保持一致,这个用来指导 Webpack 匹配 manifest 中库的路径;
  2. manifest 用来引入刚才输出的 manifest.json 文件。

DllPlugin 本质上的做法和我们手动分离这些第三方库是一样的,但是对于包极多的应用来说,自动化明显加快了生产效率。

总结

以上就是关于彻底解决Webpack打包慢问题的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
5 cool javascript apps
Mar 24 Javascript
JavaScript与Image加载事件(onload)、加载状态(complete)
Feb 14 Javascript
关于javaScript注册click事件传递参数的不成功问题
Jul 18 Javascript
js实现的类似于asp数据字典的数据类型代码实例
Sep 03 Javascript
探讨js字符串数组拼接的性能问题
Oct 11 Javascript
jquery实现的点击翻书效果代码
Nov 04 Javascript
基于javascript html5实现3D翻书特效
Mar 14 Javascript
如何编写一个d.ts文件的步骤详解
Apr 13 Javascript
vue的列表交错过渡实现代码示例
May 05 Javascript
解决vue2中使用elementUi打包报错的问题
Sep 22 Javascript
vue自定义树状结构图的实现方法
Oct 18 Javascript
详解ES6 中的Object.assign()的用法实例代码
Jan 11 Javascript
div中文字内容溢出常见的解决方法
Mar 16 #Javascript
JavaScript实现类似淘宝的购物车效果
Mar 16 #Javascript
原生js更改css样式的两种方式
Mar 15 #Javascript
js清除浏览器缓存的几种方法
Mar 15 #Javascript
从零开始做一个pagination分页组件
Mar 15 #Javascript
Vuex模块化实现待办事项的状态管理
Mar 15 #Javascript
javascript图片预览和上传(兼容IE)
Mar 15 #Javascript
You might like
解决phpmyadmin中文乱码问题。。。
2007/01/18 PHP
php中mail函数发送邮件失败的解决方法
2014/12/24 PHP
laravel容器延迟加载以及auth扩展详解
2015/03/02 PHP
基于php的微信公众平台开发入门实例
2015/04/15 PHP
Json_encode防止汉字转义成unicode的方法
2016/02/25 PHP
PHP快速排序算法实现的原理及代码详解
2019/04/03 PHP
PHP获取真实IP及IP模拟方法解析
2020/11/24 PHP
改善你的jQuery的25个步骤 千倍级效率提升
2010/02/11 Javascript
javascript中tostring()和valueof()的用法及两者的区别
2015/11/16 Javascript
微信小程序 textarea 详解及简单使用方法
2016/12/05 Javascript
BootStrap表单控件之复选框checkbox和单选择按钮radio
2017/05/23 Javascript
JavaScript实现快速排序的方法分析
2018/01/10 Javascript
vue实现点击选中,其他的不选中方法
2018/09/05 Javascript
使用Angular自定义字段校验指令的方法示例
2019/02/01 Javascript
8个有意思的JavaScript面试题
2019/07/30 Javascript
JavaScript组合设计模式--改进引入案例分析
2020/05/23 Javascript
JavaScript实现图片合成下载的示例
2020/11/19 Javascript
windows下安装python paramiko模块的代码
2013/02/10 Python
python数组过滤实现方法
2015/07/27 Python
python下10个简单实例代码
2017/11/15 Python
Python 网络爬虫--关于简单的模拟登录实例讲解
2018/06/01 Python
python版opencv摄像头人脸实时检测方法
2018/08/03 Python
python实现自动获取IP并发送到邮箱
2018/12/26 Python
python调用百度地图WEB服务API获取地点对应坐标值
2019/01/16 Python
Pandas时间序列:时期(period)及其算术运算详解
2020/02/25 Python
python数据预处理 :样本分布不均的解决(过采样和欠采样)
2020/02/29 Python
django ListView的使用 ListView中获取url中的参数值方式
2020/03/27 Python
详解如何通过H5(浏览器/WebView/其他)唤起本地app
2017/12/11 HTML / CSS
Weekendesk意大利:探索多种引人入胜的周末主题
2016/10/14 全球购物
全球性的在线鞋类品牌:Public Desire
2019/04/03 全球购物
Java 中访问数据库的步骤?Statement 和PreparedStatement 之间的区别?
2012/06/05 面试题
学习党课思想汇报
2013/12/29 职场文书
餐厅考勤管理制度
2014/01/28 职场文书
2015年安全生产目标责任书
2015/01/29 职场文书
教师个人年度总结
2015/02/11 职场文书
《狮子和鹿》教学反思
2016/02/16 职场文书