前端开发中不可忽视的Webpack 打包优化

422 阅读7分钟

现在大多前端项目开发基本上都用到Webpack或其他构建工具,特别是中大型企业项目,随着项目功能架构逐渐增大,Webpack打包构建的会出现一些构建速度变慢,等待时间变长等问题;下面是一些平常学习收集到常用的一些解决打包构建速度以及提高开发体验和效率的方案:

HappyPack 开启多进程打包

因为Webpack是单线程的, 假设一个模块依赖其他几个模块,则webpack必须对这些模块逐个进行转译;HappyPack 以此为切点它的核心特性是可以开启多个线程,并行地对不同模块进行转译,从而充分利用本地的计算资源来提升打包速度。

1. 单个loader优化

//webpack.config.js
const HappyPack =  require('happypack');
module.exports = {
    //...
    module: {
        rules: [{
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'happypack/loader'
        }]
    },
    plugins: [
        new HappyPack({
            loader: [{
                loader: 'babel-loader',
                options: {
                    presets: ['react']
                }
            }]
        })
    ]
}

在module.rules 中, 使用happypack/loader 替换了原有的babel-loader,并在plugins中添加了HappyPack的插件,将原有的babel-loader连同它的配置插入进去了。

2. 多个loader的优化

//webpack.config.js
const HappyPack =  require('happypack');
module.exports = {
    //...
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'happypack/loader?id=js'
            },
            {
                test: /\.ts$/,
                exclude: /node_modules/,
                loader: 'happypack.loader?id=ts'
            }
        ]
    },
    plugins: [
        new HappyPack({
            id: 'js',
            loaders: [{
                loader: 'babel-loader',
                options: {},//babel options
            }]
        }),
        new HappyPack({
            id: 'ts',
            loaders: [{
                loader: 'ts-loader',
                options: {},//ts options
            }]
        })
    ]
}

使用多个HappyPack loader, 也就意味着要插入多个HappyPack的插件,每个插件加上id 来标识. 同时我们还可以为每个插件设置具体不同的配置项,如使用的线程数、是否开启debug模式等。

缩小打包作用域

从宏观角度来看,提升性能的方法无非两种:增加资源或者缩小范围

增加资源是使用更多CPU和内存,用更多的计算能力来缩短执行任务的时间,缩小范围则指任务本身,比如去掉冗余的流程,尽量不做重复性的工作等。

1. exclude 和 include

exclude 排除掉特定的目录

module.exports = {
    //...
    module: {
        rules: [{
            test: /\.(js|jsx)$/,
            exclude: [/node_modules/, /static\/dist/],
            use: [{
                loader: 'babel-loader',
                options: {} //babel options
            }]
        }]
    }
}

include 只生效特定的目录

module.exports = {
    //...
    module: {
        rules: [{
            test: /\.js$/,
            include: /src/,
            loader: 'babel-loader',
        }]
    }
}

当exclude 和 include 规则有重叠的部分时, exclude 的优先级更高。

2. IngorePlugin忽略无用文件

Webpack 内置 IgnorePlugin插件,它可以完全排除一些模块,被排除模块即便被引用了,也不会被打包进资源文件中。

IgnorePlugin 可以去除一些只在本地使用的库文件,类似Moment.js处理日期相关的,可以用IgnorePlugin打包去掉。

//webpack.config.js
 {
    plugins: [
        // 忽略moment下的 /locale 目录
        new webpack.IgnorePlugin(/\.\/locale/, /moment/)
    ]
  }
  
 //业务代码
 import moment from 'moment';
 import 'moment/locale/zh-cn' //手动引入中文语言包
 moment.locale('zh-cn') //设置语言问中文
 console.log('locale', moment.locale())
 console.log('date', moment().format('ll')) //2022年xx月xx日
 

3. noParse

对于有些库,我们希望Webpack 完全不要去进行解析,即不希望应用于任何loader规则,库的内部也不会有其他模块的依赖, 那么这时可以使用noParse实现。

module.exports = {
    //...
    module: {
        noParse: /lodash/
    }
}


上面配置将会忽略所有文件中包含的lodash的模块,这些模块仍然会被打包进资源文件,只不过webpack不会对其进行任何解析。

上面三种方法的区别:

exclude 和 include 是确定loader 的规则范围, noParse是不去解析但仍会打包到bundle中, IgnorePlugin插件,它可以完全排除一些模块,被排除模块即便被引用了,也不会被打包进资源文件中。

4. 缓存利用

使用缓存也可以有效减少 Webpack 的重复工作,进而提升打包效率;Webpack 5 引入了一个新的缓存配置项.在默认情况下,它会在开发模式中开启,在生产环境下禁用。

module.exports = {
    //...
    cache: true
}

通过 true or false 控制的其实只是Webpack基于内容的缓存。

Webpack 还支持另外一种基于文件系统的缓存,这种缓存机制必须要强制开启才会生效:

module.exports = {
    //...
    cache: {
        type: 'filesystem'
    }
}

这里cache配置项是对象类型,并指明缓存类型文件系统.同时我们也可以传入更多的配置项来进行更细致的缓存管理。 相比构建的性能,Webpack更注重于构建的正确性--使用文件系统缓存可能会带来一定的风险。 Webpack直接采用文件系统缓存,可能引发的各种问题, 例如:

  • 更改Webpack配置
  • 通过命令行传入不同的构建参数
  • loader,plugin或第三方包更新
  • Node.js, npm 或 yarn更新

上述都有可能引发缓存问题险。

解决缓存问题的风险的比较简单的方案是更新配置中的cache.version. 如:

module.exports = {
    //...
    cache: {
        type: 'filesystem',
        version: '<version_string>'
    }
}

动态链接库 和 DllPlugin

