# 优化webpack打包速度

  • 升级node/webpack/yarn/npm
  • 在尽可能少的模块上使用loader(比如配置exclude/include)
  • plugin尽可能少用并且确保可靠性
  • 控制包的大小
  • thread-loader,parallel-webpack,happypack多进程打包
  • sourcemap合理使用
  • 插件分环境使用
  • resolve配置项合理优化
  • HMR(开发环境使用) HMR
  • 模块解析loader利用oneOf,这样如果一个匹配上,就不会继续对比其他loader了
  • cache:babel eslint结果可以考虑缓存
  • 预加载Preload / Prefetch:
  • Tree Shaking:减少代码体积 Tree Shaking
  • babel: 减小体积 babel
  • Image Minimizer: 图片优化
  • 打包文件hash值缓存:runtimechunk
  • pwa优化:pwa
  • Code Split:
    • 代码拆分,分多个文件处理 entry
    • 提取重复代码,重复代码根据需求可以考虑单独打包,而不是在每个引用文件中写入。optimization
    • 按需加载:import动态引入
  • noParse:忽略某些较大的库
  • dllplugin:避免打包是对不变的库重复构建

# SourceMap

SourceMap(源代码映射)是一个用来生成源代码与构建后代码一一映射的文件的方案。

它会生成一个 xxx.map 文件,里面包含源代码和构建后代码每一行、每一列的映射关系。当构建后代码出错了,会通过 xxx.map 文件,从构建后代码出错位置找到映射后源代码出错位置,从而让浏览器提示源代码文件出错位置,帮助更快的找到错误根源。

开发模式:cheap-module-source-map

  • 优点:打包编译速度快,只包含行映射
  • 缺点:没有列映射
module.exports = {
  // 其他省略
  mode: "development",
  devtool: "cheap-module-source-map",
};

生产模式:source-map | none等选项,根据实际需求

  • 优点:包含行/列映射
  • 缺点:打包编译速度更慢
module.exports = {
  // 其他省略
  mode: "production",
  devtool: "source-map",
};

SourceMap (opens new window)

# resolve模块解析

在开发中我们会有各种各样的模块依赖,这些模块可能来自于自己编写的代码,也可能来自第三方库;resolve可以帮助webpack从每个 require/import 语句中,找到需要引入到合适的模块代码;webpack 使用 enhanced-resolve 来解析文件路径;

webpack能解析三种文件路径:

  1. 绝对路径

    • 由于已经获得文件的绝对路径,因此不需要再做进一步解析。
  2. 相对路径

    • 在这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录;
    • 在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径;
  3. 模块路径

    • 在 resolve.modules中指定的所有目录检索模块;
      • 默认值是 ['node_modules'],所以默认会从node_modules中查找文件;
    • 我们可以通过设置别名的方式来替换初识模块路径,具体后面讲解alias的配置;
  4. 如果是一个文件:

    • 如果文件具有扩展名,则直接打包文件;
    • 否则,将使用 resolve.extensions选项作为文件扩展名解析;
  5. 如果是一个文件夹:

    • 会在文件夹中根据 resolve.mainFiles配置选项中指定的文件顺序查找;
      • resolve.mainFiles的默认值是 ['index'];
      • 再根据 resolve.extensions来解析扩展名;
  6. extensions是解析到文件时自动添加扩展名:

    • 默认值是 ['.wasm', '.mjs', '.js', '.json'];
    • 所以如果我们代码中想要添加加载 .vue 或者 jsx 或者 ts 等文件时,我们必须自己写上扩展名;
  7. 另一个非常好用的功能是配置别名alias:

    • 特别是当我们项目的目录结构比较深的时候,或者一个文件的路径可能需要 ../../../这种路径片段;
    • 我们可以给某些常见的路径起一个别名;

resolve方便查找补全对应文件后缀

//webpack.config.js
	resolve: {
		extensions: ['.js', '.jsx'],//未加后缀时优先匹配js、jsx,有性能损耗,不能乱用
		mainFiles:['index','xxx'],//具体文件没写,可以配置mainFiles进行自动匹配
		alias: {
			child: path.resolve(__dirname, '../src/a/b/c/child')
		}//别名配置
	},
  • resolve.alias 假设我们有个 utils 模块极其常用,经常编写相对路径很麻烦,希望可以直接 import 'utils' 来引用,那么我们可以配置某个模块的别名,如:
