抛开脚手架,徒手搭建 react 项目,webpack打包优化篇(五)

417 阅读9分钟

[《抛开脚手架,徒手搭建 react 项目(一)》](juejin.cn/post/739816… ")我们用webpack搭建了一个react项目,引入了typescript,用Babel编译,还用webpack-merge拆分了我们的配置文件。

《抛开脚手架,徒手搭建 react 项目(二)》我们在项目里面引入了eslint,还介绍了打开服务的五种方法。尤其重要的是 nginx,要你在本地体验一把线上奔跑的感觉。

《抛开脚手架,徒手搭建 react 项目(三)》我们在项目里面引入了 stylelint、还有husky相关工具,轻松实现git commit的时候,对代码格式和提交信息做具体的检测。

《抛开脚手架,徒手搭建 react 项目,webpack打包优化篇(四)》亲手测试并整理了六点减少打包时间的手段。

抛开脚手架,徒手搭建 react 项目,webpack打包优化篇(五) 亲手测试并整理了六点能够缩减打包体积的手段

今天来接上篇,继续优化webpack,上篇已经道明,优化webpack的手段分为两个途径:缩短打包时间和减小打包体积。上篇我们用:缩减打包范围,插件loader的缓存,定向搜索,分离第三方包,增加多线程,添加查看打包速度和打包体积的方法对webpack做了打包时间的优化。

总结为:定向文件,提前打包,专业分析,多手工作。

现在继续篇未完成部分:减少打包体积

一.分割代码

分割各个模块代码,提取相同部分代码,好处是减少重复代码的出现频率

代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后便能按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle、控制资源加载优先级,如果使用合理,会极大减小加载时间。

常用的代码分离方法有三种:

入口起点:使用 entry 配置手动地分离代码。

防止重复:使用 入口依赖 或者 SplitChunksPlugin 去重和分离 chunk。

动态导入:通过模块的内联函数调用分离代码。

1.多入口打包

尽管 Webpack 允许在配置中使用多个入口起点,但在可能的情况下,最好使用具有多import的单入口打包,这样才能获得更好的优化效果,并确保异步脚本标签的执行顺序一致。

Webpack文档不推荐多入口的原因有如下:

  • 每个入口起点都会生成一个输出文件,这意味着如果你使用多个入口起点,就会生成多个输出文件。这种情况下,每个输出文件都需要加载并执行,会增加额外的网络请求和加载时间。
  • 相比之下,使用多导入单入口作为起点,可以将所有的代码逻辑打包到一个输出文件中,从而减少了网络请求和加载时间。这样可以提高应用程序的性能和加载速度。
  • 如果多入口,单出口也会引入重复引用的问题。

1.配置

 const path = require('path');

 module.exports = {
   mode: 'development',
   entry: {
     index: {
       import: './src/index.js',
       dependOn: 'shared',
     },
     another: {
       import: './src/another-module.js',
       dependOn: 'shared',
     },
     shared: 'lodash',
   },
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
  optimization: {
    runtimeChunk: 'single',
  },
 };

image.png

2.测试

image.png

index.js:包含 ./src/index.js 的代码。
another.js:包含 ./src/another-module.js
的代码。 shared.js:包含 lodash 的代码。
runtime.js:包含 Webpack 运行时代码。

不推荐就不用呗,别没事给自己找事。反正现在我们的页面是SPA页面,是单入口多导入的,非常契合webpack的宗旨。

2.代码分隔

webpack的代码分隔是通过它的内置插件SplitChunksPlugin实现的,我们不用引入这个插件,直接在优化项里面配置splitchunks就好了!

1.配置

  optimization: {
    runtimeChunk: { name: "manifest" }, // 抽离WebpackRuntime函数
    splitChunks: {
      cacheGroups: {
        common: {
          minChunks: 2, // 代码块出现最少次数
          name: "common", // 代码块名称
          priority: 5, // 优先级别
          reuseExistingChunk: true, // 重用已存在代码块
          test: AbsPath("src")
        },
        vendor: {
          chunks: "initial", // 代码分割类型
          name: "vendor",
          priority: 10,
          test: /node_modules/
        }
      }, // 缓存组
      chunks: "all" // 代码分割类型:all全部模块,async异步模块,initial入口模块
    }
  },

代码分隔的时候,一定要在output里面配置动态输出,不然就会报错

image.png

2.测试

image.png

其实不管代码怎么分隔,怎么打包,都是为了浏览器能够快速的加载渲染,根本就不影响页面上显示出来的结果。

3.react项目热更新

配置webpack.config.js

const { HotModuleReplacementPlugin } = require("webpack");

.....
 plugins: [
    !isDev && new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './index.html', // 指定一个HTML模板文件
      filename: './index.html', // 输出的HTML文件名,默认是index.html
      inject: true, // 允许插件修改哪些内容,true将脚本添加到body元素的末尾
      cache: true,
    }),
    new StylelintPlugin({
      files: ["src/**/*.{scss,css,less}"], // 指定要检查的文件
      fix: true, // 是否自动修复一些样式错误
      cache: true
    }),
    new EslintPlugin({ cache: true }),
    new ProgressBarPlugin({ complete: '█', }),
    new HotModuleReplacementPlugin(),
  ],

