当前位置:   article > 正文

基于craco配置的react项目的webpack构建优化_craco打包,很慢

craco打包,很慢

项目现状

随着项目的迭代,项目越来越大,依赖越来越多,冷启动和热更新的速度越来越慢,打包后的项目体积也逐渐膨胀;为了优化开发体验和用户体验,需要对项目进行webpack构建优化,此次优化主要涉及构建速度和构建体积的优化。

以下是项目原carco.config.js配置

const FileManagerPlugin = require("filemanager-webpack-plugin");
const Webpack = require("webpack");
const fs = require("fs");
const path = require("path");
const { name, version } = require("./package.json");

const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);
const appPath = resolveApp(".");
const appBuild = resolveApp("build");

process.env.PORT = 3000;
// 环境信息
const env = process.env.REACT_APP_TARGET_ENV;

let source = `${appPath}/config.dev.js`;
if (env === "test") {
  source = `${appPath}/config.test.js`;
} else if (env === "pre") {
  source = `${appPath}/config.pre.js`;
} else if (env === "pro") {
  source = `${appPath}/config.pro.js`;
}

module.exports = {
  reactScriptsVersion: "react-scripts" /* (default value) */,
  plugins: [
    {
      plugin: {
        overrideDevServerConfig: ({ devServerConfig }) => {
          return {
            ...devServerConfig,
            headers: {
              "Access-Control-Allow-Origin": "*",
            },
          };
        },
      },
    },
  ],
  webpack: {
    alias: {
      "@": path.resolve(__dirname, "src"),
      "@components": path.resolve(__dirname, "src/components"),
      "@containers": path.resolve(__dirname, "src/containers"),
      "@constants": path.resolve(__dirname, "src/constants"),
      "@utils": path.resolve(__dirname, "src/utils"),
      "@routes": path.resolve(__dirname, "src/routes"),
      "@assets": path.resolve(__dirname, "src/assets"),
      "@styles": path.resolve(__dirname, "src/styles"),
      "@services": path.resolve(__dirname, "src/services"),
      "@mocks": path.resolve(__dirname, "src/mocks"),
      "@hooks": path.resolve(__dirname, "src/hooks"),
      "@stories": path.resolve(__dirname, "src/stories"),
    },
    // configure: { /* Any webpack configuration options: https://webpack.js.org/configuration */ },
    configure: (webpackConfig, { env, paths }) => {
      // 配置扩展扩展名
      webpackConfig.resolve.extensions = [...webpackConfig.resolve.extensions, ...[".scss", ".css"]];

      if (env !== "development") {
        webpackConfig.plugins = webpackConfig.plugins.concat(
          new FileManagerPlugin({
            events: {
              onEnd: {
                mkdir: [`zip/${name}/dist`, `zip/${name}/template`],
                copy: [
                  {
                    source: `${appPath}/conf${
                      process.env.REACT_APP_TARGET_ENV === "dev" ? ".development.js" : ".production.js"
                    }`,
                    destination: `${appBuild}/config.js`,
                  },
                  {
                    source: source,
                    destination: `${appBuild}/config.js`,
                  },
                  {
                    source: `${path.resolve("build")}`,
                    destination: `zip/${name}/dist`,
                  },
                  {
                    source: path.resolve("template"),
                    destination: `zip/${name}/template`,
                  },
                ],
                archive: [
                  {
                    source: `zip`,
                    destination: path.relative(__dirname, `./${name}-${version}-SNAPSHOT.tar.gz`),
                    format: "tar",
                    options: {
                      gzip: true,
                      gzipOptions: {
                        level: 1,
                      },
                      globOptions: {
                        nomount: true,
                      },
                    },
                  },
                ],
                delete: ["zip"],
              },
            },
            runTasksInSeries: true,
          }),
        );
      }
      webpackConfig.output.library = `${name}-[name]`;
      webpackConfig.output.libraryTarget = "umd";
      //       webpackConfig.output.jsonpFunction = `webpackJsonp_${name}`;
      webpackConfig.output.globalObject = "window";
      return webpackConfig;
    },
  },
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118

分析工具

在优化之前,我们需要了解一些量化分析的工具,使用它们来帮助我们分析需要优化的点。

webpackbar

webpackbar可以在打包时实时显示打包进度。
配置也很简单,如果是在webpack.config.js中,直接在plugins数组中加入即可;

const WebpackBar = require('webpackbar');
module.exports = {
  plugins: [
    ...
    new WebpackBar()
  ]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

如果是在craco.config.js中需要在webpack属性下的plugins里加入:

const WebpackBar = require('webpackbar');
module.exports = {
	  webpack:{
		plugins: [
	    ...
	    new WebpackBar()
	  ]
	} 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

加入这个插件之后就可以在打包的时候看到打包的进度了。
在这里插入图片描述

speed-measure-webpack-plugin

使用speed-measure-webpack-plugin可以看到每个loaderplugin的耗时情况。
和普通插件的使用略有不同,需要用它的wrap方法包裹整个webpack配置项。

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
module.exports = smp.wrap({
  entry: './src/main.js',
  ...
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

对于carco而言,需要在webpack前使用wrap方法包裹住即可;

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
module.exports = {
  reactScriptsVersion: "react-scripts" /* (default value) */,
  webpack: smp.wrap({
  		alias:{},
  }) 
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

打包后,在命令行的输出信息如下,我们可以看出哪些loaderplugin耗时比较久,然后对其进行优化

比如现在我们项目冷启动的时间是4分零4秒
在这里插入图片描述
生产环境打包需要的时间是2分34秒
在这里插入图片描述
这个构建的时间还是挺久的。

webpack-bundle-analyzer

webpack-bundle-analyzer以可视化的方式让我们直观地看到打包的bundle中到底包含哪些模块内容,以及每一个模块的体积大小。我们可以根据这些信息去分析项目结构,调整打包配置,进行优化。

plugins数组中加入该插件。构建完成后,默认会在http://127.0.0.1:8888/展示分析结果。

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
  plugins: [
    ...
    new BundleAnalyzerPlugin()
  ]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

一般我们在本地开发环境不需要分析打包体积,故craco.config.js中进行如下配置:

const WebpackBar = require("webpackbar");
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
const smp = new SpeedMeasurePlugin();

process.env.PORT = 3000;

module.exports = {
  reactScriptsVersion: "react-scripts" /* (default value) */,
  webpack: smp.wrap({
    configure: (webpackConfig, { env, paths }) => {
      if (env !== "development") {
        webpackConfig.plugins = webpackConfig.plugins.concat(
          new BundleAnalyzerPlugin({
            analyzerMode: "server",
            analyzerHost: "127.0.0.1",
            analyzerPort: 8888,
            openAnalyzer: true, // 构建完打开浏览器
            reportFilename: path.resolve(__dirname, `analyzer/index.html`),
          }),
        );
      }
    },
    plugins: [
      new WebpackBar(),
    ],
  }),
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

在这里插入图片描述
webpack-bundle-analyzer会计算出模块文件在三种情形下的大小:

  • stat:文件未经过任何转换的原始大小
  • parsed:文件经过转换后的输出大小(比如babel-loader转换ES6->ES5UglifyJsPlugin压缩等等)
  • gzip:parsed后的文件,经过Gzip压缩的大小

使用speed-measure-webpack-pluginwebpack-bundle-analyzer本身也会增加打包时间(webpack-bundle-analyzer特别耗时),所以建议这两个插件只在开发分析时使用,而在生产环境实际使用中去掉。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
可以看到,我们的项目打包压缩后还有1.03MB大小

优化构建速度

多进程构建

运行在Node.js之上的 Webpack 是单线程的,就算有多个任务同时存在,它们也只能一个一个排队执行。当项目比较复杂时,构建就会比较慢。如今大多数CPU都是多核的,我们可以借助一些工具,充分释放 CPU 在多核并发方面的优势。

比较常见的方案有HappyPackthread-loader,由于happypack的作者已经没有这个项目进行维护了,我们这里选择thread-loader
可以看到,HappyPack最后一次更新是四年前,确实很久远了。
在这里插入图片描述
thread-loader使用起来很简单,就是把它放置在其它loader之前,如下所示。放置在这个thread-loader之后的 loaders会运行在一个单独的worker池中。

module.exports = {
  module:{
    rules:[
      {
          test: /\.js$/,
          use: ['thread-loader','babel-loader']
      }
    ]
  },
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

对于craco来说,项目的loader都已经经过封装了,所以我们需要用到craco提供的一些工具方法来实现给原有loader加上loader的能力;

const { addBeforeLoaders, loaderByName } = require("@craco/craco");
configure: (webpackConfig, { env, paths }) => {
      addBeforeLoaders(webpackConfig, loaderByName("style-loader"), "thread-loader");
      addBeforeLoaders(webpackConfig, loaderByName("style-loader"), "cache-loader");
   }
  • 1
  • 2
  • 3
  • 4
  • 5

可以看到使用thread-loader之后生产环境构建用了1分58秒,构建速度提升了36秒钟
在这里插入图片描述
使用thread-loader之后开发环境冷启动时间将至了1分18秒,足足减少了两分46秒
在这里插入图片描述

利用缓存

利用缓存可以提升二次构建速度(下面的对比图都是二次构建的速度)。使用缓存后,在node_modules中会有一个.cache目录,用于存放缓存的内容。

cache-loader

在一些性能开销较大的 loader 之前添加此cache-loader,以将结果缓存到磁盘中。

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['cache-loader','babel-loader']
      }
    ]
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

可以看到生产环境二次构建速度提升了6秒钟
在这里插入图片描述
开发环境热更新时间为8秒钟
在这里插入图片描述
babel-loader使用缓存,也可以不借助cache-loader,直接在babel-loader后面加上?cacheDirectory=true

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader?cacheDirectory=true']
      }
    ]
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
hard-source-webpack-plugin

hard-source-webpack-plugin用于开启模块的缓存。
这插件很多文章都有推荐,感觉很不错的样子,用起来也很简单,只需要做如下配置:

const HardSourceWebpackPlugin = require("hard-source-webpack-plugin")
module.exports = {
  plugins:[
    new HardSourceWebpackPlugin()
  ]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

然鹅,你以为真的会这么简单吗?
实际在使用的过程中会报错:

Cannot find module 'webpack/lib/DependenciesBlockVariable'
  • 1

笔者也是百思不得其解啊,去Google也没有人遇到这种问题。

不得已,只能去hard-source-webpack-plugingithub上看issue,发现其实有人遇到这个问题的,他们的解决方案就是降低webpack的版本,可笔者这里没办法这么做,因为都集成在craco里了,根本不可能降版本。
可能是因为hard-source-webpack-plugin这个插件太久没更新,webpack版本落后导致的吧,特意查了一下,这个插件最后一次更新是在四年前,所以这个插件是没法使用了。

Webpack的持久化缓存

持久化缓存算得上是 Webpack 5 最令人振奋的特性之一,它能够将首次构建结果持久化到本地文件系统,在下次执行构建时跳过一系列解析、链接、编译等非常消耗性能的操作,直接复用 modulechunk 的构建结果。使用持久化缓存后,构建性能有巨大提升。

那么,为什么开启持久化缓存之后构建性能会有如此巨大的提升呢?一言蔽之,Webpack5 会将首次构建出的 ModuleChunkModuleGraph 等对象序列化后保存到硬盘中,后面再运行的时候就可以跳过一些耗时的编译动作,直接复用缓存信息。

使用:

module.exports = {
	cache: {
      // 缓存类型,支持 'memory' | 'filesystem',需要设置 filesystem 才能开启持久缓存
      type: "filesystem",
      // 缓存文件存放的路径,默认为 node_modules/.cache/webpack
      cacheDirectory: path.resolve(__dirname, ".temp_cache"),
      // 是否输出缓存处理过程的详细日志,默认为 false
      profile: false,
      // 缓存失效时间,默认值为 5184000000
      maxAge: 5184000000,
    },
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

对于craco来说可以使用如下方式配置:

configure: (webpackConfig, { env, paths }) => {
      // 开启持久化缓存
      webpackConfig.cache.type = "filesystem";
      return webpackConfig;
    }
  • 1
  • 2
  • 3
  • 4
  • 5

开启缓存后二次构建时间为1分51秒,快了7秒钟
在这里插入图片描述
开发环境冷启动直接降为7.3秒
在这里插入图片描述

持久化缓存详细配置可参考官方文档

include/exclude

通常来说,loader会处理符合匹配规则的所有文件。比如babel-loader,会遍历项目中用到的所有js文件,对每个文件的代码进行编译转换。而node_modules里的js文件基本上都是转译好了的,不需要再次处理,所以我们用 include/exclude 来帮我们避免这种不必要的转译。

module.exports = {
  module:{
    rules:[
      {
          test: /\.js$/,
          use: ['babel-loader'],
          exclude: /node_modules/
          //或者  include: [path.resolve(__dirname, 'src')]
      }
    ]
  },
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

include直接指定查找文件夹,比exclude效率更高,更能提升构建速度。

由于craco封装了webpack配置,不太好直接修改rules,故采用如下方案:

configure: (webpackConfig, { env, paths }) => {
      // 缩小loaders处理范围
      webpackConfig.module.rules.forEach((rule) => {
        rule.include = path.resolve(__dirname, "src");
      });
      return webpackConfig;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

优化reslove.extensions配置

resolve.extensions用于配置在尝试过程中用到的后缀列表,如果这个列表越长,或者正确的后缀在越后面,就会造成尝试的次数越多,所以resolve.extensions的配置也会影响到构建的性能。 在配置resolve.extensions时你需要遵守以下几点,以做到尽可能的优化构建性能:

  • 后缀尝试列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。
  • 频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。
  • 在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。例如在你确定的情况下把require('./data')写成require('./data.json')

craco默认的reslove.extensions配置是[ '.web.mjs', '.mjs', '.web.js', '.js', '.web.ts', '.ts', '.web.tsx', '.tsx', '.json', '.web.jsx', '.jsx' ],对于我这个项目而言,这个配置是有点冗余和不合理的,因为我这个项目主要是用ts写的,源码中有大量的tsxts文件,所以把extensios做出如下配置优化:

      // 配置扩展扩展名
      webpackConfig.resolve.extensions = [".tsx", ".ts", ".jsx", ".js", ".scss", ".css"];
  • 1
  • 2

之后再打包构建,会发现构建速度有了小幅度提升。
在这里插入图片描述

noParse

以下是webpack官网对noParse的解释:防止 webpack 解析那些任何与给定正则表达式相匹配的文件。忽略的文件中 不应该含有 import, require, define 的调用,或任何其他导入机制,忽略大型的 library 可以提高构建性能。
对于项目中有引用jquery或者lodash这种不需要解析的第三方库而言,忽略对这些第三方库的解析也会略微提升构建性能。
webpack配置:

module.exports = {
  //...
  module: {
    noParse: /jquery|lodash/,
  },
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

craco中的配置:

configure: (webpackConfig, { env, paths }) => {
      // jquery不需要解析;笔者实测,lodash需要解析,可能和我用的版本有关系
      webpackConfig.module.noParse = /jquery/;
      return webpackConfig;
    }
  • 1
  • 2
  • 3
  • 4
  • 5

代码压缩

常用的js代码压缩插件有:uglifyjs-webpack-pluginterser-webpack-plugin

在webpack4中,生产环境默认开启代码压缩。我们也可以自己配置去覆盖默认配置,来完成更定制化的需求。

v4.26.0版本之前,webpack内置的压缩插件是uglifyjs-webpack-plugin,从v4.26.0版本开始,换成了terser-webpack-plugin。我们这里也以terser-webpack-plugin为例,和普通插件使用不同,在optimization.minimizer中配置压缩插件

const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
  optimization: {
    minimizer: [  
      new TerserPlugin({
        parallel: true,  //开启并行压缩,可以加快构建速度
        sourceMap: true, //如果生产环境使用source-maps,则必须设置为true
      })
    ]
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

构建时间降到了1分33秒
在这里插入图片描述

生产环境source-map优化

对于生产环境不需要生成soure-map映射文件的项目可以优化devtool选项,来优化构建;对于carco来说可以同时在生产环境移除source-map-loader

// 生产环境移除source-map-loader
removeLoaders(webpackConfig, loaderByName("source-map-loader"));
  • 1
  • 2

优化之后生产环境打包只需要1分16秒,缩短了近一半的时间。
在这里插入图片描述

优化构建体积

代码分割

分离第三方库和业务代码中的基础库,可以避免单个bundle.js体积过大,加载时间过长。并且在多页面构建中,还能减少重复打包。

SplitChunks

SplitChunks插件是什么呢?
简单的来说就是 Webpack 中一个提取或分离代码的插件,主要作用是提取公共代码,防止代码被重复打包,拆分过大的js文件,合并零散的js文件。

使用配置:

// splitChunks打包优化
      webpackConfig.optimization.splitChunks = {
        ...webpackConfig.optimization.splitChunks,
        cacheGroups: {
          commons: {
            chunks: "all",
            // 将两个以上的chunk所共享的模块打包至commons组。
            minChunks: 2,
            name: "commons",
            priority: 80,
          },
        },
      };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

使用SplitChunks后项目体积减少到了到了995kb
在这里插入图片描述

动态import

动态import的作用主要减少首屏资源的体积,非首屏的资源在用到的时候再去请求,从而提高首屏的加载速度。一个常见的例子就是单页面应用的路由管理(比如vue-router

{
  path: '/list',
  name: 'List',
  component: () => import('../views/List.vue')  
},
  • 1
  • 2
  • 3
  • 4
  • 5

不是直接import组件(import List from '../views/List.vue'),那样会把组件都打包进同一个bundle。而是动态import组件,凡是通过import()引用的模块都会打包到独立的bundle,使用到的时候再去加载。对于功能复杂,又不是首屏必须的资源都推荐使用动态import

treeShaking

使用ES6import/export语法,并且使用下面的方式导入导出你的代码,而不要使用export default

// util.js 导出
export const a = 1
export const b = 2
export function afunc(){}export { a, b, afunc }

// index.js 导入
import { a, b } from './util.js'
console.log(a,b)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

那么在mode:production生产环境,就会自动开启tree-shaking,移除没有使用到的代码,上面例子中的afunc函数就不会被打包到bundle中。

第三方库按需加载

对于antd这种第三方库,开启按需加载;lodash也可以开启按需加载

安装相关插件:

npm install babel-plugin-import babel-plugin-lodash --save-dev
  • 1

配置babel

module.exports = {
	babel: {
	    plugins: [
	      // antd按需加载
	      [
	        "import",
	        {
	          libraryName: "antd",
	          libraryDirectory: "es",
	          style: "css",
	        },
	      ],
	      // lodash按需加载
	      "lodash",
	    ],
	  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

dayjs替换antdmoment

众所周知moment的体积远远大于dayjs的体积,而antd默认使用的是moment,这在无形中也增加了项目打包的体积,好在antd官方提供了一个webpack插件方便我们将moment替换为dayjs;(为什么替换成dayjs而不是date-fn呢?主要是因为dayjsapimoment几乎一样,基本没有切换成本)。
替换步骤如下:

  • 安装依赖
npm install antd-dayjs-webpack-plugin -D
  • 1
  • webpack配置文件中设置插件
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin');
module.exports = {
  // ...
  plugins: [
    new AntdDayjsWebpackPlugin()
  ]
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

详细配置可参考官方插件

注意:在craco.config.js中需要配置在webpack的插件中

  • 如果要使用非"英语"语言,需要在项目文件里引入对应的语言包
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
  • 1
  • 2
  • 3

动态链接库

其实第三方库代码基本都是成熟的,不用作什么处理。因此,我们可以将项目的第三方库代码分离出来。
常见的处理方式有三种:

  • Externals
  • SplitChunks
  • DllPlugin

Externals可以避免处理第三方库,但每一个第三方库都得在html文档中增加一个script标签来引入,一个页面过多的js文件下载会影响网页性能,而且有时我们只使用第三方库中的一小部分功能,用script标签全量引入不太合理。
SplitChunks在每一次构建时都会重新构建第三方库,不能有效提升构建速度。
这里推荐使用DllPluginDLLReferencePlugin(配合使用),它们是webpack的内置插件。DllPlugin会将不频繁更新的第三方库单独打包,当这些第三方库版本没有变化时,就不需要重新构建。

使用方法:
1.使用DllPlugin打包第三方库
2.使用DLLReferencePlugin引用manifest.json,去关联第1步中已经打好的包

  • 首先,新建一个webpack配置文件webpack.dll.js用于打包第三方库
// webpack.dll.js
const path = require('path');
const { DllPlugin } = require('webpack');

module.exports = {
  mode: 'production',
  entry: {
    vendor: ['react', 'react-dom', 'react-router-dom'], // 需要统一打包的类库
  },
  output: {
    filename: '[name].dll.js', //name必须要和output.library一致
    path: path.resolve(__dirname, 'public/lib'),
    library: '[name]',
  },
  plugins: [
    new DllPlugin({
      name: '[name]',
      path: path.resolve(__dirname, 'public/lib/[name].json'), //manifest.json的存放位置
    }),
  ],
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • package.json文件中的scripts属性下新增如下命令
"dll": "webpack --config webpack.dll.js",
  • 1

然后在控制台运行npm run dll即可将第三方库一起打包好;之后如果没有需要更新第三方库的需求都不需要再打包了

  • 修改项目的webpack配置,去关联第1步中已经打好的包
const { DllReferencePlugin } = require('webpack');
const manifest = require('./public/lib/vendor.json');
module.exports = {
	// 这是craco.config.js中的配置,如果是webpack.config.js中则不需要加webpack这一层
	webpack:{
		plugins:[new DllReferencePlugin({ manifest })]
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • html中引入第一步打包好的vendor.dll.js引入的顺序需要在业务代码的前面,如果不引入或引入顺序错误的话会报错vendor is undefined

gzip

开启gzip压缩,可以减小文件体积。在浏览器支持gzip的情况下,可以加快资源加载速度。服务端和客户端都可以完成gzip压缩,服务端响应请求时压缩,客户端应用构建时压缩。但压缩文件这个过程本身是需要耗费时间和CPU资源的,如果存在大量的压缩需求,会加大服务器的负担。

所以可以在构建打包时候就生成gzip压缩文件,作为静态资源放在服务器上,接收到请求后直接把压缩文件返回。

使用webpack生成gzip文件需要借助compression-webpack-plugin,使用配置如下:

const CompressionWebpackPlugin = require("compression-webpack-plugin")
module.exports = {
  plugins: [
     new CompressionWebpackPlugin({
       test: /\.(js|css)$/,         //匹配要压缩的文件
       algorithm: "gzip"
     })
  ]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在这里插入图片描述
打包完成后除了生成打包文件外,还会额外生成 .gz后缀的压缩文件。可以看出,gzip压缩文件的体积比未压缩文件的体积小很多。

但是!需要服务器支持

今天就到这里啦!
以下是craco.config.js的完整配置:

const FileManagerPlugin = require("filemanager-webpack-plugin");
const WebpackBar = require("webpackbar");
const { DllReferencePlugin } = require("webpack");
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
const TerserPlugin = require("terser-webpack-plugin");
const CompressionWebpackPlugin = require("compression-webpack-plugin");
const { addBeforeLoaders, removeLoaders, loaderByName } = require("@craco/craco");
const fs = require("fs");
const path = require("path");
const { name, version } = require("./package.json");
const manifest = require("./public/lib/vendor.json");

const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);
const appPath = resolveApp(".");
const appBuild = resolveApp("build");

const smp = new SpeedMeasurePlugin();

process.env.PORT = 3000;
// 环境信息
const env = process.env.REACT_APP_TARGET_ENV;

let source = `${appPath}/config.dev.js`;
if (env === "test") {
  source = `${appPath}/config.test.js`;
} else if (env === "pre") {
  source = `${appPath}/config.pre.js`;
} else if (env === "pro") {
  source = `${appPath}/config.pro.js`;
}

module.exports = {
  reactScriptsVersion: "react-scripts" /* (default value) */,
  babel: {
    plugins: [
      // lodash按需加载
      "lodash",
    ],
    loaderOptions: {
      // babel-loader开启缓存
      cacheDirectory: true,
    },
  },
  plugins: [
    {
      plugin: {
        overrideDevServerConfig: ({ devServerConfig }) => {
          return {
            ...devServerConfig,
            headers: {
              "Access-Control-Allow-Origin": "*",
            },
          };
        },
        overrideWebpackConfig: ({ webpackConfig, context: { env } }) => {
          if (env !== "development") {
            // 缩小生产环境所有loaders的检索范围
            webpackConfig.module.rules[0].oneOf.forEach((rule) => {
              rule.include = path.resolve(__dirname, "src");
            });
          } else {
            // 缩小本地开发环境所有loaders的检索范围
            webpackConfig.module.rules[0].include = path.resolve(__dirname, "src");
            webpackConfig.module.rules[1].oneOf.forEach((rule, index) => {
              rule.include = path.resolve(__dirname, "src");
              // 本地开发环境babel-loader比较耗时,故加上thread-loader
              if (index === 3) {
                const babelLoader = {
                  loader: rule.loader,
                  options: rule.options,
                };
                rule.use = ["thread-loader", babelLoader];
                delete rule.loader;
                delete rule.options;
              }
            });
          }
          return {
            ...webpackConfig,
          };
        },
      },
    },
  ],
  webpack: smp.wrap({
    alias: {
      "@": path.resolve(__dirname, "src"),
      "@components": path.resolve(__dirname, "src/components"),
      "@containers": path.resolve(__dirname, "src/containers"),
      "@constants": path.resolve(__dirname, "src/constants"),
      "@utils": path.resolve(__dirname, "src/utils"),
      "@routes": path.resolve(__dirname, "src/routes"),
      "@assets": path.resolve(__dirname, "src/assets"),
      "@styles": path.resolve(__dirname, "src/styles"),
      "@services": path.resolve(__dirname, "src/services"),
      "@mocks": path.resolve(__dirname, "src/mocks"),
      "@hooks": path.resolve(__dirname, "src/hooks"),
      "@stories": path.resolve(__dirname, "src/stories"),
    },
    // configure: { /* Any webpack configuration options: https://webpack.js.org/configuration */ },
    configure: (webpackConfig, { env }) => {
      // 配置扩展扩展名优化
      webpackConfig.resolve.extensions = [".tsx", ".ts", ".jsx", ".js", ".scss", ".css", ".json"];

      // 作为子应用接入微前端的打包适配,不接入微前端可以不需要
      webpackConfig.output.library = `${name}-[name]`;
      webpackConfig.output.libraryTarget = "umd";
      webpackConfig.output.globalObject = "window";
      // splitChunks打包优化
      webpackConfig.optimization.splitChunks = {
        ...webpackConfig.optimization.splitChunks,
        cacheGroups: {
          commons: {
            chunks: "all",
            // 将两个以上的chunk所共享的模块打包至commons组。
            minChunks: 2,
            name: "commons",
            priority: 80,
          },
        },
      };
      // 开启持久化缓存
      webpackConfig.cache.type = "filesystem";
      // 生产环境打包优化
      if (env !== "development") {
        webpackConfig.plugins = webpackConfig.plugins.concat(
          new FileManagerPlugin({
            events: {
              onEnd: {
                mkdir: [`zip/${name}/dist`, `zip/${name}/template`],
                copy: [
                  {
                    source: source,
                    destination: `${appBuild}/config.js`,
                  },
                  {
                    source: `${path.resolve("build")}`,
                    destination: `zip/${name}/dist`,
                  },
                  {
                    source: path.resolve("template"),
                    destination: `zip/${name}/template`,
                  },
                ],
                archive: [
                  {
                    source: `zip`,
                    destination: path.relative(__dirname, `./${name}-${version}-SNAPSHOT.tar.gz`),
                    format: "tar",
                    options: {
                      gzip: true,
                      gzipOptions: {
                        level: 1,
                      },
                      globOptions: {
                        nomount: true,
                      },
                    },
                  },
                ],
                delete: ["zip"],
              },
            },
            runTasksInSeries: true,
          }),
          new BundleAnalyzerPlugin({
            analyzerMode: "server",
            analyzerHost: "127.0.0.1",
            analyzerPort: 8889,
            openAnalyzer: true, // 构建完打开浏览器
            reportFilename: path.resolve(__dirname, `analyzer/index.html`),
          }),
          new CompressionWebpackPlugin({
            test: /\.(js|ts|jsx|tsx|css|scss)$/, //匹配要压缩的文件
            algorithm: "gzip",
          }),
        );
        webpackConfig.optimization.minimizer = [
          new TerserPlugin({
            parallel: true, //开启并行压缩,可以加快构建速度
          }),
        ];
        // 生产环境关闭source-map
        webpackConfig.devtool = false;
        // 生产环境移除source-map-loader
        removeLoaders(webpackConfig, loaderByName("source-map-loader"));
      } else {
        addBeforeLoaders(webpackConfig, loaderByName("style-loader"), "thread-loader");
        addBeforeLoaders(webpackConfig, loaderByName("style-loader"), "cache-loader");
      }
      return webpackConfig;
    },
    plugins: [new WebpackBar(), new DllReferencePlugin({ manifest })],
  }),
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198

声明:掘金同名账号guxin_duyin是我本人,在掘金上发布此文章不是抄袭。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/360969
推荐阅读
相关标签
  

闽ICP备14008679号