likes
comments
collection
share

你有一份webpack性能优化指南,请查收~

作者站长头像
站长
· 阅读数 10

前言

最近在优化公司的一个webpack项目,所以整理了webpack常见的优化手段,一起来看看吧!

优化方向

首先要弄清楚webpack打包的优化方向是什么,这样才能这知道从哪方面下手。webpack主要有两个优化方向:

  1. webpack如何优化打包构建速度?这是为了提高开发体验和效率
  2. 如何优化产出代码?这个是为了提升产品性能

webpack性能优化方向一:打包构建速度

1.优化babel-loader

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

2.IgnorePlugin:可以避免引用无用模块

举个例子:

import moment from 'moment'默认会引入所有语言的JS代码,我们很多时候其实只引入中文就够了。

// 使用
import moment from 'moment' // 235.4k (gzipped: 66.3k)
import 'moment/locale/zh-cn' // 手动引入中文语言包
moment.locale('zh-cn') // 设置语言为中文
console.log('locale', moment.locale())
console.log('date', moment().format('ll')) // 2021年xx月xx日
{
  plugins: [
    // 忽略 moment 下的locale目录
    new webpack.IgnorePlugin(/\.\/locale/, /moment/)
  ]
}

3.noParse:避免重复打包

module.exports = {
  module: {
    // 忽略对`react.min.js`文件的递归解析处理
    noParse: [/react\.min\.js/],
  }
}

IgnorePlugin vs noParse

  • IgnorePlugin直接不引入,代码中没有
  • noParse引入,但不打包

4.happyPack:多进程打包

  • JS单线程,开启多进程打包
  • 提高构建速度(利用多核CPU)
const HappyPack = require('happypack')
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        // 把对.js文件的处理转交给id为babel的HappyPack实例
        use: ['happypack/loader?id=babel'],
        include: path.resolve(__dirname, 'src'),
      }
    ]
  },
  plugins: [
    new HappyPack({
      // id代表当前的HappyPack是用来处理一类特定的文件
      id: 'babel',
      // 如何处理.js文件,用法和Loader配置中一样
      loaders: ['babel-loader?cacheDirectory'],
    })
  ]
}

5.ParallelUglifyPlugin:多进程压缩JS

  • webpack内置Uglify工具压缩JS
  • JS单线程,开启多进程压缩更快
  • 和happyPack同理
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
module.exports = {
  plugins: [
    // 并行压缩JS
    new ParallelUglifyPlugin({
      // 还是使用UglifyJS压缩,只不过帮助开启了多进程
      uglifyJS: {
        output: {
          beautify: false, // 最紧凑的输出
          comments: true, // 删除所有的注释
        },
        compress: {
          // 删除所有的`console`语句,可以兼容ie浏览器
          drop_console: true,
          // 内嵌定义了但是只用到一次的变量
          collapse_vars: true,
          // 提起出出现多次但是没有的定义成变量去引用的静态值
          reduce_vars: true,
        }
      }
    })
  ]
}

关于开启多进程:

  • 项目较大,打包较慢,开启多进程能提高速度
  • 项目较小,打包很快,开启多进程会降低速度(进程开销)
  • 按需使用

6.自动刷新

module.exports = {
  watch: true, // 开启监听,默认为false
  // 注意:开启监听之后,webpack-dev-server会自动开启刷新浏览器!!!
  // 监听配置
  watchOptions: {
    ignored: /node_modules/,
    // 防抖300ms,防止编译频率太高
    aggregateTimeout: 300, // 默认为300ms
    // 判断文件是否发生变化是通过不停的去询问系统指定文件有没有变化来实现的
    poll: 1000, // 默认每隔1000毫秒询问一次
  }
}

一般配置webpack-dev-server就够了。

7.热更新

  • 自动刷新:整个网页全部刷新,速度较慢,状态会丢失
  • 热更新:新代码生效,网页不刷新,状态不丢失
// webpack.config.js
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin')
module.exports = {
  entry: {
    index: [
      'webpack-dev-server/client?http://localhost:8080/',
      'webpack/hot/dev-server',
      path.join(__dirname, 'src', 'index.js'),
    ],
    other: path.join(__dirname, 'src', 'other.js'),
  },
  devServer: {
    port: 8080,
    progress: true, // 显示打包的进度条
    contentBase: path.join(__dirname, 'dist'), // 根目录
    open: true, // 自动打开浏览器
    compress: true, // 启动gzip压缩

    hot: true,
  },
  plugins: [
    new HotModuleReplacementPlugin(),
  ]
}

// index.js
const hello = require('./hello')
if (module.hot) {
  module.hot.accept(['./hello'], () => {
    console.log(hello) 
  })
}

8.DllPlugin:动态链接库插件

  • 前端框架如Vue、React,体积大,构建慢
  • 较稳定,不常升级版本
  • 同一个版本只构建一次即可,不用每次都重新构建

使用方式如下:

  • webpack已内置DllPlugin支持
  • DllPlugin - 打包出dll文件
  • DllReference - 使用dll文件
