前言
借助webpack-bundle-analyzer可以分析项目中各模块的大小,并生成代码分析报告,通过报告,可以帮助我们发现较大的包,并结合实际作出相应优化。
安装
npm install --save-dev webpack-bundle-analyzer
配置
由于公司的项目是用VUE来开发,因此这里是在vue.config.js中对webpack进行如下配置:
chainWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
if (process.env.npm_config_report) {
config
.plugin('webpack-bundle-analyzer')
.use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
.end();
config.plugins.delete('prefetch')
}
} }
分析
运行 npm run build --report
等到分析完毕后,会在浏览器打开一个项目打包的情况图
看下最左侧的黄色区块,这个echarts压缩后的大小居然有209.91k,虽然不是特别大,但是有必要看看,于是在项目中搜索一番,发现项目中只有一个页面使用到echarts中的折线图。
在代码中是这样引入的:
import echarts from 'echarts'
看似没有什么问题,但是echarts作为某度开发的Javascript 的图表库,功能虽然强大,可以实现各种图表的展示,但是体积可不小,如果项目中没有广泛使用,还是不要直接引入echarts比较好,最好能按需引入。
接下来百度大法一番,经过一番尝试,终于找到符合项目展示需要的引入方法,如下:
import echarts from 'echarts/lib/echarts';
import 'echarts/lib/chart/line';
import 'echarts/lib/component/tooltip';
import 'echarts/lib/component/legend';
修改引入方式后,再来运行下npm run build --report
,继续等待,接着分析的报告如下:
按需引入echarts后,引入echarts的大小由209.91k,缩小到80.19K,包大小减少了61.8%,这是令人满意的结果。接着看现在最大的包变成了我们自有的组件库,压缩完大小为120.97K,如下:
这里是否会有优化空间呢?于是看了下代码,项目中采用全局引入方式,结合实际情况,该组件库在此项目中大量使用(组件库中的组件基本上都使用了),显然如果再采用按需引入的方式,不仅会增加额外工作量,估计也无法获得满意的结果。
结合以上,得到分析报告后,需要结合项目实际情况,做出对应的优化措施。
到这里难道就完了吗?于是,在好奇心的驱使下,就跑到在github上把webpack-bundle-analyzer包clone了下来。
源码初探
1.目录结构
可以看到目录结构如下,此处采用的是3.6.0版本
2.源码文件分析
1)index.js
接下来就从src的入口文件index开始看:
const {start} = require('./viewer');
module.exports = {
start,
BundleAnalyzerPlugin: require('./BundleAnalyzerPlugin')
};
入口文件比较简单,exports出了start和BundleAnalyzerPlugin,由于BundleAnalyzerPlugin是在分析时使用的方法,于是打开BundleAnalyzerPlugin一探究竟
2)BundleAnalyzerPlugin
1.基本配置
this.opts = {
// 可以是`server`,`static`或`disabled`。
// 在`server`模式下,分析器将启动HTTP服务器来显示软件包报告。
// 在“静态”模式下,会生成带有报告的单个HTML文件。
// 在`disabled`模式下,你可以使用这个插件来将`generateStatsFile`设置为`true`来生成Webpack Stats JSON文件。
analyzerMode: 'server',
analyzerHost: '127.0.0.1',
reportFilename: 'report.html',
// defaultSizes: 应该是`stat`,`parsed`或者`gzip`中的一个
defaultSizes: 'parsed',
// openAnalyzer是否在浏览器中自动打开报告
openAnalyzer: true,
// 如果`generateStatsFile`为`true`,将会生成Webpack Stats JSON文件的名字。
generateStatsFile: false,
statsFilename: 'stats.json',
statsOptions: null,
excludeAssets: null,
logLevel: 'info',
// deprecated
startAnalyzer: true,
...opts,
analyzerPort: 'analyzerPort' in opts ? (opts.analyzerPort === 'auto' ? 0 : opts.analyzerPort) : 8888
}
根据需要可在掉用BundleAnalyzerPlugin方法时,传参覆盖默认参数即可。
2.apply方法
apply(compiler) {
this.compiler = compiler;
const done = (stats, callback) => {
callback = callback || (() => { });
const actions = [];
if (this.opts.generateStatsFile) {
actions.push(() => this.generateStatsFile(stats.toJson(this.opts.statsOptions)));
}
// Handling deprecated `startAnalyzer` flag
if (this.opts.analyzerMode === 'server' && !this.opts.startAnalyzer) {
this.opts.analyzerMode = 'disabled';
}
if (this.opts.analyzerMode === 'server') {
actions.push(() => this.startAnalyzerServer(stats.toJson()));
} else if (this.opts.analyzerMode === 'static') {
actions.push(() => this.generateStaticReport(stats.toJson()));
}
// ....
}
}
该方法中主要是根据基本配置中的参数,去调用对应的函数。例如analyzerMode为server时,调用startAnalyzerServer,开启一个http服务来展示报告,反之则调用generateStaticReport 生成html静态资源。
2)viewer.js
在BundleAnalyzerPlugin中是调用了viewer中的方法,因此来看下viewer.js做了什么。
async function startServer(bundleStats, opts) {
// 这儿包含一些基本配置的解析和处理
const {
port = 8888,
host = '127.0.0.1',
openBrowser = true,
bundleDir = null,
logger = new Logger(),
defaultSizes = 'parsed',
excludeAssets = null
} = opts || {};
const analyzerOpts = {logger, excludeAssets};
let chartData = getChartData(analyzerOpts, bundleStats, bundleDir);
// .....此处省略代码
const server = http.createServer(app); .....此处省略代码}
startServer启动node并打开浏览器(默认地址为127.0.0.1:8888)
接着看generateReport方法
async function generateReport(bundleStats, opts) {
const {
openBrowser = true,
reportFilename = 'report.html',
bundleDir = null,
logger = new Logger(),
defaultSizes = 'parsed',
excludeAssets = null
} = opts || {};
const chartData = getChartData({logger, excludeAssets}, bundleStats, bundleDir);
if (!chartData) return;
// ...
}
generateReport方法是将最终得到的数据放在配置文件中的statsFilename文件(默认为stats.json)和配置文件中reportFilename的文件(默认为report.html),接着就是通过getChartData方法获取最终数据
function getChartData(analyzerOpts, ...args) {
let chartData; const {logger} = analyzerOpts;
try {
chartData = analyzer.getViewerData(...args, analyzerOpts);
} catch (err) {
// ...
}
// ...
return chartData;
}
可以看到在getChartData中调用了analyzer中的getViewerData方法来生成数据,所以来看下analyzer是如何生成数据的。
3) analyzer
// Sometimes all the information is located in `children` array (e.g. problem in #10)
if (_.isEmpty(bundleStats.assets) && !_.isEmpty(bundleStats.children)) {
bundleStats = bundleStats.children[0];
}
// Picking only `*.js or *.mjs` assets from bundle that has non-empty `chunks` array
bundleStats.assets = _.filter(bundleStats.assets, asset => {
// Removing query part from filename (yes, somebody uses it for some reason and Webpack supports it)
asset.name = asset.name.replace(FILENAME_QUERY_REGEXP, '');
return FILENAME_EXTENSIONS.test(asset.name) && !_.isEmpty(asset.chunks) && isAssetIncluded(asset.name);
});
以上代码应该是做过滤操作,接下来继续看
try {
bundleInfo = parseBundle(assetFile);
} catch (err) {
// ...
}
这里使用了parseBundlef方法,在parseUtils中找到了该方法,里面主要的方法有2个
const ast = acorn.parse(content, {
sourceType: 'script',
// I believe in a bright future of ECMAScript!
// Actually, it's set to `2050` to support the latest ECMAScript version that currently exists.
// Seems like `acorn` supports such weird option value.
ecmaVersion: 2050
});
以上方法应该是将文件内容转换为AST,接下来使用 walker.recursive 遍历获取到的AST,生成数据。
walk.recursive(
ast,
walkState,
{
CallExpression(node, state, c) {
if (state.locations) return;
const args = node.arguments;
// ....
}
}
);
在analyzer中通过parseBundle方法获取到数据后,通过以下方法生成数组,数组中包含了bundle信息。
return _.transform(assets, (result, asset, filename) => {
result.push({
label: filename,
isAsset: true,
// Not using `asset.size` here provided by Webpack because it can be very confusing when `UglifyJsPlugin` is used.
// In this case all module sizes from stats file will represent unminified module sizes, but `asset.size` will
// be the size of minified bundle.
// Using `asset.size` only if current asset doesn't contain any modules (resulting size equals 0)
statSize: asset.tree.size || asset.size,
parsedSize: asset.parsedSize,
gzipSize: asset.gzipSize,
groups: _.invokeMap(asset.tree.children, 'toChartData')
});
}, []);
需要的数据已经处理好,再次回到viewer.js文件中
function getChartData(analyzerOpts, ...args) {
let chartData; const {logger} = analyzerOpts;
try {
chartData = analyzer.getViewerData(...args, analyzerOpts);
} catch (err) {
// ...
}
// ...
return chartData;
}
对于获取到的chartData数据,经过viewer.js中的startServer或者generateReport方法处理,就生成了我们需要展示的图形化分析报告了。
总结
通过webpack-bundle-analyzer,我们知道了每个bundle的内容、大小等,就能有针对性的优化,让bundle越来越廋。
另外本人技术有限,讲的有错误的地方,还望指出!
参考文章