【webpack】7-针对不同场景的性能优化方案

前言

每个场景的方案有的只适用于dev,有的只适用于prod,有的二者都可适用。

在每个方案后面会有标注,如果适用dev环境,就会标注(dev),如果适用prod环境,会标注(prod),都适用就(dev,prod)。

以下配置都基于上一篇的拆分配置方案。

学习代码来源慕课网


如何抽离压缩css文件(dev prod)

前面的文章也说过未抽离的情况下,js代码和css代码是压缩到一个js文件里的,在开发环境可以这样,在线上是不可取的,因为还要执行js文件才能拿到css。

抽离:

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module: {
    rules: [
    	// 抽离 css
        {
            test: /\.css$/,
            loader: [
                MiniCssExtractPlugin.loader,  // 注意,这里不再用 style-loader
                'css-loader',
                'postcss-loader'
            ]
        },
        // 抽离 less --> css
        {
            test: /\.less$/,
            loader: [
                MiniCssExtractPlugin.loader,  // 注意,这里不再用 style-loader
                'css-loader',
                'less-loader',
                'postcss-loader'
            ]
        }
    ]
},
plugins: [
	// 抽离 css 文件
    new MiniCssExtractPlugin({
        filename: 'css/main.[contentHash:8].css'
    })
],

压缩:

const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')

optimization: {
    // 压缩 css
    minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
}

打包好后就会有压缩的单独css文件


抽离公共代码和第三方代码(prod)

我们改动了业务代码,在生产环境重新打包后的js文件hash值发生变化,所有的打包文件都更新了。用户再次访问网址时又要重新获取文件资源。

但是项目里面第三方库一般是没有变化的,每次生产重新打包后这部分代码用户还要重新获取,所以我们可以隔离单独打包第三方库。这样用户下次访问网页的时候,只会拉取业务代码文件,第三方库资源拿的还是浏览器的缓存。

对于我们业务代码也一样,有些公共组件被很多路由组件引用,每次跳转到其他路由的时候,这个公共组件又被重新拉取,所以也可以把这些公共组件都合并起来单独打包成一个文件。

optimization: {
// 分割代码块
    splitChunks: {
        chunks: 'all',
        /**
         * initial 入口 chunk,对于异步导入的文件不处理
            async 异步 chunk,只对异步导入的文件处理
            all 全部 chunk
         */

        // 缓存分组
        cacheGroups: {
            // 对于容量比较大的第三方库可以单独打包
            antd: {
              name: 'antd-chunk',
              test: /antd/,
              priority: 100,
            },
            reactDom: {
              name: 'reactDom-chunk',
              test: /react-dom/,
              priority: 99,
            },
            
            // 其他容量较小的第三方模块都一起打包了
            vendor: {
                name: 'vendor', // 打包后的chunk 名称
                priority: 1, // 权限更高,优先抽离,重要!!!
                test: /node_modules/,
                minSize: 0,  // 大小限制
                minChunks: 1  // 最少复用过几次
            },

            // 公共的模块
            common: {
                name: 'common', // 打包后的chunk 名称
                priority: 0, // 优先级
                minSize: 0,  // 公共模块的大小限制
                minChunks: 2  // 公共模块最少复用过几次
            }
        }
    }
}

注意:

  • minSize表示超过指定的大小的文件才进行单独抽离打包,因为一些公共js文件其实很小,没必要专门打包成一个文件,性价比不高。
  • minChunks表示被引用的次数,引用到达这个次数的就需要单独抽离合并打包。
  • test指定第三方包在哪个位置。

如果有多入口文件,其中一个入口文件并没有引用第三方库,但是我们打包后还是引入了,可以打开这个入口文件的对应html文件查看:

<script type="text/javascript" src="common.87047fb9.js"></script>
<script type="text/javascript" src="vender.87047fb9.js"></script> 多余的引入
<script type="text/javascript" src="other.a8e79968.js"></script>

所以去公共的配置文件中的new HtmlWebpackPlugin中chunks配置只引用哪些打包后的文件:

