2021 / 07 / 29

webpack2.x构建速度优化

本文字数: 8590阅读时间: 21分钟

最近一直在改老项目的东西,但是因为体积太大的问题,热更新每次都要等好几秒,打📦发布测试环境也需要6-7分钟的时间,所以每次一个小改动需要发布就要等很久,加上之前说要优化打包速度,所以还是决定再折腾一下这个项目。

方案

这里因为babel已经升级到7了所以,想用happyPack + DllPlugin + babel7来试试看能提升多少速度。 总体来说修改了一些webpack的配置和babel的配置,去掉了一些无用的依赖,package.json里面browserlist变成了 browserlist,更换了一些webpack的插件,主要有下:

移除的依赖

{
    "babel-core": "^6.26.3",
        "babel-plugin-syntax-jsx": "^6.18.0",  
  "babel-plugin-transform-runtime": "^6.22.0",
  "babel-preset-latest": "^6.22.0",  
"babel-preset-stage-2": "^6.22.0",  
"babel-register": "^6.26.0",  
"babel-runtime": "^6.23.0",
}

更新或新增的依赖

"@babel/polyfill": "^7.6.0",  
"@babel/runtime": "^7.6.3",
"@babel/core": "^7.0.0",  
"@babel/plugin-external-helpers": "^7.2.0",  
"@babel/plugin-proposal-class-properties": "^7.0.0",  
"@babel/plugin-proposal-decorators": "^7.0.0",  
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",  
"@babel/plugin-proposal-function-sent": "^7.0.0",  
"@babel/plugin-proposal-json-strings": "^7.0.0",  
"@babel/plugin-proposal-numeric-separator": "^7.0.0",  
"@babel/plugin-proposal-throw-expressions": "^7.0.0",  
"@babel/plugin-syntax-dynamic-import": "^7.0.0",  
"@babel/plugin-syntax-import-meta": "^7.0.0",  
"@babel/plugin-syntax-jsx": "^7.0.0",  
"@babel/plugin-transform-runtime": "^7.6.2",  
"@babel/preset-env": "^7.0.0",  
"@babel/preset-es2015": "^7.0.0-beta.53",  
"@babel/preset-typescript": "^7.6.0",  
"@babel/register": "^7.0.0",  
"@babel/runtime-corejs2": "^7.0.0",
"@vue/babel-plugin-transform-vue-jsx": "^1.0.0",
"babel-loader": "^8.0.0",
"clean-webpack-plugin": "^3.0.0",
"happypack": "^5.0.1",  
"html-webpack-include-assets-plugin": "^2.0.0",
"uglifyjs-webpack-plugin": "^1.3.0",
"webpack-bundle-analyzer": "^2.13.1",
"webpack-merge": "^2.6.1",  
"webpack-parallel-uglify-plugin": "^1.1.2"

开工

happypack

这里用的是happypack开启多线程打包,达到加速的效果,首先安装happypack的依赖.然后再在webpack.base.js增加配置,达到dev或者prod环境都可以加速的效果。

引入必要的依赖

const HappyPack = require('happypack') // 引用happypack
const os = require('os'); // 获取系统配置
// 开启的线程池默认为4,这里直接取的是电脑的cpu线程数
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })

然后用happy-loader代替babel-loader,并且开启babel-loader的缓存,这里要注意,loader后面的id=happyPack对应的plugins里的id识,happypack不仅可以多线程打包js也可以打包css。

// module.rules
{
    test: /\.js$/,
    // loader: 'babel-loader',  
    loader: 'happypack/loader?id=happyPack',
    include: [
      resolve('src'),
      resolve('test'),
     ],
    exclude: /node_modules/
},
// plugins
new HappyPack({  
  //用id来标识 happypack处理那里类文件  
  id: 'happyPack',  
  //如何处理  用法和loader 的配置一样  
  loaders: [{  
  loader: 'babel-loader',  
  // here you configure babel:  
  options: { babelrc: true, cacheDirectory: true }  
 }],  //共享进程池  
  threadPool: happyThreadPool,  
  //允许 HappyPack 输出日志  
  verbose: true,  
}),

DllPlugin 和 DllReferencePlugin

Dllplgin插件的概念类似于C的动态链接库的作用,将更新次数很少或者几乎不更新的包打包在一起,拆分bundles,提升构建的速度,通过manifest.json确定包的版本和哈希值, 再用DllReferencePlugin使用该json文件来做映射依赖性。

首先我们先修改/config/index.js新增我们的dll配置方便引用,再在/build的目录下新增webpack.dll.conf.js用来打包dll。

/config/index.js

