webpack 学习笔记系列06-打包优化

2021-06-27 15:24:36 浏览数 (1)

webpack 学习笔记系列06-打包优化

Write By CS逍遥剑仙

我的主页: csxiaoyao.com

GitHub: github.com/csxiaoyaojianxian

Email: sunjianfeng@csxiaoyao.com

QQ: 1724338257

1. splitChunks 拆分代码

1.1 三种拆分方式

webpack 的三种代码拆分方式:

  • entry 入口配置
  • 使用 import()require.ensure 动态按需加载
  • webpack4 的 splitChunks 配置取代之前的 CommonsChunkPlugin

1.2 splitChunks 默认配置

splitChunks 默认配置对应上述的第二种按需加载方式:

代码语言:txt复制
module.exports = {
    // ... 
    optimization: {
        splitChunks: {
            chunks: 'async', // initial|all|async(默认)
          	maxInitialRequests: 3, // 初始化最大文件数,优先级高于 cacheGroup,为 1 时不抽取 initial common
          	maxAsyncRequests: 5, // 按需加载最大异步请求数量,为 1 时不抽取公共 chunk
            maxSize: 0, // 文件最大尺寸,0不限制
          	minSize: 30000, // 文件最小尺寸,默认30K,development 下10k,与 chunk 数量成反比
            minChunks: 1, // 默认 1,被提取的模块至少要在几个 chunk 中被引用,值越大,抽取出的文件越小
            automaticNameDelimiter: '~', // 打包文件名分隔符
            name: true, // 拆分的文件名,默认 true 自动生成文件名,若设置为固定字符串,则所有 chunk 合并成一个
            cacheGroups: {
                vendors: {
                    test: /[\/]node_modules[\/]/, // 正则规则,如果符合就提取 chunk 
                    priority: -10 // 缓存组优先级,当一个模块可能属于多个 chunkGroup,这里是优先级 
                },
                default: {
                    minChunks: 2,
                    priority: -20, // 优先级 
                    reuseExistingChunk: true // 如果该chunk包含的modules都已经另一个被分割的chunk中存在,那么直接引用已存在的c hunk,不会再重新产生一个 
                }
            }
        }
    }
};

除 JavaScript, splitChunks 也适用于使用 mini-css-extract-plugin 插件的 css 配置

1.2.1 chunks

可选值:async(默认) | initial | all(推荐),针对下面的 a.jsb.js

代码语言:txt复制
// a.js
import $ from 'jquery';
import react from 'react';
import( /* webpackChunkName: "a-lodash" */ 'lodash');

// b.js
import $ from 'jquery';
import( /* webpackChunkName: "b-react" */ 'react');
import( /* webpackChunkName: "b-lodash" */ 'lodash');

采用三种不同配置:

代码语言:txt复制
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
    mode: 'development',
    entry: {
        a: './a.js',
        b: './b.js'
    },
    plugins: [new BundleAnalyzerPlugin()],
    optimization: {
        splitChunks: {
            cacheGroups: {
                vendors: {
                    chunks: 'async', // async|initial|all 
                    test: /[\/]node_modules[\/]/
                }
            }
        }
    }
};

async: 只对动态引入的代码拆分

  • jquery 分别打包进 a.jsb.js
  • react 被打包进 a.js 和拆出 venders~b-react.js
  • lodash 拆为同一个 venders~a-lodash.js

initial: 共用即拆(动态引入一定拆分),根据阈值 minChunks 配置拆分

  • jquery 因共用被拆为 vendors~a~b.js
  • react 分别拆为 vendors~a.js(动态引入) 和 b-react.js(魔法注释),注意:若 minSize 设置较大,不会单独拆出 vendors~a.js
  • lodash 拆为同一个 a-lodash.js(魔法注释)

all: 推荐,在 initial 基础上尽可能生成复用代码,如 initialreact 拆为同一个 vendors~a~b-react.js

