webpack性能优化

269 阅读5分钟

webpack开发环境优化

热更新

module.exports = {
    devServer:{
        hot:true // 开启HMR
    },
    plugins:[
        // 可选
        new webpack.HotModuleReplacementPlugin()
    ]
}

加快打包速度

🍊SWC

  • SWC 代替 babel
  • 全称是 Speedy Web Compiler,它是一个使用 Rust 编写的编译器。
  • swc 特点:
    • 高性能
    • 兼容性
    • 生态系统
  • 用 swc 替换 babel 之后,能够获取到的性能上面的收益:
    • 编译速度
    • 多线程处理
    • 内存管理

🍊 使用 thread-loader

  • thread-loader 通过创建多个子线程,并行处理文件,从而减少主线程的负载,加速整个编译过程。
  • 为loader的运行开启多线程
  • thread-loader会开启一个线程池,线程池中包含适量的线程
  • 它会把后续的loader放到线程池的线程中运行,以提高构建效率
  • 由于后续的loader会放到新的线程中,所以,后续的loader不能:
    • 使用 webpack api 生成文件
    • 无法使用自定义的 plugin api
    • 无法访问 webpack options

在实际的开发中,可以进行测试,来决定thread-loader放到什么位置

🍊 利用 webpack5 的持久化缓存技术

持久化缓存技术是 webpack5 引入的新技术,可以对构建内容进行缓存:

  • memory:缓存在内存中,适用于开发环境
  • filesystem:以文件的形式缓存在磁盘上,适用于生产环境

一般缓存的内容:

  • 模块缓存
  • 解析缓存
  • 插件缓存
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
   // ...
  },
  module: {
   // ..
  },
  cache: {
    type: 'filesystem', // 使用文件系统进行缓存
    cacheDirectory: path.resolve(__dirname, '.webpack_cache'), // 缓存目录
    buildDependencies: {
      config: [__filename], // 当配置文件改变时,重新构建缓存
    },
    name: 'my-cache', // 缓存名称
    version: '1.0', // 缓存版本
  },
};

🍊 发环境去掉 hash

在 webpack 配置中,hash 的目的是为了生成唯一的文件名:bundle.(hash).js

不同环境下对 hash 的需求是不一样的:

  • 开发环境:频繁进行代码修改和构建,不需要长时间缓存,生成 hash 会增加不必要的构建时间。
  • 生产环境:希望生成 hash,以便利用浏览器缓存机制,提高加载速度。

因此在 webpack 配置文件里面,就可以动态的配置是否要生成 hash:

const path = require('path');

module.exports = (env, argv) => {
  // 获取当前的构建模式
  const isProduction = argv.mode === 'production';

  return {
    entry: './src/index.js',
    output: {
      // 根据不同的构建模式来决定生成的文件名是否要包含 hash 值
      filename: isProduction ? 'bundle.[contenthash].js' : 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
    module: {
      rules: [
        {
          test: /.m?js$/,
          exclude: /node_modules/,
          use: {
            loader: 'swc-loader',
          },
        },
      ],
    },
    cache: {
      type: 'filesystem',
    },
  };
};

在 package.json 中可以配置启动模式:

{
  "scripts": {
    "start": "webpack serve --mode development",
    "build": "webpack --mode production"
  }
}

🍊 升级老旧的 plugin

例如升级 terser-webpack-plugin,这个插件使用了压缩代码的,新版本相比旧版本就有很大的提升:

  1. 性能改进:
    • 算法优化:插件的新版本通常包含更高效的算法和优化策略,可以在保持相同压缩率的同时加快压缩速度。
    • 多线程处理:新版本可能引入了对多线程的支持,从而利用多核 CPU 提升压缩性能。
  1. Bug 修复和改进:
    • 修复性能瓶颈:老版本可能存在一些未被发现的性能瓶颈或 bug,通过升级可以避免这些问题。
    • 代码改进:维护者和社区贡献者会不断地改进插件的代码,以提高其性能和稳定性。
  1. 新特性:
    • 缓存支持:新版本支持持久化缓存功能,从而避免重复压缩相同的代码块,进一步提升构建速度。
    • 配置优化:简化和改进配置选项,使得更容易进行性能调优。

减少模块解析

什么叫做模块解析?

模块解析包括:抽象语法树分析、依赖分析、模块语法替换

不做模块解析会怎样?

如果某个模块不做解析,该模块经过loader处理后的代码就是最终代码。

如果没有loader对该模块进行处理,该模块的源码就是最终打包结果的代码。

如果不对某个模块进行解析,可以缩短构建时间

哪些模块不需要解析?

模块中无其他依赖:一些已经打包好的第三方库,比如jquery

module.exports = {
    mode: "development",
    devtool: "source-map",
    module: {
        noParse: /node_moudules/
    }
}

优化loader性能

进一步限制loader的应用范围

思路是:对于某些库,不使用loader

例如:babel-loader可以转换ES6或更高版本的语法,可是有些库本身就是用ES5语法书写的,不需要转换,使用babel-loader反而会浪费构建时间

lodash就是这样的一个库

通过module.rule.excludemodule.rule.include,排除或仅包含需要应用loader的场景


module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /lodash/,
                use: "babel-loader"
            }
        ]
    }
}

