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多线程打包

HappyPack是一个通过多线程来提升Webpack打包速度的工具,不是多进程,很多博客写的多进程,为此我查阅githubhappypack插件说明明确说到是多线程。
在打包过程中有一项非常耗时的工作,就是使用loader将各种资源进行转译处理
我们可以简单地将代码转译的工作流程概括如下:

1)从配置中获取打包入口;
2)匹配loader规则,并对入口模块进行转译;
3)对转译后的模块进行依赖查找(如a.js中加载了b.jsc.js);
4)对新找到的模块重复进行步骤2)和步骤3),直到没有新的依赖模块。
不难看出从步骤2)到步骤4)是一个递归的过程,Webpack需要一步步地获取更深层级的资源,然后逐个进行转译。

这里的问题在于Webpack是单线程的,假设一个模块依赖于几个其他模块,Webpack必须对这些模块逐个进行转译。虽然这些转译任务彼此之间没有任何依赖关系,却必须串行地执行。HappyPack恰恰以此为切入点,它的核心特性是可以开启多个线程,并行地对不同模块进行转译,这样就可以充分利用本地的计算资源来提升打包速度。

HappyPack适用于那些转译任务比较重的工程,当我们把类似babel-loaderts-loader迁移到HappyPack之上后,一般都可以收到不错的效果,而对于其他的如sass-loader、less-loader本身消耗时间并不太多的工程则效果一般。

每次webpack解析模块时,HappyPack都会获取它及其所有依赖项,并将这些文件分发到多个工作程序“线程”。如下图
在这里插入图片描述
在实际使用时,要用HappyPack提供的loader来替换原有loader,并将原有的那个通过HappyPack插件传进去。请看下面的例子

单个loader的优化(一般不用这个方式,都是使用多个loader的优化,多个loader只写一个就是单个loader

// 初始Webpack配置(使用HappyPack前)
module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          presets: ['react'],
        },
      }
    ],
  },
};

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

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

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

在使用HappyPack优化多个loader时,需要为每一个loader配置一个id,否则HappyPack无法知道rulesplugins如何一一对应。

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') 
            },
            {
		        test: /\.ts$/,
		        include: path.resolve(__dirname, 'src'), // 明确范围
		        loader: 'happypack/loader?id=ts',
		    }
        ]
    },
    plugins: [
		// ...省略其他代码
        // happyPack 开启多线程打包
        new HappyPack({
            // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
            id: 'babel123',
            // 如何处理 .js 文件,用法和 Loader 配置中一样
            use: ['babel-loader?cacheDirectory']
            // 这里写成loaders: ['babel-loader?cacheDirectory']也可以
            // 这里必须用数组形式
        }),
        new HappyPack({
			id: 'ts',
			use: [{
				loader: 'ts-loader',
				options: {}, // ts options
	     	}],
	    })
    ],

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

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

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 代理转发到 http://localhost:3000/api/xxx
            '/api': 'http://localhost:3000',

            // 将本地 /api2/xxx 代理转发到 http://localhost:3000/xxx
            '/api2': {
                target: 'http://localhost:3000',
                pathRewrite: {
                    '/api2': '' // 将路径中的'/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'
                    }
                }
            },
        ]
    },

这个例子,小于5kbbase64产出,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));
  });
};

为什么选择懒加载呢?
这样可以提高代码覆盖率。也就是一个js里面的代码使用率提高。我们可以在MAC电脑使用快捷键command+shift+P,输入coverage–>选择Show Coverage,然后如下图所示点击
在这里插入图片描述
如果不使用懒加载,你的代码属于Unused Bytes,使用了之后,你的代码是是属于Used Bytes,我们的目的就是提高Used Bytes,这样就提高了代码覆盖率。优化首先考虑代码覆盖率再才会考虑缓存。

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

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

实际上等同于

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

webpackChunkNamemain,当我们把/* webpackChunkName: 'test' */之后就指定webpackChunkNametest,所以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是现在不需要的,后面可能会用到,但是这里是在onclick监听事件里面才import(),属于宏任务,宏任务一定会在尝试一次DOM渲染之后才执行, 所以在这个例子中是渲染一次完成了再去加载,然后当你点击触发获取test.[contentHash:8].js的时候就不用再发起请求了,直接在本地加载,速度看起来更快。预加载目前在一些浏览器和移动端可能不支持。

举个例子:比如网页登录按钮点击之后弹出提示登录的操作,很显然我们需要懒加载这个登录界面,那么如果我点击按钮之后才去请求这个js(创建DOM结点操作显示界面),会不会有点慢,让人感觉会卡顿一下?那么这里的预加载就是很好的一种方案了。在网络带宽空闲的时候会去把这个预加载的js下载下来,再次加载的时候之后从缓存请求这个js,速度就非常快了。

有人可能会问了,这里在onlick事件里面,我没去点击按钮,没触发这个回调你怎么知道我回调函数里面有个预加载或者懒加载?因为DOM事件是宏任务,在你的同步代码执行完=>微任务=>尝试DOM渲染=>宏任务,按照这样的执行顺序来的。如果你不了解JS异步,可以看看这里JS 异步进阶【想要进大厂,更多异步的问题等着你】

4.提取公共代码(我想多说一点东西)

optimization: {
    splitChunks: {
		// initial 入口chunk,对于异步导入的文件不处理
		// async 异步chunk,只对异步导入的文件处理
		// all 全部chunk
      chunks: 'all', // 默认是async
      // 为什么默认是async呢?上面说过了,懒加载会提高代码覆盖率,而拆分同步代码只是利用缓存,优化十分有限,所以默认拆分懒加载的代码,为async!!!!
      // 下面是默认值,可以不写~
      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({})
    ]
  }

个人测试例子