plugins: [
    // new HtmlWebpackPlugin({
    //     template: path.join(srcPath, 'index.html'),
    //     filename: 'index.html'
    // })

    // 多入口 - 生成 index.html
    new HtmlWebpackPlugin({
        template: path.join(srcPath, 'index.html'),
        filename: 'index.html',
        // chunks 表示该页面要引用哪些 chunk (即上面的 index 和 other),默认全部引用
        chunks: ['index', 'vendor', 'common']  // 要考虑代码分割
    }),
    // 多入口 - 生成 other.html
    new HtmlWebpackPlugin({
        template: path.join(srcPath, 'other.html'),
        filename: 'other.html',
        chunks: ['other', 'common']  // 考虑代码分割
    })
]

为什么这个方案不用在dev环境,因为dev环境的时候打包成一个文件构建速度更快一些。


实现异步加载JS文件(dev prod)

其实可以直接在js代码中实现,无需配置,例子:

setTimeout(()=>{
  import('./xxx.js').then(res=>{
    ...res
  })
}, 1000)

打开网络就可以看到一秒后就会拉取这个文件,这个原理是webpack实现的,所以在vue中可以看到使用的例子,例如按需引入组件,路由按需引入。


babel开启缓存(dev prod)

如果es6代码没有改过,就直接取上一次打包后的缓存文件

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

IgnorePlugin忽略打包第三方库无用代码(dev prod)

在引入一些第三方库的时候,往往引入了我们项目不需要的部分,例如moment.js,里面包含了十几种语言,体积很大250kb+,那么我们需要配置忽略打包所有的语言,然后手动在main.js中引人中文包,再打包就只有50kb+。

plugins: [
		// 忽略 moment 下的 /locale 目录
        new webpack.IgnorePlugin(/\.\/locale/, /moment/),
        ]

因为我们忽略打包所有moment.js的语言包,所以要在vue中的main.js单独引入moment.js的中文包。

这里只是拿moment.js做场景举例,要学会举一反三。


noParse不重复打包第三方库(dev prod)

我们引入的第三方库有些文件其实是已经打包过的,例如react.min.js,那我们就不需要再进行二次打包了。

module: {
  noParse: [/react\.min\.js$/]
}

与IgnorePlugin的区别,前者打包后代码还有,后者打包后直接不要。


happyPack多线程打包(dev prod)

开启多进程打包(js是单线程的),提高构建速度。

const HappyPack = require('happypack')

module: {
        rules: [
            // js
            {
                test: /\.js$/,
                // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
                use: ['happypack/loader?id=babel'],
                include: srcPath,
                // exclude: /node_modules/
            },
        ]
}

plugins: [
		// happyPack 开启多进程打包
        new HappyPack({
            // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
            id: 'babel',
            // 如何处理 .js 文件,用法和 Loader 配置中一样
            loaders: ['babel-loader?cacheDirectory']
        }),
     ]


parallelUglifyPlugin多线程压缩(prod)

开启多进程压缩JS,webpack内置的Uglify压缩工具是单线程的。开启多进程压缩更快。

const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')

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

与happyPack:只有在项目很大的时候再开启这两者,因为项目小的时候加上多进程的开销,反而效果不好。


热更新(dev)

热更新和自动刷新不一样,自动刷新除了可以单独配置以外,在devServer也会开启,而热更新是新代码直接生效,网页不会刷新。

const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');