react项目还要在引入module.hot才能用,所以说webpack的热更新并不是一个开箱即用的模块,而是你的手动引入进来才行。不过现在好了,有个插件可以帮我做这些事情

安装命令

npm i @pmmmwh/react-refresh-webpack-plugin react-refresh -D

配置webpack。config。js

const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = merge(baseConfig, {
  // ...
  plugins: [
    new ReactRefreshWebpackPlugin(), // 添加热更新插件
  ]
})

babel-loader配置react-refesh刷新插件,修改babel.config.js文件

image.png

热更新的时候,webpack,webpack-cli,webpack-dev-server的版本至关重要,千万不要乱下版本,不然你就芭比Q了,你有解决不完的bug

具体可以参照这篇文章:juejin.cn/post/735356…

二.tree-shaking处理js

Tree Shaking的意思就是摇树,伴随着摇树这个动作,树上的枯叶都会被摇晃下来

tree-shaking意思就是在webpack打包过程中,它会生成一颗依赖树,在这颗依赖树上会明确的标注清楚,哪些文件export了,但是没有import,最后在生成target文件的时候,就把哪些没有import的文件干掉。

在webpack5里面,只要我们的mode:production,它就会自动trr-shaking

1. 开发环境下的配置

// webpack.config.js
module.exports = {
  // ...
  mode: 'development',
  optimization: {
    usedExports: true,
  }
};

2.生产环境下的配置


// webpack.config.js
module.exports = {
  // ...
  mode: 'production',
};

测试

image.png

三.动态垫片

在webapck打包的过程中,我们往往需要对代码做兼容处理。比如我们写的代码使用了async await,有些浏览器就不支持这个关键字,怎么办?就需要@babel/polyfill出马,把他编译成浏览器能看懂的代码。但是问题是浏览器不一样,支持的属性也不一样,所以@babel/polyfill有个配置项是:useBuiltIns,

  •  false:无视target.browsers将所有Polyfill加载进来
  •  entry:根据target.browsers将部分Polyfill加载进来(仅引入有浏览器不支持的Polyfill,需在入口文件import "core-js/stable")
  •  usage:根据target.browsers与检测代码中ES6的使用情况将部分Polyfill加载进来(无需在入口文件import "core-js/stable")

使用html-webpack-tags-plugin在打包时能够,自动加入动态垫片,可根据浏览器UserAgent返回当前浏览器Polyfill

具体配置如下:

import HtmlTagsPlugin from "html-webpack-tags-plugin";

export default {
	// ...
	plugins: [
		// ...
		new HtmlTagsPlugin({
			append: false, // 在生成资源后加入
			publicPath: false, // 使用公共路径
			tags: ["https://polyfill.alicdn.com/polyfill.min.js"] // 资源路径
		})
	]
};

四.按需加载

