Webpack 优化

打包体积优化,增加缓存命中率,splitChunks 拆分代码,打包速度优化,打包清除无用的代码......


一 体积优化

web 项目打完包之后,需要发布到服务器上供用户使用,受带宽的限制,项目体积需要越小越好。

1. javascript

mode=production下,Webpack 会自动压缩代码,但也可以自定义压缩工具,terser-webpack-plugin是 Webpack 官方维护的用来压缩 JavaScript 代码的压缩插件。与另一种压缩插件 UglifyJS 相比,terser-webpack-plugin在压缩 ES6 方面做的更优秀。

javascript 复制代码
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    optimization: {
        minimizer: [new TerserPlugin()]
    }
};

在实际开发中,可以通过移除一些不用的代码从而达到优化代码体积的作用,Tree-Shaking 也是依赖这个插件的。 压缩是发布前处理最耗时间的一个步骤,terser-webpack-plugin 支持多线程压缩。

javascript 复制代码
new TerserPlugin({
    // 使用 cache,加快二次构建速度
    cache: true,
    terserOptions: {
        comments: false,
        parallel: true   // 多线程
        compress: {
            // 删除无用的代码
            unused: true,
            // 删掉 debugger
            drop_debugger: true, // eslint-disable-line
            // 移除 console
            drop_console: true, // eslint-disable-line
            // 移除无用的代码
            dead_code: true // eslint-disable-line
        }
    }
});

作用域提升 Scope Hoistingwebpack, 通过 ES6 语法的静态分析,分析出模块之间的依赖关系,可以把模块放到同一个函数中,以减少文件体积。

javascript 复制代码
// webpack.config.js
module.exports = {
    optimization: {
        concatenateModules: true
    }
};

2. css

CSS 文件导出到单独的 CSS 文件中,导出 CSS 文件需要使用 mini-css-extract-plugin 这个插件。

javascript 复制代码
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].css',
            chunkFilename: '[name].[contenthash:8].css'
        })
    ],
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            publicPath: '../',
                            hmr: process.env.NODE_ENV === 'development'
                        }
                    },
                    'css-loader'
                ]
            }
        ]
    }
};

css-loader 本身已经集成了压缩工具 cssnano,还可以使用 optimize-css-assets-webpack-plugin 来自定义 cssnano 的规则。optimize-css-assets-webpack-plugin 是一个 CSS 的压缩插件,默认的压缩引擎就是 cssnano。

javascript 复制代码
// webpack.config.js
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
    plugins: [
        new OptimizeCssAssetsPlugin({
            assetNameRegExp: /\.optimize\.css$/g,
            cssProcessor: require('cssnano'), // 这里制定了引擎,不指定默认也是 cssnano
            cssProcessorPluginOptions: {
                preset: ['default', {discardComments: {removeAll: true}}]
            },
            canPrint: true
        })
    ]
};

optimize-css-assets-webpack-plugin 插件默认的 cssnano 配置已经做的很友好了,不需要额外的配置就可以达到最佳效果。

3. 图片资源优化

Webpack 模块化开发与配置章节中介绍了 url-loader、svg-url-loader 和 image-webpack-loader 和雪碧图来优化图片,这里不在详述。

二 增加缓存命中

利用 HTTP 协议和浏览器的缓存来做页面代码的持久化缓存。

浏览器的持久化缓存方案分两步:

一是将静态资源(JavaScript 、CSS、图片字体文件等)这些不经常变动的文件寻找更合适的服务器存放,比如放到 CDN 服务器上,并且配置单独的域名,将静态资源存放到单独的服务器之后,需要做的是配置合理的 HTTP 缓存相关协议;详细见HTTP 协议

二是要针对发生了变更的静态资源进行重命名,这样静态的文件虽然使用了 CDN 和强缓存,但是只要内容变化,那么文件的路径(网址)发生了变化,浏览器还是会重新请求下载的。