动态链接库是早期windows系统由于受限于当时计算机内存空间较小的问题而出现的一种内存优化的方法。 当一段相同的子程序被多个程序调用时,为了减少内存的消耗,可以将这段子程序存储为一个可执行文件,只在 内存中生成和使用同一个实例。

DllPlugin 借鉴了动态链接库的这种思路,对于第三方模块或者一些不常变化的模块,可以将它们预先编译和打包, 然后在项目实际构建过程中直接取用即可。当然通过DllPlugin实际生成的还是JS文件而不是动态链接库,取这个名字只是由于方法类似罢了,在打包vendor的时候还会附加生成一份vendor的模块清单,这份清单将会在工程业务模块打包的时候起到链接和索引的作用。

DllPlugin的配置打包流程:

1. 创建 webpack.dll.js

module.exports = {
    entry: {
      vendors: ['lodash'], //指定打包模块
      react: ['react', 'react-dom'] //指定打包模块
    },
    output: {
        filename: '[name].dll.js', //输出文件名称
        path: path.resolve(__dirname, '../dll'), //输出文件夹
        library: '[name]' //全局变量暴露
    },
    plugins: [
        new webpack.DllPlugin({ 
            name: '[name]',
            path: path.resolve(__dirname, '../dll/[name].manifest.json')
        })
    ]
}

配置中的entry指定了把哪些模块打包为vendors 和 react, plugins的部分我们引入了Dll-Plugin,并添加了下面的配置:

  • name: 导出的动态链接库的名字,它需要与output.library的值对应。
  • path: 资源清单的绝对路径,业务代码打包时将会使用这个清单进行模块索引。

2. package.json 配置

{
    "script": {
        "build:dll" "webpack --config ./build/webpack.dll.js"
    }
}

3. 配置 addAssetWebpackPlugin 插件

npm install add-asset-webpack-plugin --save-dev

// webpack.config.js 通过插件将webpack.dll.js 添加到页面中

const addAssetWebpackPlugin = require('add-asset-webpack-plugin');

{
    plugins: [
        new addAssetWebpackPlugin({
            filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
        })
    ]
}

4.vendor打包

执行命令: npm run build:dll

dll文件夹中生成: [name].manifest.json 和 [name].dll.js

5. DllReferencePlugin 配置映射

// webpack.config.js
{
    plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        }),
        new addAssetWebpackPlugin({
            filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
        }),
        new addAssetWebpackPlugin({
            filepath: path.resolve(__dirname, '../dll/react.dll.js')
        }),
        new webpack.DllReferencePlugin({ //Dll引用插件
            manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
        }),
        new webpack.DllReferencePlugin({ //Dll引用插件
            manifest: path.resolve(__dirname, '../dll/react.manifest.json')
        })
    ]
}

Webpack内置插件 DllReferencePlugin 做了映射后,引入三方模块,就优先去找dll/vendors.manifest.json映射关系,然后找到 dll文件夹的内容,找不到再去node_modules里面去找,很大程度上提高了打包速度。

对 webpack.config.js 配置的 plugins 优化,更适用大工程:

const pugins =  plugins: [
    new HtmlWebpackPlugin({
        template: 'src/index.html'
    })
]

const files = fs.readdirSync(path.resolve(__dirname, '../dll'))
files.forEach(file => {
    if(/.*\.dll.js/.test(file)){
        pugins.push(new addAssetWebpackPlugin({
            filepath: path.resolve(__dirname, '../dll/', file)
        }))   
    }
    if(/.*\.manifest.json/.test(file)){
        pugins.push(new webpack.DllReferencePlugin({ 
            manifest: path.resolve(__dirname, '../dll/', file)
        }))   
    }
})

动态读取dll文件夹后,想用DllPlugin打包多个vendor的时候,直接在entry新增打包模块即可。

需要DllPlugin场景:

  • 前端框架 Vue, React等, 体积比较大,构建慢;
  • 引用的模块较稳定,不常升级;
  • 同一个版本只构建一次即可,不用每次都重新构建;

DllPlugin总结:

DllPlugin 和代码分片有点类似,都可以用来提取公共模块,但本质上有一些区别;代码分片的思路是设置一些特定的规则并在打包的过程中根据这些规则提取模块。

DllPlugin则是将vendor完全拆出来,定义一套自己的 Webpack 配置并独立打包,在实际工程构建时侯就不用再对它进行任何处理,直接取用开即可。从理论上来说, DllPlugin 会比代码分片在打包速度上更胜一筹,但也相应地增加了配置,以及资源管理的复杂度。

去除死代码

当设置mode=production 时 Webpack 自动开启 除死代码(tree shaking)功能:

//index.js
import { sum } from './util.js';
sum(1,2);

// util.js 
export function sum(a, b){
   return a + b;
}

export function mult(a, b){//没有被任何其他模块引用,属于“死代码”
    return a * b;
}

Webpack 打包时会在mult方法添加一个标记,在正常开发模式下它仍然存在,只是在生产环境的压缩那一步会被移除掉。

切记: 去除死代码只能对ES6 Module静态引入生效; npm 包同时提供了ES6 Module和CommonJs两种形式,我们应该尽可能使用ES6 Module形式的模块,这样去除死代码效率更高。

其他方式

以上是笔记在学习Webpack过程中收集到的一些常用的打包优化的方法,当然还有一些其他方法例如:

  • resolve 参数合理配置(extensions,mainFiles, alias),因为有解析消耗
  • source-map合理使用, 溯源越详细,打包速度越慢
  • Plugin尽可能精简并确保可靠,尽量使用官方推荐的
  • 还有跟上技术的更新迭代(Node, Npm, Yarn)

如果还有其他的比较好的优化方案,欢迎评论区交流!