webpack调优总结

  • 前言

webpack的出现为前端开发带来翻天覆地的变化,无论你是用React,Vue还是Angular,webpack都是主流的构建工具。我们每天都跟它打交道,但却很少主动去了解它,就像写字楼里的礼仪小姐姐,既熟悉又陌生。随着项目复杂度的上升,打包构建的时间会越来越长。终于有一天,你发现npm run dev后,去泡个茶,上了个厕所,跟同事bb一轮后回到座位,项目还没构建完的时候,你就会下定决心好好了解下这个熟悉的陌生人。

这次优化的目的主要有两个:

  • 加快编译构建速度
  • 减少页面加载的时间

现状是每次开发模式构建,大概要花120秒;生产模式构建,大概要花300秒。项目总共有将近150个chunk。

  • 加快编译构建速度

有2种方式可以加快编译的速度,分别是减少每次打包的文件数目,并行的去执行打包任务。这里用到了2个webpack插件:

  • DllPlugin(减少每次打包的文件数目)
  • HappyPack(并行的去执行打包任务)

下面对这两个插件作详细的介绍。

  • DllPlugin

dll是Dynamic Link Library(动态链接库)的缩写,是Windows系统共享函数库的一种方式。将一些比较少改变的库和工具,比如React、React-DOM,事先独立打包成一个chunk,以后每次构建的时候再直接导入,就不用每次都对这些文件打包了。这里有2个分解动作:

  • 独立打包dll
  • 导入dll

使用DllPlugin可以独立打包dll,具体的配置如下:

const path = require('path');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

const env = process.env.NODE_ENV;

module.exports = {

    entry: {
        vendor: ['react', 'react-dom', 'react-router', 'redux', 'react-redux', 'redux-thunk'],
    },

    output: {
        filename: '[name]_dll_[chunkhash].js',
        path: path.resolve(__dirname, 'dll'),
        library: '_dll_[name]',
    },

    resolve: {
        mainFields: ['jsnext:main', 'browser', 'main'],
    },

    plugins: [
        new webpack.DllPlugin({
            name: '_dll_[name]',
            path: path.join(__dirname, 'dll', '[name].manifest.json'),
        }),
        new webpack.DefinePlugin({
            'process.env': {
                NODE_ENV: JSON.stringify(env),
            },
        }),
        new UglifyJSPlugin({
            cache: true,
            parallel: true,
            exclude: [/node_modules/],
            uglifyOptions: {
                compress: {
                    warnings: false,
                    drop_console: true,
                    collapse_vars: true,
                    reduce_vars: true,
                },
                output: {
                    beautify: false,
                    comments: false,
                },
            },
        }),
    ],
};

DllPlugin网上有一些例子,但都不完美,体现在以下2点:

  • 没有压缩代码
  • 没有hash,当依赖更新时无法通知浏览器更新缓存

第1点比较好处理,加上DefinePlugin和UglifyJSPlugin就可以了。处理第2点的时候,除了在output加上chunkhash,在引入dll的时候需要做一些额外的操作,下文会讲解。

这时在package.json加上一个命令,npm run dll一下就会生成一个类似这样的文件:vendor_dll_be1f5270e490dcb25f.js

{
    ...
    "scripts": {
        "dll": "cross-env NODE_ENV=production webpack --config webpack.dll.js --progress"
    }
    ...
}

dll生成后,就要在构建的配置文件里将其引入,这时候就用到DllReferencePlugin和AddAssetHtmlPlugin,配置如下

const fs = require('fs');
const path = require('path');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');

const files = fs.readdirSync(path.resolve(__dirname, 'dll'));
const vendorFiles = files.filter(file => file.match(/vendor_dll_\w+.js/));
const vendorFile = vendorFiles[0];

