webpack4.41+性能优化(高级篇)

以下配置是在webpack 4.41.6+测试


可用于生产环境:

  • babel-loader缓存优化
  • ignoreplugin
  • noparse
  • happyPack
  • ParallelUglifyPlugin

不可用于生产环境的:

  • 自动刷新
  • 热更新
  • DllPlugin

babel-loader的缓存优化

module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'babel-loader?cacheDirectory', // 开启缓存
                include: path.resolve(__dirname, 'src'), // 明确范围
                // 排除范围,include和exclude两者选一个就行
                // exclude: path.resolve(__dirname, 'node_modules') 
            }
        ]
    }

这里的?cacheDirectory放在babel-loader后面,把语法转换的代码缓存下来。只要ES6代码没有改变的,第二次编译的时候,这些ES6没有改动的部分就不会重新编译,直接使用缓存,编译速度更快。

或者这样写

          {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            options: {
              // 开启babel缓存
              // 第二次构建时,会读取之前的缓存
              cacheDirectory: true
            }
          },

一般来说,一个loader写成loader:"babel-loader"这种字符串的形式,多个loader写成use:["babel-loader", "eslint-loader"]字符串数组的形式

happyPack多进程打包

提高构建速度,利用好多核CPU
1.安装happyPack
2.引入const HappyPack = require('happypack')
3.使用

module: {
        rules: [
            {
                test: /\.js$/,
                // 把对 .js 文件的处理转交给 id 为 babel123 的 HappyPack 实例
                loader: 'happypack/loader?id=babel123',
                include: path.resolve(__dirname, 'src'), // 明确范围
                // 排除范围,include和exclude两者选一个就行
                // exclude: path.resolve(__dirname, 'node_modules') 
            }
        ]
    },
    plugins: [
		// ...省略其他代码
        // happyPack 开启多进程打包
        new HappyPack({
            // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
            id: 'babel123',
            // 如何处理 .js 文件,用法和 Loader 配置中一样
            use: ['babel-loader?cacheDirectory']
            // 这里写成loaders: ['babel-loader?cacheDirectory']也可以
            // 这里必须用数组形式
        }),
    ],

如果你的happyPack的id对应不上就会报如下错误
AssertionError [ERR_ASSERTION]: HappyPack: plugin for the loader 'babel123' could not be found! Did you forget to add it to the plugin list?...

ParallelUglifyPlugin多进程压缩JS

现在的webpack内置Uglify工具压缩js,只要你是生产环境就会自动压缩js(当然你webpack版本太旧是不能自动在生产环境压缩的),因为JS是单线程的,开启多线程会压缩的更快。
1.安装webpack-parallel-uglify-plugin
2.引入const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')

plugins: [
		// ...省略部分无关代码
        new ParallelUglifyPlugin({
            // 传递给 UglifyJS 的参数
            // (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
            uglifyJS: {
                output: {
                    beautify: false, // 最紧凑的输出
                    comments: false, // 删除所有的注释
                },
                compress: {
                    // 删除所有的 `console` 语句,可以兼容ie浏览器
                    drop_console: true,
                    // 内嵌定义了但是只用到一次的变量
                    collapse_vars: true,
                    // 提取出出现多次但是没有定义成变量去引用的静态值
                    reduce_vars: true,
                }
            }
        })
    ],

热更新

热更新:新代码生效,网页不刷新,状态不丢失
自动网页刷新状态会丢失
自动刷新会用到devServer
1.引入const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
2.在plugins加入配置

plugins: [
        // ...省略其他无关代码
        new HotModuleReplacementPlugin()
    ],

3.在devServer加入hot: true

    devServer: {
        port: 8080,
        progress: true,  // 显示打包的进度条
        contentBase: distPath,  // 根目录
        open: true,  // 自动打开浏览器
        compress: true,  // 启动 gzip 压缩

        hot: true, // ======在这里加入热更新配置=============

        // 设置代理
        proxy: {
            // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
            '/api': 'http://localhost:3000',

            // 将本地 /api2/xxx 代理到 localhost:3000/xxx
            '/api2': {
                target: 'http://localhost:3000',
                pathRewrite: {
                    '/api2': ''
                }
            }
        }
    },

举例子:
这里开启devServer,如果不是热更新,我们修改代码会自动刷新整个网页。如果每次刷新都会有网络请求,增加了后台负担;如果填写都表单有数据,网页刷新表单数据会丢失;如果你进了路由都子路由的子路由,层级比较深,而刷新后又回到了根路由…
开启热更新之后,需要热更新部分加上监听