alias: {
  utils: path.resolve(__dirname, 'src/utils') // 这里使用 path.resolve 和 __dirname 来获取绝对路径
}

上述的配置是模糊匹配,意味着只要模块路径中携带了 utils 就可以被替换掉,如:

import 'utils/query.js' // 等同于 import '[项目绝对路径]/src/utils/query.js'

如果需要进行精确匹配可以使用:

alias: {
  utils$: path.resolve(__dirname, 'src/utils') // 只会匹配 import 'utils'
}
  • resolve.modules 前面的内容有提到,对于直接声明依赖名的模块(如 react ),webpack 会类似 Node.js 一样进行路径搜索,搜索 node_modules 目录,这个目录就是使用 resolve.modules 字段进行配置的,默认就是:
resolve: {
  modules: ['node_modules'],
},

通常情况下,我们不会调整这个配置,但是如果可以确定项目内所有的第三方依赖模块都是在项目根目录下的 node_modules 中的话,那么可以在 node_modules 之前配置一个确定的绝对路径:

resolve: {
  modules: [
    path.resolve(__dirname, 'node_modules'), // 指定当前目录下的 node_modules 优先查找
    'node_modules', // 如果有一些类库是放在一些奇怪的地方的,你可以添加自定义的路径或者目录
  ],
},

这样配置在某种程度上可以简化模块的查找,提升构建速度。

  • resolve.mainFields 有 package.json 文件则按照文件中 main 字段的文件名来查找文件 我们之前有提到这么一句话,其实确切的情况并不是这样的,webpack 的 resolve.mainFields 配置可以进行调整。当引用的是一个模块或者一个目录时,会使用 package.json 文件的哪一个字段下指定的文件,默认的配置是这样的:
resolve: {
  // 配置 target === "web" 或者 target === "webworker" 时 mainFields 默认值是:
  mainFields: ['browser', 'module', 'main'],

  // target 的值为其他时,mainFields 默认值为:
  mainFields: ["module", "main"],
},

因为通常情况下,模块的 package 都不会声明 browser 或 module 字段,所以便是使用 main 了。

在 NPM packages 中,会有些 package 提供了两个实现,分别给浏览器和 Node.js 两个不同的运行时使用,这个时候就需要区分不同的实现入口在哪里。如果你有留意一些社区开源模块的 package.json 的话,你也许会发现 browser 或者 module 等字段的声明。

  • resolve.mainFiles 当目录下没有 package.json 文件时,我们说会默认使用目录下的 index.js 这个文件,其实这个也是可以配置的,是的,使用 resolve.mainFiles 字段,默认配置是:
resolve: {
  mainFiles: ['index'], // 你可以添加其他默认使用的文件名
},

通常情况下我们也无须修改这个配置,index.js 基本就是约定俗成的了。

  • resolve.resolveLoader 这个字段 resolve.resolveLoader 用于配置解析 loader 时的 resolve 配置,原本 resolve 的配置项在这个字段下基本都有。我们看下默认的配置:
resolve: {
  resolveLoader: {
    extensions: ['.js', '.json'],
    mainFields: ['loader', 'main'],
  },
},

这里提供的配置相对少用,我们一般遵从标准的使用方式,使用默认配置,然后把 loader 安装在项目根路径下的 node_modules 下就可以了。

# webapck Thead

可以开启多进程同时处理 js 文件,这样速度就比之前的单进程打包更快了.

需要注意:请仅在特别耗时的操作中使用,因为每个进程启动就有大约为 600ms 左右开销。如果简单内容打包,反而会时间耗费更多。

npm i thread-loader -D

eslint中 babel中 都可以配置多进程打包,不过开发环境中js

const os = require("os");
const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
// webpack5开箱自带,如果默认配置,不需要处理,如果重新自定义配置,最好重新安装
const TerserPlugin = require("terser-webpack-plugin");