按需加载:是页面加载成功以后,并不着急加载这些文件,而是等用到了再去加载文件。最大的好处是减轻首屏渲染的负担。因为项目功能越多其打包体积越大,导致首屏渲染速度越来越慢。

动态引入的都要重启下生效!

动态导入react的组件用React.lazy(()=>import('./User'));

动态导入函数直接用import('./utils/math')

1.配置

如果直接用import()加载组件是渲染不出来的,比如下图

image.png

2.测试

包打出来,启动看看

并没有渲染出我的组件,为什么?因为import是异步的

image.png

修改成React.lazy(()=>import('./user'))

image.png

打包执行还是报错了

image.png

翻了很久才找到解决方案,原来是babel编译问题,加上下面这个,打包就好了

image.png

如果你的动态导入还在报错就尝试一下:@babel/plugin-syntax-dynamic-import

五.作用提升

作用提升最早出现的rollup上,是rollup的核心概念。它的的作用是分析模块之间的关系,然后把打包好的模块全部合并到一个函数里面。

在webpack打包过程中,将大部分函数都打包成一个一个的IIFE,就是自执行函数。这样会使代码的体积不断增大,导致占用的内存也越来越多。

有了作用提升后,构建好的代码,会根据引用的顺序存放到一个函数作用域里面,通过各种手段防止变量名冲突,减少函数声明,和内存花销。

webpack中只需将打包环境设置为生产环境就能让作用提升生效,或显示设置concatenateModules

export default {
	// ...
	mode: "production"

	// ...
	optimization: {
		// ...
		concatenateModules: true
	}
};

六.压缩资源

前提条件是:浏览器本就支持 gzip压缩包,所以你打包后的文件压缩以后,传递给浏览器,浏览器会自动解压,然后执行。

压缩HTML/CSS/JS代码,字体/图像/音频/视频 的 好处是更有效减少打包体积

1.压缩html文件

压缩方式:

  1. 移除 HTML 文件中的注释。
  2. 压缩 HTML 文件,例如删除空格和换行符。

有些博文说需要配置: html-webpack-plugin,不过我发现只要mode:production它就会自动压缩,mode:development就不会压缩测试看看

image.png

2.压缩css文件

如果没有具体的插件处理css文件,那么打包以后,css文件是包含在js文件里面,要想压缩,那么第一步就是先把css文件分离出来,使用mini-css-extract-plugin插件。

安装

npm install --save-dev mini-css-extract-plugin

配置

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 
module.exports = {
  // ...
  module: {
    rules: [
        {
        test: /.less$/,
        use: [
          isDev ? "style-loader" : MiniCssExtractPlugin.loader,
          "css-loader",
          'postcss-loader',
          "less-loader"
        ]
      }
      // ... 其他规则
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'index.css'
    }),
    // ... 其他插件
  ]
  // ...
};

测试看看

image.png

为了压缩输出文件,请使用类似于 css-minimizer-webpack-plugin 这样的插件。 安装

npm i  css-minimizer-webpack-plugin -D

配置

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
      chunkFilename: "[id].css",
    }),
  ],
  module: {
    rules: [
      {
        test: /.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  optimization: {
    minimizer: [
      // For webpack@5 you can use the `...` syntax to extend existing minimizers (i.e. `terser-webpack-plugin`), uncomment the next line
      // `...`,
      new CssMinimizerPlugin(),
    ],
  },
};

测试

image.png

3.压缩js代码

具体用哪个取决于你webpack的版本,但是我发现只要mode:production,webpack就会对代码做压缩,即使我不配置 terser-webpack-plugin

测试如下:

image.png

我们安装下terser-webpack-plugin,看他在mode:development下面的打包结果是什么

npm install terser-webpack-plugin --save-dev

配置

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin()],
  },
};

测试结果:

image.png

就说明只要是mode:production它就会对js代码做压缩,我们就不要再安装压缩工具了。

总结:

webpack有12个优化方向,要么是缩减打包时间,要么是缩减打包体积。缩减打包时间的目的是要我们能够快速的打包,提高开发效率。缩减打包体积是节省浏览器加载资源的时间,提升页面的性能。

image.png