// 增加,开启热更新之后的代码逻辑
if (module.hot) {
    module.hot.accept(['./math.js'], () => {
        const sumRes = sum(10, 30)
        console.log('sumRes in hot', sumRes)
    })
}

那么你只要修改了math.js里面的代码,就只会热更新,执行这里module.hot.accept的第二个参数----回调函数中的内容。
并且这里不会清空你在Console中定义的变量值,不会清空你在input框里面的值,因为它并不会刷新整个网页,仅仅只是针对math.js里面的东西作出响应。

webpack在生产环境的常用优化思路

1.小图片base64编码

module: {
        rules: [
        	// ...省略无关代码
            // 图片 - 考虑 base64 编码的情况
            {
                test: /\.(png|jpg|jpeg|gif)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        // 小于 5kb 的图片用 base64 格式产出
                        // 否则,依然延用 file-loader 的形式,产出 url 格式
                        limit: 5 * 1024,

                        // 打包到 img 目录下
                        outputPath: '/img1/',

                        // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
                        // publicPath: 'http://cdn.abc.com'
                    }
                }
            },
        ]
    },

这个例子,小于5kb以base64产出,url-loader处理,打包到了对应js,这样就不会单独打包成图片,减少网络请求的耗时。
太大对图片就单独打包成图片,避免js文件过大,下载太耗时导致页面渲染卡住。

2.bundle加上hash值

output: {
        // filename: 'bundle.[contentHash:8].js',  // 打包代码时,加上 hash 戳
        filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
        path: path.join(__dirname, '..', 'dist'),
        // publicPath: 'http://cdn.abc.com'  // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
    },

加上contentHash是因为只要文件js内容不变,这个contentHash值就不会变,这样上线之后用户发起请求可以命中缓存,直接取本地缓存,当内容变化之后contentHash变化,缓存失效,再发起请求拉去新的文件。
为什么不用[hash]而是[contentHash],因为webpack每次打包都会有一个hash,而且每次不一样,这样每次还是回去请求新的文件,没有利用到缓存,失去了意义。
后面对:8是取contentHash值的前8位。
CSS操作也是一样,css-loader是将css文件变成commonjs模块加载js中,里面内容是样式字符串,这样CSS文件就放在了打包后的JS文件中,当多个JS引入相同的CSS的时候,如果这样操作,每个打包出来的CSS文件都放在不同的JS文件中,而这些CSS又是重复的样式,所以需要把CSS提取出来减小JS体积,我们一般会对CSS文件命名,这里也是加上了[contentHash:8]

	plugins: [
        // ...省略无关代码
        // 抽离 css 文件
        new MiniCssExtractPlugin({
            filename: 'css/main.[contentHash:8].css'
        })
    ],

3.懒加载和预加载

比较大的文件用懒加载

document.getElementById('btn').onclick = function() {
  // 懒加载~:当文件需要使用时才加载~
  // 预加载 prefetch:会在使用之前,提前加载js文件 
  // 正常加载可以认为是并行加载(同一时间加载多个文件)  
  // 预加载 prefetch:等其他资源加载完毕,浏览器空闲了,再偷偷加载资源
  import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test').then(({ mul }) => {
    console.log(mul(4, 5));
  });
};

这里写了/* webpackChunkName: 'test', webpackPrefetch: true */
表示这里的回调函数的内容会打包到chunkName为test到js中,默认entry我们是单入口文件,比如

entry: './src/js/index.js',

实际上等同于

entry: {
	main: './src/js/index.js' // 这个默认的main就是默认的webpackChunkName
}

webpackChunkName是main,当我们把/* webpackChunkName: 'test' */之后就指定webpackChunkName是test,所以console.log(mul(4, 5));会打包到test.[contentHash:8].js
当然,你的输出文件名仍然是可以在output修改的

  output: {
    filename: 'js/[name].[contenthash:10].js',
    path: resolve(__dirname, 'build'),
    chunkFilename: 'js/[name].[contenthash:10]_chunk.js' // 这个[name]是你/* webpackChunkName: 'xxx'*/指定的,打包出来就是js/xxx.[contentHash:10]_chunk.js
    // 如果你不指定webpackChunkName,这里就会输出js/[id].[contentHash:10]_chunk.js,以从0开始的数字往后命名,看你webpack打包日志的chunks这一项是什么数字,这个[id]就会显示多少
  },


