webpack性能优化

114 阅读7分钟

本篇内容汇集webpack性能优化相关知识点,包含打包效率、多进程压缩js、自动刷新、热更新DllPlugin、打包效率总结、产出代码优化。

一、打包效率

1、优化babel-loader

Webpack是一个强大的打包工具,但是随着项目的复杂度增加,Webpack打包的速度会变得很慢。针对这个问题,我们可以采取一些措施来优化Webpack的性能。

优化babel-loader的几个方法:

  1. 使用cacheDirectory选项:在babel-loader中添加cacheDirectory选项,可以让Webpack在第一次编译时将编译结果缓存起来,下次编译时可以直接使用缓存,从而提高编译速度。示例代码如下:
{
  test: /\.js$/,
  use: {
    loader: 'babel-loader',
    options: {
      cacheDirectory: true
    }
  },
  exclude: /node_modules/
}

2. 使用HappyPack插件:HappyPack可以将loader的执行过程放在单独的进程中,从而提高编译速度。示例代码如下:

const HappyPack = require('happypack');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'happypack/loader?id=js',
        exclude: /node_modules/
      }
    ]
  },
  plugins: [
    new HappyPack({
      id: 'js',
      threadPool: happyThreadPool,
      loaders: [
        {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true
          }
        }
      ]
    })
  ]
};

3. 使用babel-plugin-import按需加载:如果你的项目使用了第三方UI组件库,可以使用babel-plugin-import插件来实现按需加载,减小打包体积,从而提高编译速度。示例代码如下:

{
  "plugins": [
    ["import", {
      "libraryName": "antd",
      "libraryDirectory": "es",
      "style": "css"
    }]
  ]
}

以上是优化babel-loader的几个方法,当然还有很多其他的优化Webpack性能的方法,比如使用DllPlugin和DllReferencePlugin插件、使用Tree Shaking等。总之,我们需要根据具体的项目情况来选择合适的优化方法。

2、IgnorePlugin 避免引入哪些模块

以常用的 moment 为例。安装 npm i moment -d 并且 import moment from 'moment' 之后,monent 默认将所有语言的 js 都加载进来,使得打包文件过大。可以通过 ignorePlugin 插件忽略 locale 下的语言文件,不打包进来。

plugins: [
    // 忽略 moment 下的 /locale 目录
    new webpack.IgnorePlugin(/./locale/, /moment/)
]
import moment from 'moment'
import 'moment/locale/zh-cn' // 手动引入中文语言包
moment.locale('zh-cn')

3、noParse 避免重复打包

module.noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。 原因是一些库,例如 jQuery 、ChartJS, 它们庞大又没有采用模块化标准,让 Webpack 去解析这些文件耗时又没有意义。

module.exports = {
  module: {
    // 独完整的 *`react.min.js`* 文件就没有采用模块化
    // 忽略对 *`react.min.js`* 文件的递归解析处理
    noParse: [/react\.min\.js$/],
  },
};

两者对比一下:

  • IgnorePlugin 直接不引入,代码中不存在
  • noParse 引入,但不再打包编译

4、happyPack 多进程打包

【注意】大型项目,构建速度明显变慢时,作用才能明显。否则,反而会有副作用。

webpack 是基于 nodejs 运行,nodejs 是单线程的,happyPack 可以开启多个进程来进行构建,发挥多核 CPU 的优势。

const path = require('path')
const HappyPack = require('happypack')

module.exports = {
  module: {
    rules: [
      {
        test: /.js$/,
        // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
        use: ['happypack/loader?id=babel'],
        exclude: path.resolve(__dirname, 'node_modules')
      }
    ]
  },
  plugins: [
    new HappyPack({
      // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
      id: 'babel',
      // 如何处理 .js 文件,用法和 Loader 配置中一样
      loaders: ['babel-loader?cacheDirectory'],
      // ... 其它配置项
    })
  ]
}

二、多进程压缩js(ParallelUglifyPlugin)

webpack 默认用内置的 uglifyJS 压缩 js 代码。 大型项目压缩 js 代码时,也可能会慢。可以开启多进程压缩,和 happyPack 同理。

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

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

三、自动更新

watch 默认关闭。但 webpack-dev-server 和 webpack-dev-middleware 里 Watch 模式默认开启。

先验证下 webpack 是否能默认自动刷新页面