// output配置
entry: path.join(__dirname, 'src/index.js'),
output: {
     filename: 'test.js',
     path: path.resolve(__dirname, '../dist'),
     chunkFilename: '[name]_chunk.js'
 },

optimization.splitChunks就是默认配置,和上面一样。

    optimization: {
        splitChunks: {
            chunks: 'all' // 这里要写,不然就是只分割异步代码
            // 后面不写,都是默认配置
        }
    }

代码中自定义webpackChunkNamevConsole

import _ from 'lodash'// 这个在node_modules里面,分割时属于vendors组的规则
import myjs from './lib/myjs' // 这个在我新建的lib文件夹下的目录
// promise方式
if (process.env.NODE_ENV === 'development') {
	let VConsole;
    import(/* webpackChunkName: 'vConsole' */'vconsole/dist/vconsole.min.js').then((module) => {
        VConsole = module.default;
        new VConsole();
    });
}
// 或者async/await方式
async created() {
  if (process.env.NODE_ENV === 'development') {
        try {
            const { default: VConsole } = 
            	await import(/* webpackChunkName: 'vConsole' */ 'vconsole/dist/vconsole.min.js');
            new VConsole();
        } catch (err) {
            console.log(err);
        }
    }
},

打包出来的vendors~vConsole_chunk.jsmain_chunk.jsvendors~main_chunk.js

vendors~vConsole_chunk.js的文件说明

vendors是缓存组cacheGroups的组名字,~是默认的automaticNameDelimiter名称链接符,vConsole _chunk.js就是output中的chunkFilename规则[name]_chunk.js,这里[name]就是由于魔法注释(magic comment)/* webpackChunkName: 'vConsole' */变成vConosle。这里如果打生产包是不会把vconsole打进去的,因为process.env.NODE_ENV === 'development'false

main_chunk.js的文件说明

入口文件默认chunk名为main,这里面有lodash的映射关系,但是lodash库的js不在这。
不过myjs在这里,所以引入的js或者库文件只要不是node_modules目录下的js都会打包在这里

vendors~main_chunk.js的文件说明

默认cacheGroups里面的test: /[\\/]node_modules[\\/]/, 所以node_modules里面的文件都会满足这个拆分规则,[name]vendors,所以node_modules里的lodash包拆分到这里来了,~是默认的automaticNameDelimiter名称链接符,因为入口文件默认chunkNamemainchunkFilename规则[name]_chunk.js,连起来就是main_chunk.js

默认配置的一些属性说明:

chunks: 'all', // 默认是async
为什么默认是async呢?上面说过了,懒加载会提高代码覆盖率,而拆分同步代码只是利用缓存,优化十分有限,所以默认拆分懒加载的代码,为async!!!!

注意: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


我们建议webpack都升级到4.0以上,如果你还是webpack 4.0以下,那就不得不说一下runtimeChunk,这是为了防止修改a文件导致b文件的contenthash变化,做代码分割一定要加上runtimeChunk,否则可能导致缓存失效。
我们先看现象,再讲解原因

// a.js
export function add(x, y) {
  return x + y;
}
// main.js
import(/* webpackChunkName: 'a' */'./a.js').then(({ add }) => {
  console.log(add(1, 2));
});

举个例子,还是上面的默认分割规则,没有配置runtimeChunk的时候,打包出来如下

main.[contentHash:10].js中存在映射关系,包含了a.[contentHash:10].js文件映射,在html文件的<script>标签只会引入main.e9bc442f61.js文件,而main.e9bc442f61.js会动态创建<script>标签并引入a.ad13327f26_chunk.js
在这里插入图片描述
如果我修改a.js文件的内容,打包后a.jscontentHash会变化,因为映射关系要对应,从而会导致main.jscontentHash会变化,那么客户端根据缓存发现哈希值不一致,会重新下载。所以我们需要把映射关系从main.[contentHash:10].js提取出来,加上runtimeChunk配置之后,打包如下
在这里插入图片描述
打包之后 html文件中,<script>只引入main.[contentHash:10]_chunk.jsruntime-main.[contentHash:10].js,映射关系跑到了runtime-main.[contentHash:10].js里面去了,而打开runtime-main.[contentHash:10].js会发现是管理着映射关系,会动态创建<script>标签然后引入a.[contentHash:10]_chunk.js

所以再次修改a.js,就只是runtime-main.[contentHash:10].jsa.[contentHash:10]_chunk.js去变化,main.[contentHash:10]_chunk.js就不会改变。这里main也能变成chunk块了,匹配output.chunkFilename:[name]_chunk.js规则。这样客户端只会拉取下载runtime-main.[contentHash:10].jsmain.[contentHash:10]_chunk.js利用缓存还可以继续使用。

实际上,平时写代码的时候,main.[contenthash:10].js是业务逻辑,vendors.[contenthash:10].js放的是库文件,业务逻辑和库文件的映射关系代码叫做manifest,默认manifest存在于main.[contenthash:10].js中,也存在于vendors.[contenthash:10].js里面,manifest在旧版webpack中打包可能会有差异,正是这种差异导致在旧版中哪怕内容没改变,contenthash值也会发生改变,原因在于包之间的关系或者js之间的关系嵌套在mainvendors文件里面,打包的时候会发生变化。我们把manifest里关联关系的代码抽离出来放在runtime文件里去。这样的话,main里面就是业务相关的代码,vendors就是库文件代码,关联关系代码放在runtime文件,这样打包后main文件的contenthashvendors文件的contenthash都不会变,这样新旧版本都实现了每次打包只要内容不变,contenthash就不改变的情况。

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

7.使用production

  • 会自动开启代码压缩
  • vuereact等会自动删掉调试代码(如开发环境的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================
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页