这个就不多说了,不然篇幅太长。
这里还提到了/* webpackPrefetch: true */,懒加载是等用到的时候再去发起请求获取数据,而预加载是按照正常加载,正常渲染,而之后需要加载到数据在渲染完成后再下载下来,比如这里的test.[contentHash:8].js是现在不需要的,后面可能会用到,等你渲染完成了我再去加载,然后当你点击触发获取test.[contentHash:8].js的时候就不用再发起请求了,直接在本地加载,速度看起来更快。预加载目前在一些浏览器和移动端可能不支持。
有人可能会问了,这里在onlick事件里面,我没去点击按钮,没触发这个回调你怎么知道我回调函数里面有个预加载或者懒加载?因为DOM事件是宏任务,在你的同步代码执行完=>微任务=>尝试DOM渲染=>宏任务,按照这样的执行顺序来的。如果你不了解JS异步,可以看看这里JS 异步进阶【想要进大厂,更多异步的问题等着你】

4.提取公共代码

optimization: {
    splitChunks: {
		// initial 入口chunk,对于异步导入的文件不处理
		// async 异步chunk,只对异步导入的文件处理
		// all 全部chunk
      chunks: 'all',
      // 默认值,可以不写~
      minSize: 30 * 1024, // 分割的chunk最小为30kb
      maxSize: 0, // 最大没有限制
      minChunks: 1, // 要提取的chunk最少被引用1次
      maxAsyncRequests: 5, // 按需加载时并行加载的文件的最大数量
      maxInitialRequests: 3, // 入口js文件最大并行请求数量
      automaticNameDelimiter: '~', // 名称连接符
      name: true, // 可以使用命名规则
      // === 以上为公共规则 ==========
      cacheGroups: {
        // 分割chunk的组的规则
        // node_modules文件会被打包到 vendors 组的chunk中。--> vendors~xxx.js,这个~是名称链接符
        // 满足上面的公共规则,如:大小超过30kb,至少被引用一次。比如vue、vue-router等等
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          // 优先级
          priority: -10
        },
        default: {
          // 要提取的chunk最少被引用2次
          minChunks: 2,
          // 优先级
          priority: -20,
          // 如果当前要打包的模块,和之前已经被提取的模块是同一个,就会复用,而不是重新打包模块
          reuseExistingChunk: true
        } 
      }
    },
    // 将当前模块的记录其他模块的hash单独打包为一个文件 runtime
    // 解决:修改a文件导致b文件的contenthash变化,做代码分割一定要加上runtimeChunk,否则导致缓存失效
    runtimeChunk: {
      name: entrypoint => `runtime-${entrypoint.name}`
    },
    minimizer: [
      // 配置生产环境的压缩方案:js和css,4.26以上的webpack压缩js使用terser-webpack-plugin
      // 压缩js
      new TerserWebpackPlugin({
        // 开启缓存
        cache: true,
        // 开启多进程打包
        parallel: true,
        // 启动source-map
        sourceMap: true
      }),
      // 压缩css
      new OptimizeCSSAssetsPlugin({})
    ]
  }

这里需要说明一下

terser-webpack-plugin插件压缩js,而不是uglifyjs-webpack-plugin,在webpack4.26+就用terser-webpack-plugin去压缩js,因为uglifyjs-webpack-plugin不再维护了。

缓存组cacheGroups里面default组里有一个reuseExistingChunk: true,解释一下,比如文件c.js里引入a.jsb.js,而a.js里面又引入里b.js,打包的时候设置reuseExistingChunk: true,则会忽略第二次引入b.js,这样就避免了重复引入b.js

webpack 5开始就不支持{cacheGroup}.name,即

optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
-         name: 'vendors', // 这里不支持
          chunks: 'all'
        }
      }
    }
  }

这里块名称是commons,那么分割出的包名就是commons.jsname命名无效,默认就是块名称。

这里为什么写/[\\/]node_modules[\\/]/而不是/node_modules/
webpack在处理文件路径时,默认在Unix/,在Windows\[\\/]避免在跨平台使用时出现问题

分割chunk组规则里的优先级priority有什么用?
当满足公共规则的时候,比如提取出引入的第三方jquery,既满足vendors组的规则(因为在node_modules路径下),也满足default组的规则的时候,谁的优先级高就匹配对应组的规则,这里-10 > -20,所以打包出来的[name]vendors而不是default

这里不得不说一下runtimeChunk,这是为了防止修改a文件导致b文件的contenthash变化,做代码分割一定要加上runtimeChunk,否则导致缓存失效。
举个例子,没有配置runtimeChunk的时候,打包出来如下