使用 Webpack 构建项目可以通过哈希值(hash)做到自动更新:

  • chunkhash:chunk 的 hash,根据不同的 chunk 及其包含的模块计算出来的 hash,chunk 中包含的任意模块发生变化,则 chunkhash 发生变化;

  • contenthash:CSS 文件特有的 hash 值,是根据 CSS 文件内容计算出来的,CSS 发生变化则其值发生变化,推荐 CSS 导出中使用。

三 splitChunks 拆分代码

在 Webpack 中,有三种方式来实现代码拆分(Code Splitting):

  • entry 配置:通过多个 entry 文件来实现;
  • 动态加载(按需加载):通过写代码时主动使用import()或者require.ensure来动态加载;
  • 抽取公共代码:使用splitChunks配置来抽取公共代码。

splitChunks 的默认配置:

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

splitChunks默认配置对应的就是 chunk 生成的第二种情况:通过写代码时主动使用import()或者require.ensure来动态加载。

四 打包速度优化

影响 Webpack 构建速度的有两个主要原因:一是 loader 和 plugin 方面的构建过程,一是压缩。把这两个东西优化起来,可以减少很多发布的时间。

1. 使用 resolve.alias 减少查找过程

resolve.alias 配置项通过别名(alias)来把原导入路径映射成一个新的导入路径,alias 可以减少查找过程,具体示例配置如下:

javascript 复制代码
module.exports = {
    resolve: {
        // 使用 alias 把导入 react 的语句换成直接使用单独完整的 react.min.js 文件,
        // 减少耗时的递归解析操作
        alias: {
            react: path.resolve(__dirname, './node_modules/react/dist/react.min.js'),
            '@lib': path.resolve(__dirname, './src/lib/')
        }
    }
};

2. 使用 resolve.extensions 优先查找

在导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试询问文件是否存在,查询的顺序是按照我们配置的 resolve.extensions 顺序从前到后查找,如果我们配置 resolve.extensions= ['js', 'json'],那么会先找xxx.js然后没有再查找xxx.json,所以我们应该把常用到的文件后缀写在前面,或者我们导入模块时,尽量带上文件后缀名。

3. 排除不需要解析的模块

被忽略掉的文件里不应该包含 import、require、define 等模块化语句.

javascript 复制代码
module.exports = {
    module: {
        noParse: /node_modules\/(jquey\.js)/;
    }
}

4. 合理配置 rule 的查找范围

在 rule 配置上,有test、include、exclude三个可以控制范围的配置,最佳实践是:

  • 只在 test 和 文件名匹配中使用正则表达式;
  • 在 include 和 exclude 中使用绝对路径数组;
  • 尽量避免 exclude,更倾向于使用 include。

exclude 优先级要优于 include 和 test,所以当三者配置有冲突时,exclude 会优先于其他两个配置。

javascript 复制代码
rules: [
    {
        // test 使用正则
        test: /\.js$/,
        loader: 'babel-loader',
        // 排除路径使用数组
        exclude: [path.resolve(__dirname, './node_modules')],
        // 查找路径使用数组
        include: [path.resolve(__dirname, './src')]
    }
];

5. 利用多线程提升构建速度

多线程打包有两种方案:thread-loader 和 HappyPack。

thread-loader 是针对 loader 进行优化的,它会将 loader 放置在一个 worker 池里面运行,以达到多线程构建。

javascript 复制代码
// webpack.config.js
module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                include: path.resolve('src'),
                use: [
                    'thread-loader'
                    // 你的高开销的loader放置在此 (e.g babel-loader)
                ]
            }
        ]
    }
};

给 loader 配置使用 HappyPack 需要对应的 loader 支持才行,例如 url-loader 和 file-loader 就不支持 HappyPack。

javascript 复制代码
// webpack.config.js
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']
        })
    ]
};

6. DLLPlugin 预先编译

把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码。DLL(Dynamic Link Library) 文件为动态链接库文件。

DllPlugin 的预先编译需要配合 webpack.DllReferencePlugin 来使用。