// cpu核数
const threads = os.cpus().length;
module.exports = {
  module: {
    rules: [
      {
        oneOf: [    
          {
            test: /\.js$/,
            // exclude: /node_modules/, // 排除node_modules代码不编译
            include: path.resolve(__dirname, "../src"), // 也可以用包含
            use: [
              {
                loader: "thread-loader", // 开启多进程
                options: {
                  workers: threads, // 数量
                },
              },
              {
                loader: "babel-loader",
                options: {
                  cacheDirectory: true, // 开启babel编译缓存
                },
              },
            ],
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules", // 默认值
      cache: true, // 开启缓存
      // 缓存目录
      cacheLocation: path.resolve(
        __dirname,
        "../node_modules/.cache/.eslintcache"
      ),
      threads, // 开启多进程
    }),
    // 提取css成单独文件
    new MiniCssExtractPlugin({
      // 定义输出文件名和目录
      filename: "static/css/main.css",
    }),
    // css压缩
    // new CssMinimizerPlugin(),
	//  new TerserPlugin({
    //     parallel: threads // 开启多进程
    //   })
  ],
  // css压缩和TerserPlugin插件可以放在插件中处理,不过官方更推荐放在optimization中处理
  optimization: {
    minimize: true,
    minimizer: [
      // css压缩也可以写到optimization.minimizer里面,效果一样的
      new CssMinimizerPlugin(),
      // js压缩 当生产模式会默认开启TerserPlugin,但是需要进行其他配置,就要重新写了
	  // js开发环境没压缩,不需要处理这步
	  // 如果重新配置TerserPlugin,terser-webpack-plugin最好重新安装
      new TerserPlugin({
        parallel: threads // 开启多进程
      })
    ],
  },
  mode: "production",
  devtool: "source-map",
};

# new webpack.DllPlugin()

使用DllPlugin 提高打包速度

cnpm i add-asset-html-webpack-plugin --save

创建一个webpack.dll.js文件

const path = require('path');
const webpack = require('webpack');

module.exports = {
	mode: 'production',
	entry: {
		vendors: ['lodash'],//entry需要的文件都是已经安装在node_modules中了
		react: ['react', 'react-dom'],
		jquery: ['jquery']
	},
	output: {
		filename: '[name].dll.js',
		path: path.resolve(__dirname, '../dll'),
		library: '[name]'
	},
	plugins: [
		new webpack.DllPlugin({
			name: '[name]',
			path: path.resolve(__dirname, '../dll/[name].manifest.json'),
		})
	]
}

# webpack.DllReferencePlugin()

const path = require('path');
const fs = require('fs');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const webpack = require('webpack');

const plugins = [
	new HtmlWebpackPlugin({
		template: 'src/index.html'
	}), 
	new CleanWebpackPlugin(['dist'], {
		root: path.resolve(__dirname, '../')
	})
];

const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
files.forEach(file => {
	if(/.*\.dll.js/.test(file)) {
		plugins.push(new AddAssetHtmlWebpackPlugin({
			filepath: path.resolve(__dirname, '../dll', file)
		}))
	}
	if(/.*\.manifest.json/.test(file)) {
		plugins.push(new webpack.DllReferencePlugin({
			manifest: path.resolve(__dirname, '../dll', file)
		}))
	}
})

module.exports = {
	entry: {
		main: './src/index.js',
	},
	resolve: {
		extensions: ['.js', '.jsx'],
	},
	module: {
		rules: [{ 
			test: /\.jsx?$/, 
			include: path.resolve(__dirname, '../src'),
			use: [{
				loader: 'babel-loader'
			}]
		}, {
			test: /\.(jpg|png|gif)$/,
			use: {
				loader: 'url-loader',
				options: {
					name: '[name]_[hash].[ext]',
					outputPath: 'images/',
					limit: 10240
				}
			} 
		}, {
			test: /\.(eot|ttf|svg)$/,
			use: {
				loader: 'file-loader'
			} 
		}]
	},
	plugins,
	optimization: {
		runtimeChunk: {
			name: 'runtime'
		},
		usedExports: true,
		splitChunks: {
      chunks: 'all',
      cacheGroups: {
      	vendors: {
      		test: /[\\/]node_modules[\\/]/,
      		priority: -10,
      		name: 'vendors',
      	}
      }
    }
	},
	performance: false,
	output: {
		path: path.resolve(__dirname, '../dist')
	}
}

package.json

"scripts": {
    "dev-build": "webpack --config ./build/webpack.dev.js",
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js",
    "build:dll": "webpack --config ./build/webpack.dll.js"
  },

让第三方模块只打包一次,以后(不换版本的话)可以不需要再度打包,提高webpack打包速度

# webapck Cache

每次打包时 js 文件都要经过 Eslint 检查 和 Babel 编译,速度比较慢。

可以缓存之前的 Eslint 检查 和 Babel 编译结果,这样第二次打包时速度就会更快了。

const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
module.exports = {
  entry: "./src/main.js",
  output: {
    path: undefined, // 开发模式没有输出,不需要指定输出目录
    filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
    // clean: true, // 开发模式没有输出,不需要清空输出结果
  },
  module: {
    rules: [
      {
        oneOf: [
          {
            test: /\.js$/,
            // exclude: /node_modules/, // 排除node_modules代码不编译
            include: path.resolve(__dirname, "../src"), // 也可以用包含
            loader: "babel-loader",
            options: {
              cacheDirectory: true, // 开启babel编译缓存 默认false
              cacheCompression: false, // 缓存文件不要压缩,压缩也要耗时,不压缩只是占电脑空间大些
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules", // 默认值
      cache: true, // 开启缓存
      // 缓存目录
      cacheLocation: path.resolve(
        __dirname,
        "../node_modules/.cache/.eslintcache"
      ),
    })
  ],
  mode: "development"
};

# noParse

webpack noParse作用主要是过滤不需要解析的文件,比如打包的时候依赖了三方库(jquyer、lodash)等,而这些三方库里面没有其他依赖,可以通过配置noParse不去解析文件,提高打包效率

module.exports = {
    // 模块处理
    module: {
        noParse: '/jquery|lodash/',  // 不去解析三方库
        // loader
        rules: []
    }
}

# 4 个角度对 webpack 和代码进行了优化:

  1. 提升开发体验
  • 使用 Source Map 让开发或上线时代码报错能有更加准确的错误提示。
  1. 提升 webpack 提升打包构建速度
  • 使用 HotModuleReplacement 让开发时只重新编译打包更新变化了的代码,不变的代码使用缓存,从而使更新速度更快。
  • 使用 OneOf 让资源文件一旦被某个 loader 处理了,就不会继续遍历了,打包速度更快。
  • 使用 Include/Exclude 排除或只检测某些文件,处理的文件更少,速度更快。
  • 使用 Cache 对 eslint 和 babel 处理的结果进行缓存,让第二次打包速度更快。
  • 使用 Thead 多进程处理 eslint 和 babel 任务,速度更快。(需要注意的是,进程启动通信都有开销的,要在比较多代码处理时使用才有效果)
  1. 减少代码体积
  • 使用 Tree Shaking 剔除了没有使用的多余代码,让代码体积更小。
  • 使用 @babel/plugin-transform-runtime 插件对 babel 进行处理,让辅助代码从中引入,而不是每个文件都生成辅助代码,从而体积更小。
  • 使用 Image Minimizer 对项目中图片进行压缩,体积更小,请求速度更快。(需要注意的是,如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。)
  1. 优化代码运行性能
  • 使用 Code Split 对代码进行分割成多个 js 文件,从而使单个文件体积更小,并行加载 js 速度更快。并通过 import 动态导入语法进行按需加载,从而达到需要使用时才加载该资源,不用时不加载资源。
  • 使用 Preload / Prefetch 对代码进行提前加载,等未来需要使用时就能直接使用,从而用户体验更好。
  • 使用 Network Cache 能对输出资源文件进行更好的命名,将来好做缓存,从而用户体验更好。
  • 使用 Core-js 对 js 进行兼容性处理,让我们代码能运行在低版本浏览器。
  • 使用 PWA 能让代码离线也能访问,从而提升用户体验。
最后更新: 5/1/2023, 4:47:21 PM