// 新增配置
...
dll: {  
  static: path.resolve(__dirname, '../static/lib'),  // dll存放路径 相对于项目的static/lib
  list: {  // 多个包列表用来区分
      vendor: ['vue/dist/vue.common.js','vue-router', 'vuex'],  
      'asset': ['superagent']  
}

/build/webpack.dll.conf.js

var path = require('path')

var webpack = require('webpack')
var CleanWebpackPlugin = require('clean-webpack-plugin')

var config = require('../config')
var __root = path.resolve(__dirname, '../')

module.exports = {
    context: __root, // 这里配置的context就是后面dll的context
    entry: config.dll.list,
    output: {
        path: config.dll.static,
        filename: '[name]-[chunkhash:7].dll.js',
        library: 'lib_[name]',
        // *** 这里不要添加libraryTarget,否则webpack打包时会出错。
        // (提示是__WEBPACK__EXTERNAL__MODULE__xxx未定义) ***
        // libraryTarget: 'umd'
    },
    resolve: {
        modules: [path.resolve(__root, 'node_modules')],
        extensions: ['.js', '.json'],
        alias: {
            'vue$': 'vue/dist/vue.esm.js'
        }
    },
    // 这里没有写loaders,如果有需要可以自行添加loaders
    plugins: [
        // *** 这里很关键 ***
        new webpack.DllPlugin({ // 因为上面写了context,所以这里可以不指定context
            // 这里manifest的名字必须要有变量,因为类似上面的core和asset会分别创建一个manifest,
            // 如果名称相同,manifest会生成不规范的json,在引用时会报错。
            path: path.resolve(__root, 'static/manifest/[name].manifest.json'),
            name: 'lib_[name]' // *** 这里的名字必须与output.library一致 ***
        }),
        // 这个是用来稳定hash值,防止出现webpack的hash出现莫名的变化
        new webpack.HashedModuleIdsPlugin(),
        new webpack.NamedChunksPlugin(),
    ]
}

然后在package.json添加"build:dll": "webpack --config build/webpack.dll.conf.js"用来生成dll,执行一下试试。

执行dll打包结果[ 执行dll打包结果 ]

在项目的static/lib的目录下会生成对应的dll.js, 而且在static/manifest目录下也会生成对应的manifest.json。我们在每次打包之后需要将他们一起上传到git/svn等源码仓库,因为几乎不需要更新或者很少更新,所以只需要每次执行完之后记得上传到源码仓库即可。如果不手动上传,那么在服务器打包代码之前一定要执行一次npm run build:dll命令

打包完dll之后,需要修改webpack.base.js让我们的在编译/打包的时候感受构建速度的提升。

webpack.base.js

const HTMLWebpackIncludeAssetsPlugin = require('html-webpack-include-assets-plugin')  // 把我们的dll引入html
const CopyWebpackPlugin = require('copy-webpack-plugin') // 将dll文件拷贝到你的dist目录下
const __root = path.resolve(__dirname, '../')
// 获取dll文件的manifest
function getDllManifest() {
    var plugins = []
    Object.keys(config.dll.list).forEach((name) => {
        plugins.push(
            new webpack.DllReferencePlugin({
                context: __root, // 这里的context必须与DllPlugin中的context保持一致
                manifest: path.resolve(__root, 'static/manifest/[name].manifest.json').replace(
                    /\[name\]/gi, name)
            })
        )
    })
    return plugins
}

//plugins

plugins: [
    //...
    ...getDllManifest(),
    new HTMLWebpackIncludeAssetsPlugin({
        append: false,
        publicPath: config.build.assetsPublicPath,
        assets: [{
            path: config.build.assetsSubDirectory,
            glob: '**/*.js',
            globPath: config.dll.static
        }],
    }),
]

这样DllPlugin 和 DllReferencePlugin的配置基本就完成了

其他构建的小优化和坑

之前构建的时候会一直看到这样的错误: # DefaultsError: 'warnings' is not a supported option 是因为UglifyJsPlugin更新了,配置方式不一样了,解决方案就是引用外部的uglifyjs-webpack-plugin)插件代替webpack自带的。

修改如下:

// new webpack.optimize.UglifyJsPlugin({  
//   compress: {  
//     warnings: false  
//   },  
//   sourceMap: true  
// }),
new UglifyJsPlugin({  
  cache: '.cache/',  
  parallel: true,  
}),

然后在dev环境中新增BundleAnalyzerPlugin用来查看到底是什么依赖或者哪个文件太大,可以针对性的做代码优化

babe7升级

升级到babel7非常简单,可以看官网升级指南。 其实主要更新有以下几点: 1. 自带支持TypeScript的支持,ts-loader或许可以不用了? 2. 移除stage-* 3. 以下预设都归并到"@babel/preset-env"了 - babel-preset-es2015 - babel-preset-es2016 - babel-preset-es2017 - babel-preset-latest - A combination of the above ^

所以这里展示一下升级后.bablerc,仅供参考~

.bablerc

// {
//   "presets": [
//     ["latest", {
//       "es2015": { "modules": false }
//     }],
//     "stage-2"
//   ],
//   "plugins": ["transform-runtime", "transform-vue-jsx"],
//   "comments": false,
//   "env": {
//     "test": {
//       "presets": ["latest", "stage-2"],
//       "plugins": [ "istanbul" ]
//     }
//   }
// }

{  
  "presets": ["@babel/preset-env", "@babel/preset-typescript", "@vue/babel-preset-jsx"],  
  "plugins": ["@babel/plugin-transform-runtime", "@babel/plugin-external-helpers"]  
}

测试

到这里公司的元老级项目基本已经优化完毕了,在本地和服务器测试一下打包速度的区别试试.

mac 6核12线程

更新前打包速度

mac-更新前打包速度[ mac-更新前打包速度 ]

更新后打包速度

mac-更新后打包速度[ mac-更新后打包速度 ]

centos 单核双线程

更新前打包速度

centos-更新前打包速度[ centos-更新前打包速度 ]

更新后打包速度

centos-更新后打包速度[ centos-更新后打包速度 ]

可以看到,无论是本地还是服务器,提升速度约等于3/5,相对于每次提交CI/CD走下来6-7分钟还是有很大提升的,起码泡☕️变成泡速溶☕️了hhhh。

总结

总的来说,在这些方法的推动下的构建速度优化还是有着肉眼可见的提升速度的,这里基于webpack2.x的提升方案的效果因项目而异,可能效果很大,也可能没有效果,这里只是提供一些思路,如果有更好的方案或者不足可以在评论区指出,谢谢🙏~

更多阅读