如果暴力一点,甚至可以排除掉node_modules目录中的模块,或仅转换src目录的模块


module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                //或
                // include: /src/,
                use: "babel-loader"
            }
        ]
    }
}

这种做法是对loader的范围进行进一步的限制,和noParse不冲突,想想看,为什么不冲突

缓存loader的结果

我们可以基于一种假设:如果某个文件内容不变,经过相同的loader解析后,解析后的结果也不变

于是,可以将loader的解析结果保存下来,让后续的解析直接使用保存的结果

cache-loader可以实现这样的功能


module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                use: ['cache-loader', ...loaders]
            },
        ],
    },
};

有趣的是,cache-loader放到最前面,却能够决定后续的loader是否运行

实际上,loader的运行过程中,还包含一个过程,即pitch

cache-loader还可以实现各自自定义的配置,具体方式见文档

特别注意,开启和管理线程需要消耗时间,在小型项目中使用thread-loader反而会增加构建时间

webpack生产环境优化

分包

手动分包

和自动分包比较

  • 优点: 构建速度快
  • 缺点: 相比于自动分包, 麻烦, 降低开发效率

🍊 自动分包

  • 抽离公共代码, 合理使用缓存
  • 将react-dom, antd, 这些第三方库抽离出来
  • 这些第三方库的代码基本上是不会变的
  • 将这些第三方库缓存在本地
  • 谁然第一次加载慢,但之后由于缓存的原因,加载就会很快了
module.exports = {
  optimization: {
    splitChunks: {
      // 分包策略
    }
  }
}

🍊 css拆分

  • 将css代码从js中抽离出来
  • 打包成一个独立的css文件
  • MiniCssExtractPlugin.loader作用: 生成 link 标签
  • style-loader作用: 生成 style 标签
  • 既然css已经被抽离到了一个单独的文件,所以就不需要style标签了,而是link标签
  • new MiniCssExtractPlugin()作用: 单独生成一个css文件
module.exports = {
  module: {
    rules: [{
        test: /\.css$/, 
        use: [MiniCssExtractPlugin.loader, "css-loader"] 
    }]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: "./public/index.html",
      chunks: ["index"]
    }),
    new MiniCssExtractPlugin({
      filename: "[name].[hash:5].css",
      // chunkFilename是配置来自于分割chunk的文件名
      chunkFilename: "common.[hash:5].css" 
    })
  ]
}

css分包

  • 多页面应用会拆分出多个css文件
  • css分包就是提取出这些文件的公共样式到一个单独的文件中,从而减少代码体积
module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        styles: {
          test: /\.css$/, // 匹配样式模块
          minSize: 0, // 覆盖默认的最小尺寸,这里仅仅是作为测试
          minChunks: 2 // 覆盖默认的最小chunk引用数
        }
      }
    }
  },
  module: {
    rules: [{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: "./public/index.html",
      chunks: ["index"]
    }),
    new MiniCssExtractPlugin({
      filename: "[name].[hash:5].css",
      // chunkFilename是配置来自于分割chunk的文件名
      chunkFilename: "common.[hash:5].css" 
    })
  ]
}

🍊 懒加载

  • 打包成一个异步的chunk
const btn = document.querySelector("button");
btn.onclick = async function() {
  //动态加载
  //import 是ES6的草案
  //浏览器会使用JSOP的方式远程去读取一个js模块
  //import()会返回一个promise   (* as obj)
  // const { chunk } = await import(/* webpackChunkName:"lodash" */"lodash-es");
  const { chunk } = await import("./util");
  const result = chunk([3, 5, 6, 7, 87], 2);
  console.log(result);
};

react中

import { lazy } from 'react';

// import Editor from '../question/editor';

const Editor = lazy(() => import('../question/editor'));

代码压缩

🍊 Terser或者UgilyfyJs

  • 例如: 把代码变成一行
  • UgilyfyJs: 不支持es6语法
  • Terser: 支持es6语法,webpack内置Teser

webpack自动集成了Terser

如果你想更改、添加压缩工具,又或者是想对Terser进行配置,使用下面的webpack配置即可

const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
  optimization: {
    // 是否要启用压缩,默认情况下,生产环境会自动开启
    minimize: true, 
    minimizer: [ // 压缩时使用的插件,可以有多个
      new TerserPlugin(), 
      new OptimizeCSSAssetsPlugin()
    ],
  },
};

🍊 three shaking

  • 使用export xxx导出,而不使用export default {xxx}导出

  • 使用import {xxx} from "xxx"导入,而不使用import xxx from "xxx"导入

🍊 gzip

CmpressionWebpackPlugin 用于对构建后的 .js 文件进行压缩,test: /.js/ 表示只对 .js 文件应用压缩,而 minRatio: 0.5 表示只有文件大小减少一半或以上时才进行压缩。

const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const CmpressionWebpackPlugin = require("compression-webpack-plugin")
module.exports = {
  mode: "production",
  optimization: {
    splitChunks: {
      chunks: "all"
    }
  },
  plugins: [
    new CleanWebpackPlugin(),
    new CmpressionWebpackPlugin({
      test: /\.js/,
      minRatio: 0.5
    })
  ]
};