module.exports = {
    ...
    plugins: [
        ...
        new webpack.DllReferencePlugin({
            manifest: require('./dll/vendor.manifest.json'),
        }),
        new AddAssetHtmlPlugin({
            filepath: path.resolve(__dirname, `dll/${vendorFile}`),
            includeSourcemap: false
        }),
        ...
    ],
};

DllReferencePlugin的作用是将打包好的dll文件传入构建的代码里面,而AddAssetHtmlPlugin的作用是在生成的html文件中加入dll文件的script引用。网上的例子一般是将dll的文件名直接写死的,但由于在上一步构建dll的时候加入了hash,所以要通过fs读取真实的文件名,再注入到html中。

  • HappyPack

大家都知道webpack是运行在node环境中,而node是单线程的。webpack的打包过程是io密集和计算密集型的操作,如果能fork多个进程并行处理各个任务,将会有效的算短构建时间,HappyPack就能做到这点。下面是它的相关配置:

const HappyPack = require('happypack');
const os = require('os');

const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.js$/,
                include: [
                    path.resolve(__dirname, 'src')
                ],
                use: [{
                    loader: 'happypack/loader?id=happyBabel',
                }],
            },
            {
                test: /\.css$/,
                include: [
                    path.resolve(__dirname, 'src')
                ],
                use: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: ['happypack/loader?id=happyCss'],
                }),
            }
        ],
        ...
        plugins: [
            ...
            new HappyPack({
                id: 'happyBabel',
                loaders: [{
                    loader: 'babel-loader',
                    options: {
                        cacheDirectory: true,
                        presets: ['react', 'es2015', 'stage-0'],
                        plugins: ['add-module-exports', 'transform-decorators-legacy'],
                    },
                }],
                threadPool: happyThreadPool,
                verbose: true,
            }),
            new HappyPack({
                id: 'happyCss',
                loaders: ['css-loader', 'postcss-loader'],
                threadPool: happyThreadPool,
                verbose: true,
            }),
        ],

其中happyThreadPool是根据cpu数量生成的共享进程池,防止过多的占用系统资源。

  • 减少页面加载时间

对于web应用来说,减少页面加载时间一般有2种方法。一是充分利用浏览器缓存,减少网络传输的时间。另外就是减少JS运行的时间,通过SSR等方式实现。利用webpack能有效的抽取出共享的资源,提高缓存的命中率。这里用到的插件除了上文提到的DllPlugin外,还有CommonsChunkPlugin,相关配置如下:

module.exports = {

    entry: {
        vendor: ['zent','lodash']
        app: ['babel-polyfill', 'react-hot-loader/patch', './src/main.js']
    },
    ...
    plugins: [
        ...
        new webpack.optimize.CommonsChunkPlugin({
            names: ['vendor'],
            minChunks: Infinity,
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'app',
            minChunks: 3,
            children: true,
            async: 'chunk-vendor',
        }),
        new webpack.optimize.CommonsChunkPlugin({
            names: ['manifest'],
            minChunks: Infinity,
        }),
        new webpack.HashedModuleIdsPlugin(),
        new InlineManifestWebpackPlugin({
            name: 'webpackManifest',
        }),
        ...
    ],
};

插件的第一部分是将vendor构建一个独立包;第二部分是抽取app入口文件code split之后所有子模块的公共模块,进一步减少子模块的大小;第三部分将webpack的启动代码独立打成一个manifest包,配合HashedModuleIdsPlugin可以保证vendor的hash不变。InlineManifestWebpackPlugin的作用是将manifest文件内联到html模板中,减少一次网络请求。

  • 总结

经过上述的优化之后,开发模式构建只需要60秒左右;生产模式构建只需要150秒左右,时间减少一半!缓存命中方面,可以做到基础模块(React等)和比较少变动的模块(组件库)分离出来,当组件库更新的时候依然可以使用基础模块的缓存(通过dll实现)。

通过这次的优化,对webpack的理解加深了不少,取得了比较不错的优化效果。另外也学习了loader和plugin的工作原理,有机会另写一篇文章分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章