main.[contentHash:10].js中存在映射关系,包含了a.[contentHash:10].js文件映射
在这里插入图片描述
如果我修改a.js文件的内容,打包后a.js的contentHash会变化,因为映射关系要对应,从而会导致main.js的contentHash会变化,所以我们需要提取出来,加上runtimeChunk之后,打包如下
在这里插入图片描述
映射关系跑到了runtime-main里面去了,而打开runtime-main.[contentHash:10].js会发现是管理着映射关系,所以再次修改a.js,就只是runtime-main.[contentHash:10].jsa.[contentHash:10].js去变化,main.[contentHash:10].js就不会改变了。

5.IgnorePlugin

在项目中可能有几处体积占用较大的库,其中一个便是moment.js这个日期处理库。对于一个日期处理的功能,为何这个库会占用如此大的体积,仔细查看发现当引用这个库的时候,所有的locale文件都被引入,而这些文件甚至在整个库的体积中占了大部分,因此当webpack打包时移除这部分内容会让打包文件的体积有所减小。
webpack自带的两个库可以实现这个功能:

IgnorePlugin
ContextReplacementPlugin

IgnorePlugin的使用方法如下:

// 插件配置
plugins: [
  // 忽略moment.js中所有的locale文件
  new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
// 使用方式
const moment = require('moment');
// 引入zh-cn locale文件
require('moment/locale/zh-cn');
moment.locale('zh-cn');

复制代码ContextReplacementPlugin的使用方法如下:

// 插件配置
plugins: [
  // 只加载locale zh-cn文件
  new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /zh-cn/),
],
// 使用方式
const moment = require('moment');
moment.locale('zh-cn');

复制代码通过以上两种方式,moment.js的体积大致能缩减为原来的四分之一。

6.CDN加速

你要引入一个库,但是这个库的在线js比较慢,你可以放到CDN。
如果你最终是在线页面,你会把这些资源包上传到公司的CDN或者自己的CDN,你可以这么写

output: {
        // filename: 'bundle.[contentHash:8].js',  // 打包代码时,加上 hash 戳
        filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
        path: path.join(__dirname, '..', 'dist'),
        publicPath: 'http://cdn.abc.com'  // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
    },

这里的publicPath写为你公司的CDN或者自己的CDN,打包之后是这样的
在这里插入图片描述
如果不写,那么publicPath默认是相对路径,相对于根目录
在这里插入图片描述
如果你最终是会变成下载下来的本地包加载,那么就不用写在线CDN的URL了,直接写上publicPath: '/'或者publicPath: './',根据你的的资源最后打包出来的路径选择
这个publicPath也可以写在loaderoptions里面,比如写在url-loader里面,去解析图片,这样打包出来的东西大于指定范围limit的东西会变成file-loader处理输出,outputPath决定输出路径,而publicPath的可以改变在线CDN的前缀路径。

7.使用production

  • 会自动开启代码压缩
  • vue、react等会自动删掉调试代码(如开发环境的warning)
  • 启动Tree Shaking(1. 必须使用ES6模块化import引入 2. 开启production环境)

说一下Tree Shaking摇树,如果是开发环境,如果JS中有很多函数,而我只import了一个函数,打包的时候会把所有的函数代码打包进去,而生产环境,就只会引入你用到的那个函数。
形象比喻:树上很多果子代表函数,你只要一个果子,生产环境就是就会把整个树上无用的果子摇掉,简称“摇树Tree Shaking

为什么必须使用ES6模块化import引入才能Tree Shaking呢?

  • ES6 Module是静态引入,编译时引入
  • Commonjs是动态引入,执行时引入
  • 只有ES6 Module才能静态分析,实现Tree Shaking
    Commonjs执行的时候才知道哪个函数需要哪个不需要,Commonjs就不能实现编译的时候摇树

commonjs可以加上条件判断去引入,因为动态执行的时候根据条件变化可以执行,而ES6 Module静态编译的时候无法确定条件,会直接报错告诉你Module parse failed: 'import' and 'export' may only appear at the top level只能出现在最外层,外层不能再加条件判断了。

const flag = true
if (flag) {
	import test from './test
} // 会直接报错
const flag = true
if (flag) {
	require('./test')
} // 完全没问题

8.Scope Hosting

创建函数作用域更少,体积更小,可读性更好,现在的webpack自动集成了这一功能
以前引入一个js,默认打包的时候就会产生一个新的作用域,当引入文件比较多的时候就产生了很多作用域,现在的webpack将这些代码优化在了一个作用域,减小了体积。




关注、留言,我们一起学习。


===============Talk is cheap, show me the code================

猜你喜欢

转载自blog.csdn.net/qq_34115899/article/details/107427338
今日推荐