可以把构建过程分成 dll 构建过程和主构建过程(实质也就是如此),所以需要两个构建配置文件,例如叫做webpack.config.js和webpack.dll.config.js。

1. dll 构建

DllPlugin 是 webpack 内置的插件,不需要额外安装,直接配置 webpack.dll.config.js 文件:

javascript 复制代码
// webpack.dll.config.js
module.exports = {=
  entry: {
    // 第三方库
    react: ['react', 'react-dom', 'react-redux']
  },
  output: {
    // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
    filename: '[name].dll.js',
    path: resolve('dist/dll'),
    // library必须和后面dllplugin中的name一致 后面会说明
    library: '[name]_dll_[hash]'
  },
  plugins: [
  // 接入 DllPlugin
    new webpack.DllPlugin({
      // 动态链接库的全局变量名称,需要和 output.library 中保持一致
      // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
      name: '[name]_dll_[hash]',
      // 描述动态链接库的 manifest.json 文件输出时的文件名称
      path: path.join(__dirname, 'dist/dll', '[name].manifest.json')
    }),
  ]
}

执行:webpack --config webpack.dll.config,然后到指定的输出文件夹查看输出:

  • react.dll 文件里是使用数组保存的模块,索引值就作为id;
  • react.manifest.json 文件里,是用来描述对应的dll文件里保存的模块
javascript 复制代码
// react.manifest.json
{
  "name":"react_dll_553e24e2c44987d2578f",
  "content":{
    "./node_modules/webpack/node_modules/process/browser.js":{"id":0,"meta":{}},"./node_modules/react/node_modules/fbjs/lib/invariant.js":{"id":1,"meta":{}},"./node_modules/react/lib/Object.assign.js":{"id":2,"meta":{}},"./node_modules/react/node_modules/fbjs/lib/warning.js":{"id":3,"meta":{}}
    // ...
  }
}

2. 主构建过程

在 webpack.config 中使用 dll 要用到 DllReferencePlugin, 这个插件通过引用 dll 的 manifest 文件来把依赖的名称映射到模块的 id 上,之后再在需要的时候通过内置的 webpack_require 函数来 require 他们。

javascript 复制代码
new webpack.DllReferencePlugin({
  context: __dirname,
  manifest: require('./dist/dll/react.manifest.json')
})

dll 构建过程产出的 manifest 文件就用在这里,给主构建流程作为查找 dll 的依据:DllReferencePlugin 去 manifest.json 文件读取 name 字段的值,把值的内容作为在从全局变量中获取动态链接库中内容时的全局变量名,因此:在 webpack_dll.config.js 文件中,DllPlugin 中的 name 参数必须和 output.library 中保持一致。

3. 在入口文件引入dll文件

生成的dll暴露出的是全局函数,因此还需要在入口文件里面引入对应的dll文件。

html 复制代码
<body>
  <div id="app"></div>
  <!--引用dll文件-->
  <script src="../../dist/dll/react.dll.js" ></script>
</body>

7. 缓存(Cache)相关

babel-loader 提供了 cacheDirectory 配置给 Babel 编译时给定的目录,并且将用于缓存加载器的结果,但是这个设置默认是false关闭的状态,我们需要设置为true,这样 babel-loader 将使用默认的缓存目录 。

javascript 复制代码
rules: [
    {
        test: /\.js$/,
        loader: 'babel-loader',
        options: {
            cacheDirectory: true
        },
        // 排除路径
        exclude: /node_modules/,
        // 查找路径
        include: [path.resolve('.src')]
    }
];

8. 其他构建过程的优化点

  • sourceMap 生成耗时严重,构建 sourceMap 选择合适的devtool值;
  • 切换一些 loader 或者插件,比如:fast-sass-loader可以并行处理 sass 文件,要比 sass-loader 快 5~10 倍

9. 压缩优化

以上都是构建过程的优化,这一小节是压缩过程的优化,压缩方面主要的优化配置是开启多线程和缓存。

