webpack-bundle-analyzer应用

788 阅读6分钟

前言

 借助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

直接看getViewerData方法,从中挑出部分代码来说

// 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越来越廋。

另外本人技术有限,讲的有错误的地方,还望指出!

参考文章

www.cnblogs.com/hss-blog/p/…

fanjunzhi.cn/posts/webpa…