Webpack 是 OneAPM 前端技术栈中很重要的一部分,它非常好用,如果你还不了解它,建议你阅读这篇 Webpack 入门指迷 ,在 OneAPM 我们用它完成静态资源打包,ES6 代码的转换 ,React 组件的组织等,在接下来的日子里,我们将通过一系列文章和业界分享我们在使用 Webpack 过程中关于性能方面的经验。
作为系列文章的第一篇,我们会重点介绍 Webpack 中的 resolve.alias
,也就是请求重定向。不过请注意 Webpack 里的请求是对模块的依赖,也就是一个 require
语句,而不是一个 HTTP 请求。
要实现的功能很简单,就是在页面上用中文显示当前时间,需要用到 moment 这个库,这个库封装了很多和日期相关的函数,而且自带了国际化的支持。
使用 npm init
初始化你的项目,然后通过npm install moment -D
加上 moment
的开发者依赖。
新建一个entry.js
作为入口文件,当然你也可以用 app.js
这样的名字,只是大部分的 Webpack 示例都是用的是 entry.js
。
var moment = require('moment');
document.write(moment().locale('zh-cn').format('LLLL'));
新建一个页面index.html
, 引用 bundle.js
:
<body>
<h5>当前时间:h5>
<script src="dist/bundle.js">script>
body>
此时的文件目录看起来是这样的:
index.html
package.json
entry.js
node_modules/moment
到目前为止 bundle.js
这个文件还不存在,不过别着急,接下来的工作就交给 Webpack 来完成。
index.html ------------------------+
package.json |
+--> <Clock App>
entry.js --------+ |
+-->bundle.js+--+
node_modules/moment-+
如图,Webpack 会把 entry.js
和 moment
模块一起打包成一个 bundle.js
文件,和 index.html
一起构成了我们的 Clock App。怎么样,是不是已经听到 Clock App 滴答作响了?
在命令行执行:
webpack --entry ./entry.js --output-path dist --output-file bundle.js
你会看到类似下面的输出结果:
Hash: bf9007fb1e0cb30e3ef7
Version: webpack 1.10.0
Time: 650ms
Asset Size Chunks Chunk Names
bundle.js 378 kB 0 [emitted] null
[0] ./entry.js 125 bytes {0} [built]
+ 86 hidden modules
可以看到,耗时 650ms,这么慢着实让人意外,一定要想办法提高“新一代神器”速度;另一方面,最后一行的 + 86 hidden modules 非常让人怀疑:明明是一个简单的 Clock App,怎么会有这么多的依赖。
再一次,在命令行输入:
webpack --entry ./entry.js --output-path dist --output-file bundle.js \
--colors \
--profile \
--display-modules
不过这次新增加了三个参数,这三个参数的含义分别是:
--colors
输出结果带彩色,比如:会用红色显示耗时较长的步骤--profile
输出性能数据,可以看到每一步的耗时--display-modules
默认情况下 node_modules
下的模块会被隐藏,加上这个参数可以显示这些被隐藏的模块 Hash: bf9007fb1e0cb30e3ef7
Version: webpack 1.10.0
Time: 650ms
Asset Size Chunks Chunk Names
bundle.js 378 kB 0 [emitted] null
[0] ./entry.js 125 bytes {0} [built]
factory:11ms building:8ms = 19ms
[1] ../~/moment/moment.js 102 kB {0} [built]
[0] 19ms -> factory:7ms building:141ms = 167ms
[2] (webpack)/buildin/module.js 251 bytes {0} [built]
[0] 19ms -> [1] 148ms -> factory:132ms building:159ms = 458ms
[3] ../~/moment/locale ^\.\/.*$ 2.01 kB {0} [optional] [built]
[0] 19ms -> [1] 148ms -> factory:6ms building:10ms dependencies:113ms = 296ms
[4] ../~/moment/locale/af.js 2.57 kB {0} [optional] [built]
[0] 19ms -> [1] 148ms -> [3] 16ms -> factory:52ms building:65ms dependencies:138ms = 438ms
..... 广告分割线,Node.js 工程师简历请发 nodejs@oneapm.com ......
[85] ../~/moment/locale/zh-cn.js 4.31 kB {0} [optional] [built]
[0] 22ms -> [1] 162ms -> [3] 18ms -> factory:125ms building:145ms dependencies:22ms = 494ms
[86] ../~/moment/locale/zh-tw.js 3.07 kB {0} [optional] [built]
[0] 22ms -> [1] 162ms -> [3] 18ms -> factory:126ms building:146ms dependencies:21ms = 495ms
从命令行的结果里可以看到从 Request[4] 到 Request[86] 都是在解析 moment.js
附带的大量本地化文件。所以我们遇到的速度慢的问题其实是由 moment
引起的。
如果你想知道为什么 Webpack 会加载这么多的模块,可以参考这篇文章 Why Enormous Locales During Webpack MomentJS
我们再来看看 entry.js
代码的第一行,标准的 CommonJS
写法:
var moment = require('moment');
也就是说,请求的是 moment
的源码。实际上,通过 NPM 安装moment
的时候会同时安装 moment
的源码和压缩后的代码,试验证明下面这种写法也是可行的:
var moment = require('moment/min/moment-with-locales.min.js');
只不过这样改,可读性会有所下降,而且每一个用到moment
的地方都得这么写。另外,如果同样的问题出现在第三方模块中,修改别人代码就不那么方便了。下面来看看用 Webpack 怎么解决这个问题。
别名(resolve.alias
) 是 Webpack 的一个配置项,它的作用是把用户的一个请求重定向到另一个路径,例如通过修改 webpack.config.js
配置文件,加入:
resolve: {
alias: {
moment: "moment/min/moment-with-locales.min.js"
}
}
这样待打包的脚本中的 require('moment')
; 其实就等价于 require('moment/min/moment-with-locales.min.js');
。通过别名的使用在本例中可以减少几乎一半的时间。
Hash: cdea65709b783ee0741a
Version: webpack 1.10.0
Time: 320ms
Asset Size Chunks Chunk Names
bundle.js 148 kB 0 [emitted] main
[0] ./entry.js 125 bytes {0} [built]
factory:11ms building:9ms = 20ms
[1] ../~/moment/min/moment-with-locales.min.js 146 kB {0} [built] [1 warning]
[0] 20ms -> factory:8ms building:263ms = 291ms
[2] (webpack)/buildin/module.js 251 bytes {0} [built]
[0] 20ms -> [1] 271ms -> factory:3ms building:1ms = 295ms
WARNING in ../~/moment/min/moment-with-locales.min.js
Module not found: Error: Cannot resolve 'file' or 'directory' ./locale in */webpack_performance/node_modules/moment/min
@ ../~/moment/min/moment-with-locales.min.js 1:2731-2753
Webpack
中忽略对已知文件的解析module.noParse
是 webpack
的另一个很有用的配置项,如果你 确定一个模块中没有其它新的依赖 就可以配置这项,webpack
将不再扫描这个文件中的依赖。
module: {
noParse: [/moment-with-locales/]
}
这样修改,再结合前面重命名的例子,更新后的流程是:
webpack
检查到 entry.js
文件对 moment
的请求;alias
重定向,转而请求 moment/min/moment-with-locales.min.js;
noParse
规则中的 /moment-with-locales/
一条生效,所以 webpack
就直接把依赖打包进了 bundle.js
。Hash: 907880ed7638b4ed70b9
Version: webpack 1.10.0
Time: 76ms
Asset Size Chunks Chunk Names
bundle.js 147 kB 0 [emitted] main
[0] ./entry.js 125 bytes {0} [built]
factory:13ms building:13ms = 26ms
[1] ../~/moment/min/moment-with-locales.min.js 146 kB {0} [built]
[0] 26ms -> factory:13ms building:5ms = 44ms
时间进一步被压缩,只需要 76ms,比前一步还减少了 75%。
Webpack 是如此的强大,用其打包的脚本可以运行在多种环境下,Web 环境只是其默认的一种,也是最常用的一种。考虑到 Web 上有很多的公用 CDN 服务,那么 怎么将 Webpack 和公用的 CDN 结合使用呢?方法是使用 externals
声明一个外部依赖。
externals: {
moment: true
}
当然了 HTML 代码里需要加上一行
这次打包,结果只用了 49 ms,几乎达到了极限。
本文结合本地时钟的例子,展示了定位 Webpack 性能问题的步骤,以及所需要的两个参数 :--display-modules
和 --profile
。然后,重点介绍了 resolve.alias
即利用别名做重定向的方法和场景,在此基础上,配合module.noParse
忽略某些模块的解析可以进一步加快速度。最后介绍了用 externals
定义外部依赖方法来使用公用 CDN。
本文相关的源码在: https://github.com/wyvernnot/webpack_performance/tree/master/moment-example;
本文系OneAPM工程师原创文章。OneAPM是中国基础软件领域的新兴领军企业,能帮助企业用户和开发者轻松实现:缓慢的程序代码和SQL语句的实时抓取。想阅读更多技术文章,请访问OneAPM官方技术博客。