javascript 复制代码
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    optimization: {
        minimizer: [
            new TerserPlugin({
                cache: true, // 开启缓存
                parallel: true // 多线程
            })
        ]
    }
};

五 Tree-Shaking

Tree-Shaking 是一个前端术语,本意为摇树的意思,在前端术语中通常用于描述移除 JavaScript 上下文中没用的代码,这样可以有效地缩减打包体积。

Webpack 的 Tree-Shaking 实际分了两步来实现:

  • Webpack 自己来分析 ES6 Modules 的引入和使用情况,去除不使用的 import 引入;
  • 借助工具(如 uglifyjs-webpack-plugin和terser-webpack-plugin)进行删除,这些工具只在mode=production中会被使用。

副作用

对于相同的输入就有相同的输出,不依赖外部环境,也不改变外部环境的函数称为纯函数,不纯就是具有副作用的,是可能对外界造成影响的。

Webpack 的项目中,可以在 package.json 中使用 sideEffects 来告诉 webpack 哪些文件中的代码具有副作用,从而对没有副作用的文件代码可以放心的使用 Tree-Shaking 进行优化。

javascript 复制代码
// package.json
{
    "name": "tree-shaking-side-effect",
    "sideEffects": ["./src/utils.js"]
}

如果sideEffects: false 则表示可以放心的对该项目进行 Tree-Shaking,而不必考虑副作用。

Tree-Shaking 是前端进化的一个理想状态,要发挥 Tree-Shaking 还需要我们在日常的代码中保持良好的开发习惯:

  • 要使用 Tree-Shaking 必然要保证引用的模块都是 ES6 规范的,很多工具库或者类库都提供了 ES6 语法的库,例如 lodash 的 ES6 版本是lodash-es;
  • 按需引入模块,避免笼统引入,例如我们要使用 lodash 的isNumber,可以使用import isNumber from 'lodash-es/isNumber';,而不是import {isNumber} from 'lodash-es';
  • 减少代码中的副作用代码。

六 区分多环境配置

javascript 复制代码
// webpack.json
/**
* 开发环境配置webpack.config.dev.js: 注重效率,打包速度
*
* 生产环境配置webpack.config.prod.js: 线上最优打包配置,包括splitChunks、压缩资源、CDN 路径配置(在output配置)等相关配置,
*                                    去除debugger、alert等
*/
{
  // ...
  "scripts": {
    "start": "cross-env NODE_ENV=development webpack --config webpack.config.dev.js --mode development",
    "build": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js --mode producation"
  }
  // ...
}

七 Webpack 监测分析工具

这里提供几个 webpack 构建项目过程的分析监测工具,具体使用方法这里不详述:

  • 分析与可视化:Stats
  • 体积分析:source-map-explorer 、webpack-bundle-analyzer
  • 构建速度分析:speed-measure-webpack-plugin
相关推荐
乌兰麦朵11 分钟前
Vue吹的颅内高潮,全靠选择性失明和 .value 的PUA!
前端·vue.js
Code季风11 分钟前
Gin Web 层集成 Viper 配置文件和 Zap 日志文件指南(下)
前端·微服务·架构·go·gin
蓝倾12 分钟前
如何使用API接口实现淘宝商品上下架监控?
前端·后端·api
舂春儿13 分钟前
如何快速统计项目代码行数
前端·后端
毛茸茸13 分钟前
⚡ 从浏览器到编辑器只需1秒,这个React定位工具改变了我的开发方式
前端
Pedantic14 分钟前
我们什么时候应该使用协议继承?——Swift 协议继承的应用与思
前端·后端
Software攻城狮15 分钟前
vite打包的简单配置
前端
Codebee15 分钟前
如何利用OneCode注解驱动,快速训练一个私有的AI代码助手
前端·后端·面试
流星稍逝16 分钟前
用vue3的写法结合uniapp在微信小程序中实现图片压缩、调整分辨率、做缩略图功能
前端·vue.js
知了一笑19 分钟前
独立开发问题记录-margin塌陷
前端