entry: {
      index: [
          'webpack-dev-server/client?http://localhost:8080/',
          'webpack/hot/dev-server',
          path.join(srcPath, 'index.js')
      ],
plugins: [
      new HotModuleReplacementPlugin()
],

devServer: {
        hot: true,

以上配置完后只对css生效,js的话还需要手动去指定,哪些问题是可以热更新的。

import { sum } from './xxx'
if (module.hot) {
   module.hot.accept(['./xxx'], () => {
       // 当你在xxx文件里修改了代码,就会触发这个回调,xxx里的代码支持热更新
   })
}

js的相对麻烦,一般可以在一些需要的开发场景下再去手动指定,例如几十个表单输入框的页面。


DllPlugin打包dll文件,提升热刷新速度(dev)

一般用于例如vue、react这种大型的库,一个项目中这种库的版本基本不会升级,所以我们只需要打包一次就好。(PS说实话上面也有类似的方案,整得我不知道到底开哪个好了233)好处就是每次热更新的时候这些大的库就不会重新打包了,热更新速度更快。

手动(不推荐)

webpack已经内置DllPlugin,它会把vue打包成dll文件,然后每次打包DllReference会去使用dll文件,而不会重新对vue做打包。

在webpack-config文件中新建webpack.dll.js:

const path = require('path')
const DllPlugin = require('webpack/lib/DllPlugin')
const { srcPath, distPath } = require('./paths')

module.exports = {
  mode: 'development',
  // JS 执行入口文件
  entry: {
    // 把 React 相关模块的放到一个单独的动态链接库
    react: ['react', 'react-dom']
  },
  output: {
    // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
    // 也就是 entry 中配置的 react 和 polyfill
    filename: '[name].dll.js',
    // 输出的文件都放到 dist 目录下
    path: distPath,
    // 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
    // 之所以在前面加上 _dll_ 是为了防止全局变量冲突
    library: '_dll_[name]',
  },
  plugins: [
    // 接入 DllPlugin
    new DllPlugin({
      // 动态链接库的全局变量名称,需要和 output.library 中保持一致
      // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
      // 例如 react.manifest.json 中就有 "name": "_dll_react"
      name: '_dll_[name]',
      // 描述动态链接库的 manifest.json 文件输出时的文件名称
      path: path.join(distPath, '[name].manifest.json'),
    }),
  ],
}

主要配置了如何去打包react,然后在package.json中注册脚本:

"dll": "webpack --config build/webpack.dll.js"

执行一下npm run dll,dist文件夹中会有react.dll.js打包后的文件和react.manifest.json(前者的目录索引,为了能让webpack读懂react.dll.js的内容)

然后在dev配置文件中加上:

const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');

rules: [
            {
                test: /\.js$/,
                use: ['babel-loader'],
                include: srcPath,
                exclude: /node_modules/ // 第二,不要再转换 node_modules 的代码
            },
        ]


plugins: [
	        // 第三,告诉 Webpack 使用了哪些动态链接库
	        new DllReferencePlugin({
	            // 描述 react 动态链接库的文件内容
	            manifest: require(path.join(distPath, 'react.manifest.json')),
	        })
        ]

最后在index.html中引入这个打包后的文件<script src="./react.dll.js"></script>

缺点就是在上传生产代码的时候需要手动去掉!

自动(推荐)

const AutoDllPlugin = require("autodll-webpack-plugin@0.4.2")

new AutoDllPlugin({
	inject: true, // 在html中引入
	filename: '[name]_[hash].dll.js',
	entry: {
		'vue',
		'element-ui',
		'vuex',
		'vue-router'
	}
})

base64 (dev prod)

之前有记录了【webpack】5-如何解析html文件、图片等其他资源


加哈希值(dev prod)

看上一篇已经配置好了。


CDN优化(prod)

原理就是公司自己建立CDN网址,然后把打包后的文件(js css 图片)放上去,项目根据CDN的方式去获取,说白了就是个私有CDN。

output: {
        publicPath: 'http://cdn.abc.com'  // 修改所有js css静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
    },
    
    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'
                    }
                }
            },
        ]
  }

mode启用production(prod)

前面的文章也有提到,这里再做一些补充。

  • 会自动开启代码压缩
  • vue react等库会自动删除调式代码(例如开发环境的warning)
  • 启动tree-shaking(通过es module引入的代码,用到的才打包起来,例如工程里的tools.js文件里的用不到的函数就不会打包。注意commonJS不会开启tree-shaking)

为什么commonJS不会开启tree-shaking?

先看看关于commonJS与ES Module的区别【Node.js】学习系列2-commonJS的模块规范

所以打包的时候,webpack只能静态分析代码,只认识es module,实现tree-shaking。


scope hosting(prod)

开发环境没有开启这个东西,你去看打包的文件,会发现每一个模块的代码都是被包裹在一个立即执行函数里。这是为了作用域安全的问题,但会带来个问题就是执行函数过多,且代码量也会多一些。

在生产环境打包配置scope hosting,就会减少这种现象,带来了:

  • 创建函数作用域更少(就是函数的数量更少)
  • 代码可读性更好
  • 代码体积更小

配置方式先不记录了。


在vue-cli中配置常用的性能优化设置

可以参考下b站这个视频:vue-cli项目实战中优化打包速度,实用技巧分享

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值