56

欢迎关注我的公众号睿Talk,获取我最新的文章:
clipboard.png

一、前言

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

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

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

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

如果你对 webpack 的工作原理感兴趣,可以看看我写的另一篇文章webpack启动代码源码解读

二、加快编译构建速度

有 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 的工作原理,有机会另写一篇文章分享。

如果你对 webpack 的工作原理感兴趣,可以看看我写的另一篇文章webpack启动代码源码解读


Dickens
5.5k 声望424 粉丝