module.export = {
  watch: true, // 开启监听,默认为 false
  // 注意,开启监听之后,webpack-dev-server 会自动开启刷新浏览器!!!

  // 监听配置
  watchOptions: {
    ignored: /node_modules/, // 忽略哪些
    // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
    // 默认为 300ms
    aggregateTimeout: 300,
    // 判断文件是否发生变化是通过不停的去询问系统指定文件有没有变化实现的
    // 默认每隔1000毫秒询问一次
    poll: 1000
  }
}

四、热更新

热更新(Hot Module Replacement,简称HMR)是webpack中一个非常重要的性能优化手段。它可以在不刷新整个页面的情况下,实时更新页面中的模块内容,从而提升开发效率。

在webpack中,我们可以通过配置devServer选项来开启热更新功能。具体配置如下:

const webpack = require('webpack');

module.exports = {
  // ...
  devServer: {
    hot: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
};

这里我们使用了devServer选项来开启热更新功能,同时使用了HotModuleReplacementPlugin插件来实现热更新。

配置完成后,我们可以在代码中使用module.hot.accept()方法来实现模块的热更新。具体使用方法如下:

import { sum } from './math.js';

console.log(sum(1, 2));

if (module.hot) {
  module.hot.accept('./math.js', function() {
    console.log('math.js has been updated');
    console.log(sum(1, 2));
  });
}

这里我们使用了module.hot.accept()方法来监听math.js模块的变化,当模块发生变化时,会触发回调函数中的代码,从而实现模块的热更新。

需要注意的是,热更新只适用于开发环境,不适用于生产环境。在生产环境中,我们需要使用webpack的代码分割和懒加载等技术来优化性能。

五、动态链接库(DllPlugin)

Webpack性能优化是一个非常重要的话题,动态链接库(DLL)是其中的一种优化方式。DLL是一种将第三方库打包为单独的文件的技术,然后在应用程序中动态加载这些库。这样做的好处是,可以避免每次打包时都重新编译这些第三方库,从而提高打包速度。

使用DLL的步骤如下:

  1. 创建一个Webpack配置文件,用于打包第三方库。这个配置文件需要设置entry和output属性,来指定要打包的库和输出文件的位置。

  2. 在打包第三方库的Webpack配置文件中,使用DllPlugin插件来生成一个manifest文件。这个manifest文件包含了第三方库的映射关系,可以在应用程序中使用。

  3. 在应用程序的Webpack配置文件中,使用DllReferencePlugin插件来引用第二步中生成的manifest文件。这个插件会告诉Webpack,哪些第三方库已经被打包成单独的文件,并且在应用程序中需要动态加载这些文件。

  4. 在应用程序中,通过script标签动态加载第三方库的文件。这个过程可以通过使用html-webpack-plugin插件来自动完成。

六、产出代码优化

使用 production

  • 开启压缩代码
  • 开启 tree shaking(必须是 ES6 Module 语法才行)

ES6 Module 和 commonjs 的区别

  • ES6 Module 是静态引入,编译时引入
  • commonjs 是动态引入,执行时引入
// commonjs
let apiList = require('../config/api.js')
if (isDev) {
    // 可以动态引入,执行时引入
    apiList = require('../config/api_dev.js')
}
import apiList from '../config/api.js'
if (isDev) {
    // 编译时报错,只能静态引入
    import apiList from '../config/api_dev.js'
}

小图片 base64 编码

bundle 加 hash

使用 CDN

配置 publicPath

提取公共改代码

懒加载

scope hosting 将 module 合并到一个函数中

使用前后的对比,使用的好处

配置如下

const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin')

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

同时,考虑到 Scope Hoisting 依赖源码需采用 ES6 模块化语法,还需要配置 mainFields。因为大部分 Npm 中的第三方库采用了 CommonJS 语法,但部分库会同时提供 ES6 模块化的代码,为了充分发挥 Scope Hoisting 的作用。

七、总结 - 提高构建效率的方法

哪些可用于线上,哪些用于线下

  • 优化 babel-loader(可用于线上)
  • IgnorePlugin 避免引入哪些模块(可用于线上)
  • noParse 避免重复打包(可用于线上)
  • happyPack 多进程打包(可用于线上)
  • ParallelUglifyPlugin 多进程压缩 js(可用于线上)
  • 自动刷新(仅开发环境)
  • 热更新(仅开发环境)
  • DllPlugin(仅开发环境)