// webpack.dll.js 单独抽离一个dll配置文件
const path = require('path')
const DllPlugin = require('webpack/lib/DllPlugin')
const distPath = path.join(__dirname, 'dist')
module.exports = {
  mode: 'development',
  entry: {
    // 把React相关模块放到一个单独的动态链接库
    react: ['react', 'react-dom'],
  },
  output: {
    // 输出的动态链接库的文件名称,[name]代表当前动态链接库的名称,也就是entry中配置的react
    filename: '[name].dll.js',
    // 输出的文件目录
    path: distPath,
    // 存放动态链接库的全局变量名称,例如对用react来说就是_dll_react
    // 之所以在前面加上_dll是为了防止全局变量冲突
    library: '_dll_[name]',
  },
  plugins: [
    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'),
    })
  ]
}

// package.json
{
  "scripts": {
    "dll": "webpack --config webpack.dll.js"
  }
}
// 先运行npm run dll打包出react.dll.js和react.manifest.json文件

// webpack.dev.js
const path = require('path')
// 一、使用react.manifest.json文件
// 1.引入DllReferencePlugin
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin')
const srcPath = path.join(__dirname, 'src', 'index.js')
const distPath = path.join(__dirname, 'dist')
module.exports = {
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.js/,
        loader: ['babel-loader'],
        include: srcPath,
        // 2.不要再转换node_modules
        exclude: /node_modules/,
      }
    ]
  },
  plugins: [
    // 3.告诉webpack使用了哪些动态链接库
    new DllReferencePlugin({
      manifest: require(path.join(distPath, 'react.manifest.json')),
    })
  ]
}
// 二、使用react.dll.js文件
// index.html
<script src="./react.dll.js"></script>

构建速度优化总结

webpack优化构建速度(可用于生产环境):

  • 优化babel-loader
  • IgnorePlugin
  • noParse
  • happyPack
  • ParallelUglify(这个一般只会用于生产环境)

webpack优化构建速度(不用于生产环境):

  • 自动刷新
  • 热更新
  • DllPlugin

webpack性能优化方向二:产出代码

  • 体积更小
  • 合理分包,不重复加载
  • 速度更快、内存使用更少

策略:

1. 小图片base64编码

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|jpeg|gif)$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 8 * 1024,
            outputPath: '/img/',
            // 设置图片的cdn地址(也可以统一在外面的output中)
            // publicPath: 'http://cdn.abc.com',
          }
        }
      }
    ]
  }
}

###2. bundle加hash

module.exports = {
  output: {
    filename: '[name].[contentHash:8].js',
    path: distPath,
    // 修改所有静态文件url的前缀
    // publicPath: 'http://cdn.abc.com', 
  }
}

3. 懒加载

打包import()、组件、路由、组件的异步加载,图片的懒加载等。

4. 提取公共代码splitChunks

module.exports = {
  optimization: {
    // 压缩css
    // minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin()]
    splitChunks: {
      /**
      initial 入口chunk,对于异步导入的文件不处理
      async 异步chunk,只对异步导入的文件处理
      all 全部chunk
      */
      chunks: 'all',
      // 缓存分组
      cacheGroups: {
        // 第三方模块
        vendor: {
          name: 'vendor',
          priority: 1, // 权限更高,优先抽离
          test: /node_modules/,
          minSize: 0, // 大小限制
          minChunks: 1, // 最少复用过几次
        }
        // 公共的模块
        common: {
          name: 'common',
          priority: 0,
          minSize: 0,
          minChunks: 2,
        }
      }
    }
  }
}

5. IgnorePlugin

可以使用IgnorePlugin减少打包体积。

6. 使用CDN加速

可以将静态资源html css js等上传到CDN,以加快访问速度。

7. 使用production

  • 自动开启代码压缩
  • Vue、React等会自动删除调试代码(如开发环境的warning)
  • 启动Tree-Shaking

8. 使用Scope Hosting

  • 代码体积更小
  • 创建函数作用域更少
  • 代码可读性更好

打包文件:

// hello.js
export default 'Hello'

// main.js
import str from './hello.js'
console.log(str)

默认打包结果:

[
  (function (module, __webpack_exports__, __webpack_require__) {
    var __WEBPACK_IMPORTED_MODULE_0__util_js__ = __webpack_require__(1);
    console.log(__WEBPACK_IMPORTED_MODULE_0__util_js__["a"]);
  }),
  (function (module, __webpack_exports__, __webpack_require__) {
    __webpack_exports__["a"] = ('hello');
  }),
]

开启Scope Hosting:

[
  (function (module, __webpack_exports__, __webpack_require__) {
    var hello = ('hello');
    console.log(hello);
  }),
]

配置Scope Hoisting:

const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin')
module.exports = {
  resolve: {
    // 针对npm中的第三方模块优先采用jsnext:main中指向的ES6模块化语法的文件
    mainFields: ['jsnext:main', 'browser', 'main'],
  },
  plugins: [
    // 开启Scope Hoisting
    new ModuleConcatenationPlugin(),
  ]
}

小结

webpack优化一直是老生常谈的话题,上面给大家介绍了很多优化思路,大家可以根据自己的项目需要去运用合理的优化策略,以达到最好的优化状态。