1.2.2 maxInitialRequests / maxAsyncRequests / maxSize / minSize

优先级:maxInitialRequest / maxAsyncRequests < maxSize < minSize

maxInitialRequests: 针对每个 entry 初始化的最大文件数,优先级高于 cacheGroup,因此为 1 时不会抽取 initial common

maxAsyncRequests: 每次按需加载最多有多少个异步请求,为 1 时不抽取公共 chunk

maxSize: 文件最大尺寸,0不限制

minSize: 文件最小尺寸,默认30K,development 下10k,与 chunk 数量成反比

webpack 魔法注释

import(/* webpackChunkName: "react" */ 'react'); // 可以设置生成的 bundle 名称

使用 webpack-bundle-analyzer 插件查看打包情况

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports = { mode: 'production', entry: { main: './default/index.js' }, plugins: [new BundleAnalyzerPlugin()] };

1.3 cacheGroups 缓存组

splitChunks 的配置项都是作用于 cacheGroup 上的,默认有两个 cacheGroup:vendorsdefault (上节 splitChunks 默认配置),支持重写,也支持设置为 false (splitChunks将失效)

cacheGroups 除拥有上节所有 splitChunks 默认配置,额外支持 test / priority / reuseExistingChunk

1.3.1 reuseExistingChunk

是否使用已有的 chunk

1.3.2 priority

权重,若一个模块满足多个缓存组条件,则按权重决定

1.3.3 test

缓存组命中条件,取值为正则、字符串和函数

代码语言:txt复制
cacheGroups: {
    vendors: {
      	// test: /[\/]node_modules[\/]/,
        test(module, chunks) {
            //...
            return module.type === 'javascript/auto'; 
        },
        priority: -20
    }
}

2. 构建速度优化

影响 webpack 构建速度的主要是:

  • loader/plugin 的构建过程
  • 压缩过程

可以从减少查找过程、多线程、提前编译和 Cache 多角度优化

2.1 减少查找过程

  • resolve.alias: 通过别名跳过耗时的递归模块解析操作
代码语言:txt复制
module.exports = {
    resolve: {
        // 生产环境直接使用 react.min.js
        alias: {
            react: path.resolve(__dirname, './node_modules/react/dist/react.min.js'),
            '@lib': path.resolve(__dirname, './src/lib/')
        }
    }
};
  • resolve.extensions: 配置后缀顺序,减少模块导入时的推测
代码语言:txt复制
resolve.extensions = ['js', 'json']
  • module.noParse: 排除不需要解析的模块

尤其是 jQuery 等未采用模块化标准且体积庞大的库,但要注意,排除的文件不能包含 importrequiredefine 等模块化语句。

  • rule: 通过 testincludeexclude 控制查找范围
代码语言:txt复制
rules: [{
    loader: 'babel-loader',
    test: /.js$/, // test 正则
    exclude: [path.resolve(__dirname, './node_modules')], // 排除绝对路径 Array
    include: [path.resolve(__dirname, './src')] // 查找绝对路径 Array
}];

exclude 优先级 > include / test,建议多用 include 避免用 exclude。

2.2 多线程

使用 thread-loaderHappyPack 可以实现对大项目的多线程打包。

thread-loader: 将 loader 放在一个 worker 池中运行,以达到多线程构建

代码语言:txt复制
module.exports = {
    module: {
        rules: [{
            test: /.js$/,
            include: path.resolve('src'),
            use: [
            		'thread-loader', // 需要放在其他 loader 之前
                // 其他高开销 loader (e.g babel-loader)
            ]
        }]
    }
};

HappyPack: 通过多进程模型加速代码构建,但需要对应的 loader 支持

代码语言:txt复制
const os = require('os');
const HappyPack = require('happypack');
// 根据 cpu 数量创建线程池 
const happyThreadPool = HappyPack.ThreadPool({
    size: os.cpus().length
});
module.exports = {
    module: {
        rules: [{
            test: /.js$/,
            use: 'happypack/loader?id=jsx'
        }, {
            test: /.less$/,
            use: 'happypack/loader?id=styles'
        }]
    },
    plugins: [
        new HappyPack({
            id: 'jsx',
            threads: happyThreadPool,
            loaders: ['babel-loader']
        }),
        new HappyPack({
            id: 'styles',
            threads: 2, // 自定义线程数量 
            loaders: ['style-loader', 'css-loader', 'less-loader']
        })
    ]
};

2.3 DllPlugin 预编译

预先编译打包一些不变化的库文件,在业务代码中直接引入。需要单独为 dll 文件创建一个配置文件,通过 DLLPlugin 插件将第三方依赖打包到 bundle 文件,并生成 manifest.json 文件,在项目的 webpack 配置文件中使用 DllReferencePlugin 插件解析 manifest.json,跳过 dll 中包含的依赖的打包。

代码语言:txt复制
// 单独的 dll 打包配置文件 webpack.config.dll.js 
const webpack = require('webpack');
const vendors = ['react', 'react-dom']; // 第三方依赖库 
module.exports = {
    mode: 'production',
    entry: {
        vendor: vendors // 打包公共文件的入口文件设为 vendor.js
    },
    output: {
        filename: '[name].[chunkhash].js',
        library: '[name]_[chunkhash]' // 将 verdor 作为 library 导出,并指定全局变量名 [name]_[chunkhash]
    },
    plugins: [
      	new webpack.DllPlugin({
            path: 'manifest.json', // 设置 mainifest.json 路径
            name: '[name]_[chunkhash]',
            context: __dirname
    		})
    ]
};

执行 webpack --config webpack.config.dll.js

代码语言:txt复制
// 项目配置文件 webpack.config.js 
const webpack = require('webpack');
module.exports = {
    output: {
        filename: '[name].[chunkhash].js'
    },
    entry: {
        app: './src/index.js'
    },
    plugins: [
      	new webpack.DllReferencePlugin({
            context: __dirname,
            manifest: require('./manifest.json') // 导入
    		})
    ]
};

注意:打包后的 html 中不会主动引入 dll 的 vendor.js 文件,需要手动处理。

2.4 cache 缓存

babel-loader 往往是编译过程中最耗时的环节,虽然提供了 cacheDirectory 配置指定缓存目录,但默认为 false 关闭,设为 true 则使用默认的缓存目录 node_modules/.cache/babel-loader

代码语言:txt复制
rules: [{
    test: /.js$/,
    loader: 'babel-loader',
    options: {
        cacheDirectory: true
    },
    exclude: /node_modules/,
    include: [path.resolve('.src')]
}];

2.5 其他

  • sourceMap 选择合适的 devtool 值
  • 切换更快的 loader
  • terser-webpack-plugin 开启多线程和缓存
代码语言:txt复制
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
    optimization: {
        minimizer: [new TerserPlugin({
            cache: true, // 开启缓存
            parallel: true // 多线程
        })]
    }
};

3. Tree-Shaking

ES6 Modules 是 Tree-Shaking 静态分析的基础。Webpack 通过分析 ES6 模块的引入和使用情况,去除不使用的 import 引入;此外,可以借助工具如 uglifyjs-webpack-pluginterser-webpack-plugin 进行删除(仅 mode=production 下生效)。树摇的实现需要保持良好的开发习惯:

  1. 必须使用 ES6 模块
  2. 按需引入,尤其是 UI 框架
  3. 减少代码中的副作用(纯函数)
代码语言:txt复制
// package.json
{
    "name": "tree-shaking-side-effect",
    "sideEffects": ["./src/utils.js"]
}

package.json 中,除了通过 sideEffects 指定有副作用的文件,若能确保没有副作用,可以设置 sideEffects: false 不再考虑副作用。

signsign

0 人点赞