案例引入

  • 基于 Module Federation 的一些微前端方案
  • 基于 webpack-dev-server 的一些 热更新
  • 基于 tree-Shaking, code spliting, terser / minify / uglify 的一些压缩方案
  • lazy import 的一些懒加载方案
  • Js, Json,二进制的解析、生成能力

整体流程

bundle: 由 webpack 打包出来的文件
chunk: 指 webpack 在进行模块依赖分析的时候,代码分割出来的代码块
module: 开发中的单个模块

输入

从文件系统中读取代码

  • entry: 用于定义项目的入口,Webpack 会从这些入口文件中找到所有文件 (SFC 单文件组件)
  • context: 定义项目的执行上下文

模块递归处理

调用 Loader 转译 Module 内容,并将结果转换为 AST,从中分析出模块依赖关系,进一步递归调用模块处理过程,直到所有依赖文件都处理完毕;

  • resolve: 用于配置模块路径的解析规则,可以帮助 webpack 更精确,更高效的找到指定模块;
  • module: 配置模块加载规则,loaders
  • externals: 声明资源之间的关系。用 externals 声明的外部资源, webpack 会进行忽略。
  • chunk: 代码块,一个 chunk 由多个模块组合而成,用于代码合并与分割

后处理

所有模块递归处理完毕后开始执行后处理,包括模块合并、注入运行时、产物优化等,最终输出 chunk 集合

  • optimization: 用于控制如何优化产物包体积,scope hoisting、code spliting、代码混淆、代码压缩
  • target: 用于配置编译产物的目标环境, web,、node、electron
  • mode: dev prod 环境下的声明
  • output: 输出,产出物

额外的开发效率工具

  • devtools 决定 sourcemap 的生成规则
  • DevServer HMR: 内置 node 服务,websocket 通知,再根据新 hash 值请求更新内容
  • watch 模式,用于配置持续监听文件变化
  • cache webpack 5 以后,缓存编译信息

异步加载

原理

  1. 请求资源时,先通过 jsonp 的方式去加载 js 模块所对应的文件,会保存在一个全局的 webpackJsonp 中
  2. 加载回来后在浏览器中执行此 JS 脚本,将请求过来的模块定义合并到 main.js 中的 modules 中去: webpackJsonp.push 的的值,两个参数分别为异步加载的文件中存放的需要安装的模块对应的 id 和异步加载的文件中存放的需要安装的模块列表。
  3. 合并完后,去加载这个模块
  4. 拿到该模块导出的内容

优势

  1. 代码分割(Code Splitting): Webpack 允许将代码拆分为多个块(chunks),并在需要时动态加载这些块。意味着可以将应用程序划分为更小的模块,只在需要时加载,而不是一次性加载整个应用程序。这可以减少初始加载时间,提高性能。
  2. 动态导入语法: Webpack 提供了动态导入语法,例如使用 import() 函数或 require.ensure() 函数来异步加载模块。这些函数返回一个 Promise,可以使用 then 方法处理加载成功的回调,或使用 catch 方法处理加载失败的回调。
  3. 按需加载: 通过异步加载模块,可以根据需要加载特定的模块,而不是将所有模块打包到同一个文件中。这样可以减少初始加载时间,并在用户需要时动态加载额外的模块。
  4. 代码并行加载: Webpack 可以同时加载多个模块,利用浏览器的并行加载能力,从而加快加载速度。这对于大型应用程序和复杂的依赖关系特别有用。

loaders

webpack 某个阶段的解析器,对不同文件进行解析
Webpack 选择了 compose 方式,函数组合是函数式编程中非常重要的思想。在 compose 中是采用 reduceRight,从右往左执行

babel loader

css loaders

原生 webpack 不能识别 css 语法,直接导入 .css 文件会失败,为此, 在 webpack 中,处理 css 文件,通常要使用到

css-loader

将 css 翻译成类似 module.exports = ${css} 的 JS 代码,使 CSS 文件可以和 JS 一样作为资源。同时可以提供 sourcemap、css-in-module

style-loader

在具体的产物中,注入 runtime 代码。让这些代码将 CSS 注入到页面中。

less/sass-loader

通过原本的 less / sass 的解析器解析,最后生成 css.

postcss-loader

CSS 界的 babel.

file loaders

file-loader

经过 file-loader 处理后,原始图片会被重命名并复制到产物文件夹,同时在代码里插入图片 URL

url-loader

对于小于阈值的图像,直接 base 64 编码

raw-loader

都不处理,直接拷贝,一般 svg 会用他。

手写 loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// less-loader
const less = require("less");

module.exports = function loader(source) {
const callback = this.async();
less.render(source, { sourceMap: {} }, function (err, res) {
let { css, map } = res;
callback(null, css, map);
});
};

// style-loader
module.exports = function loader(source, map) {
let style = `
const style = document.createElement('style');
style.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(style);
`;

return style;
};

plugins

在打包过程中不同周期内加入一些功能

常见的 plugins

  • terser-webpack-plugin 压缩 js
  • pnp-webpack-plugin Yarn Plug Play 插件
  • html-webpack-plugin 自动生成带有入口文件的 index.html 模板注入
  • webpack-manifest-plugin 生产资产的显示清单
  • mini-css-extract-plugin
  • define-plugin
  • friendly-errors-webpack-plugin 友好的错误日志。

webpack 运行原理

  1. 初始化参数: 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译: 用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法,初始化一个 compilation 对象,执行 compilation 中的 build 方法开始执行编译,触发 compiler 对象的 done 钩子,完成编译
  3. 确定入口: 根据配置中的 entry 找出所有的入口文件
  4. 编译模块: 从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译: 在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  6. 输出资源: 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成: 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

compiler / compilation

compiler

该对象包含了 webpack 的所有配置信息,包括 entry、output、module、plugins 等,compiler 对象会在启动 webpack 时,一次性地初始化创建,它是全局唯一的,可以简单理解为 webpack 的实例

compilation

该对象代表一次资源的构建,通过一系列 API 可以访问/修改本次模块资源、编译生成的资源、变化的文件、以及被跟踪依赖的状态信息等,当我们以开发模式运行 webpack 时,每当检测到一个文件变化,就会创建一个新的 compilation 对象,所以 compilation 对象也是一次性的,只能用于当前的编译

  • compilation.modules: 解析后的所有模块
  • compilation.chunks: 所有的代码分块 chunk
  • compilation.assets: 本次打包生成的所有文件
  • compilation.hookscompilation: 所有的钩子

总结: compiler 代表的是整个 webpack 从启动到关闭的生命周期(终端结束,该生命周期结束),而 compilation 只是代表了一次性的编译过程,如果是 watch 模式,每次监听到文件变化,都会产生一个新的 compilation,所以 compilation 代表一次资源的构建,会多次被创建,而 compiler 只会被创建一次

插件实现原理

通过 tapable 链式调用
通过 compiler 对象的 hooks 属性访问到 emit 钩子,再通过 tap 方法注册一个钩子函数,这个方法接收两个参数: 插件名称,callback(compilation: 上下文,其中 assets 属性为即将写入输出目录的资源文件信息, for in 去遍历 assets ,得到键名,即每个文件的名称,然后通过遍历的值对象中的 source 方法获取文件内容,处理过后暴露一个 source 方法用来返回新的内容。另外还需要再暴露一个 size 方法,用来返回内容大小)

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
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesBailHook,
AsyncSeriesHook,
AsyncSeriesLoopHook,
AsyncSeriesWaterfallHook,
Hook,
} = require("tapable");

const hook = new SyncHook(["arg1", "arg2", "arg3"]);

// 注册事件
hook.tap("flag1", (arg1, arg2, arg3) => {
console.log("flag1:::", arg1, arg2, arg3);
});

hook.tap("flag2", (arg1, arg2, arg3) => {
console.log("flag2:::", arg1, arg2, arg3);
});

setTimeout(() => {
hook.call("a", "b", "c");
}, 1000);

const webpack = {
compiler: {
hooks: {
make: new SyncHook(),
seal: new SyncHook(),
compile: new SyncHook(),

emit: new SyncHook(),
afterEmit: new SyncHook(),
},
},

compilation: {
hooks: {},
},
};

compiler.hooks.emit.tap("flag1", (stats, callback) => {
console.log("flag1:::", stats, callback);
});

class APlugin {
apply(compiler) {
compiler.hooks.done.tap("APlugin", () => {});
}
}

tree-shaking

原理

依赖标记阶段

Make 阶段: 收集模块导出变量并记录到模块依赖关系图 ModuleGraph 变量中

  1. 将模块的所有 ESM 导出语句转换为 Dependency 对象,并记录到 module 对象的 dependencies 集合,转换规则: - 具名导出转换为 HarmonyExportSpecifierDependency 对象 - default 导出转换为 HarmonyExportExpressionDependency 对象
  2. 所有模块都编译完毕后,触发 compilation.hooks.finishModules 钩⼦,开始执行 FlagDependencyExportsPlugin 插件回调
  3. FlagDependencyExportsPlugin 插件从 entry 开始读取 ModuleGraph 中存储的模块信息,遍历所有 module 对象
  4. 遍历 module 对象的 dependencies 数组,找到所有 HarmonyExportXXXDependency 类型的依赖对象,将其转换为 ExportInfo 对象,并记录到 ModuleGraph 体系中

Seal 阶段: 遍历 ModuleGraph 标记模块导出变量有没有被使⽤

  1. 触发 compilation.hooks.optimizeDependencies 钩⼦,开始执⾏ FlagDependencyUsagePlugin 插件逻辑
  2. 在 FlagDependencyUsagePlugin 插件中,从 entry 开始逐步遍历 ModuleGraph 存储的所有 module 对象
  3. 遍历 module 对象对应的 exportInfo 数组
  4. .为每⼀个 exportInfo 对象执⾏ compilation.getDependencyReferencedExports ⽅法,确定其对应的 dependency 对象有否被其它模块使⽤
  5. 被任意模块使⽤到的导出值,调⽤ exportInfo.setUsedConditionally ⽅法将其标记为已被使⽤
  6. exportInfo.setUsedConditionally 内部修改 exportInfo._usedInRuntime 属性,记录该导出被如何使⽤
  7. 结束

产物生成: 若变量没有被其他模块使用时,则删除对应的导出语句

  1. 打包阶段,调⽤ HarmonyExportXXXDependency.Template.apply ⽅法⽣成代码
  2. 在 apply ⽅法内,读取 ModuleGraph 中存储的 exportsInfo 信息,判断哪些导出值被使⽤,哪些未被使⽤
  3. 对已经被使⽤及未被使⽤的导出值,分别创建对应的 HarmonyExportInitFragment 对象,保存到 initFragments 数组
  4. 遍历 initFragments 数组,⽣成最终结果 (__webpack_exports__对象)

标记阶段会将 import & export 标记为 3 类

  • 所有 import 标记为 /* harmony import */
  • 被使用过的 export 标记为 /* harm export([type])*/ ,其中 [type] 和 webpack 内部相关,可能是 binding,immutable 等等
  • 未被使用过的 import 标记为 /* unused harmony export [FuncName] */,其中 [FuncName] 为 export 的方法名称

删除阶段

使用 Terser 删除没有用到的导出语句 (dead code)

原理总结

  • 在 FlagDependencyExportsPlugin 插件中根据模块的 dependencies 列表收集模块导出值,并记录到 ModuleGraph 体系的 exportsInfo 中
  • 在 FlagDependencyUsagePlugin 插件中收集模块的导出值的使用情况,并记录到 exportInfo._usedInRuntime 集合中
  • 在 HarmonyExportXXXDependency.Template.apply 方法中根据导出值的使用情况生成不同的导出语句
  • 使用 DCE 工具删除 Dead Code,实现完整的树摇效果

启用 tree-shaking

需满足三个条件

  1. 使用 ESM 规范编写模块,在引入模块时应局部引入,才可以触发 tree shaking 机制
1
2
3
4
5
6
7
8
// 导入所有内容(不会触发 tree-shaking)
import lodash from "lodash";

// 导入命名导出 (会触发 tree-shaking)
import { debounce } from "lodash";

// 直接导入项目 (会触发 tree-shaking)
import debounce from "lodash/lib/debounce";
  1. 配置 optimization.usedExport 为 true,启用标记功能,标记代码无副作用。在 package.json 中配置 sideEffects: false,可以安全删除

  2. 启用代码优化功能,有以下途径

  • 配置 mode = production
  • 配置 optimization.minimize = true
  • 提供 optimization.minimizer 数组
1
2
3
4
5
6
7
8
module.exports={
entry: "./src/index",
mode: "production",
devtool:false
optimization: {
usedExports:true
},
};

最佳实践

  1. 避免无意义的赋值: Tree Shaking 逻辑停留在代码静态分析层面,只是浅显地判断:
  • 模块导出变量是否被其它模块引用
  • 引用模块的主体代码中有没有出现这个变量

无法从语义上分析模块导出值是不是真的被有效使用,导致出现一些无意义复制被保留的情况

  1. 使用 #pure 标注纯函数调用
1
2
3
4
5
6
const fun = (str) => {
console.log(str);
};

fun("jack"); // 会保留
/*#__pure__*/ fun("lucy"); // 不会保留
  1. 禁止 Babel 转译模块导入导出语句: 避免 ESM 语句转译成 CommonJS 语句
  2. 优化导出值的粒度: 尽量明确导出对象最小粒度
  3. 使用支持 Tree Shaking 的包

source-map

Source Map 是一个信息文件,存储了代码打包转换后的位置信息,实质是一个 json ,维护了打包前后的代码映射关系

生成 source-map

UglifyJS

1
uglifyjs app.js - o app.min.js--source - map app.min.js.map

Grunt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
grunt.initConfig({
concat: {
options: {
sourceMap: true,
},
},
uglify: {
options: {
sourceMap: true,
sourceMapIn: function (uglifySource) {
return uglifySource + ".map";
},
},
},
});

Gulp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var gulp = require("gulp");
var plugin1 = require("gulp-plugin1");
var plugin2 = require("gulp-plugin2");
var sourcemaps = require("gulp-sourcemaps");

gulp.task("javascript", function () {
gulp
.src("src/**/*.js")
.pipe(sourcemaps.init())
.pipe(plugin1())
.pipe(plugin2())
.pipe(sourcemaps.write("../maps"))
.pipe(gulp.dest("dist"));
});

SystemJS

1
2
3
4
builder.bundle("myModule.js", "outfile.js", {
minify: true,
sourceMaps: true,
});

Webpack

1
devtool: "source-map";

Webpack 中的 Source Map

配置 devtool 的不同取值使用不同的 source map 策略

  • 内联: 构建速度更快
  • 外部: 会生成 .map 文件
  • source-map: 外部。可以查看错误代码准确信息和源代码的错误位置
  • inline-source-map: 内联。只生成一个内联 Source Map,可以查看错误代码准确信息和源代码的错误位置
  • hidden-source-map: 外部。可以查看错误代码准确信息,但不能追踪源代码错误,只能提示到构建后代码的错误位置
  • eval-source-map: 内联。每一个文件都生成对应的 Source Map,都在 eval 中,可以查看错误代码准确信息和源代码的错误位置
  • nosources-source-map: 外部。可以查看错误代码错误原因,但不能查看错误代码准确信息,并且没有任何源代码信息
  • cheap-source-map: 外部。可以查看错误代码准确信息和源代码的错误位置,只能把错误精确到整行,忽略列
  • cheap-module-source-map: 外部。可以错误代码准确信息和源代码的错误位置,module 会加入 loader 的 Source Map

使用总结

  1. 开发环境: 需要考虑速度快,调试更友好。综合速度 (eval > inline > cheap) 和调试 (souce-map),有两种方案
  • eval-source-map: 完整度高,内联速度快
  • eval-cheap-module-souce-map: 错误提示忽略列但是包含其他信息,内联速度快
  1. 生产环境: 需要考虑源代码要不要隐藏,调试要不要更友好 (内联会让代码体积变大,生产环境不用内联),有两种方案
  • nosources-source-map: 全部隐藏 (打包后的代码与源代码)
  • hidden-source-map: 只隐藏源代码,会提示构建后代码错误信息

综合选择

  • dev: source-map (最完整)
  • prod: cheap-module-souce-map (错误提示一整行忽略列)

性能优化

减少打包体积

  1. 只打包需要的模块: 使用 tree-shaking
  2. 代码压缩:
  • 使用 css-minimizer-webpack-plugin: 压缩和去重 CSS
  • terser-webpack-plugin: 压缩和去重 JavaScript,或其他 UglifyJS 插件
  • source-map: 在开发模式下生成更准确 (但更大) 的 source-map;在生产模式下生成更小 (但不那么准确) 的 source-map
  • webpack-bundle-analyzer: 查看打包后的体积,后续优化
1
2
3
4
5
6
7
8
9
module.exports = {
// 开发模式
mode: "development",
devtool: "eval-cheap-module-source-map",

// 生产模式
mode: "production",
devtool: "nosources-source-map",
};

使用缓存

  1. 使用缓存提高打包速度: 避免每次修改代码后重新构建和打包,可使用 cache-loader 、 hard-source-webpack-plugin 、HotModuleReplacementPlugin 等插件缓存打包结果
  2. 使用浏览器缓存加快页面速度: 避免重复访问服务器。可以通过在 Webpack 中设置 output.chunkFilename 和 output.filename 来控制静态资源的命名规则,并设置 max-age 等缓存策略
1
2
3
4
5
6
7
8
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[hash:8].js',
chunkFilename: '[name].[hash:8].js'
},
...
}

优化图片加载

  1. 使用 file-loader 和 url-loader 进行图片处理: file-loader 会将图片打包后生成一个 url,url-loader (或者 webpack5 的 assets-moudle) 会根据图片大小来决定是否将图片转为 base64 编码
1
2
3
4
5
6
7
8
9
10
11
12
13
{
test: /\.(png|jpe?g|gif|svg|webp)$/,
type: 'asset',
parser: {
// Conditions for converting to base64
dataUrlCondition: {
maxSize: 25 * 1024, // 25kb
}
},
generator: {
filename: 'images/[contenthash][ext][query]',
},
},
  1. 使用图片压缩工具压缩: tinyPng、Gzip (需后端配合) 等
1
2
3
4
5
6
7
8
9
10
const CompressionPlugin = require("compression-webpack-plugin");

plugins: [
// gzip
new CompressionPlugin({
algorithm: "gzip",
threshold: 10240,
minRatio: 0.8,
}),
];

代码分割和加载

  1. 使用 SplitChunksPlugin 插件进行代码分割: 可以将公共的依赖模块抽取成 chunk,并且将多个 chunk 之间的重复依赖提取成单独的 chunk
1
2
3
4
5
6
7
8
module.exports = {
optimization: {
splitChunks: {
chunks: "all"
}
},
...
}
  1. 按需加载: 资源动态加载,使用 React.lazy() 函数或者 import() 语法,可以实现组件的按需加载

CDN 加速

CDN: 内容分发网络,通过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源,从而加速资源的获取速度。 CDN 其实是通过优化物理链路层传输过程中的网速有限、丢包等问题来提升网速的,其大致原理可以如下: 因为 CDN 都有缓存,所以为了避免 CDN 缓存导致用户加载到老版本的问题,需要遵循以下规则:

  • 针对 HTML 文件: 不开启任何缓存,不放入 CDN
  • 针对静态 JS 、CSS 、图片等文件: 开启 CDN 和缓存,放入 CDN 服务器,并且给每一个文件名带入 Hash 值,避免文件重名导致访问到同名缓存废弃文件的问题
  • 介于浏览器对同一时刻、同一域名的请求个数有限制的状况,请求资源过多的话,可能导致加载文件被阻塞。所以,当同一时间加载资源过多时,我们可以针对不同的文件类型放入不同的 CDN 上
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
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
...
// 1. 在 output 中设置 publicPath
output: {
publicPath: 'http://cdn.example.com/'
},

// 2. 配置 externals,将一些第三方库从打包文件中抽离出来,以便于在 HTML 文件中引入 CDN 资源。
// 当使用 externals 时,需要在页面中手动引入对应的 CDN 资源
externals: {
jquery: 'https://cdn.example.com/jquery.min.js'
},

// 3. 使用插件 html-webpack-plugin,可以在打包后的 HTML 文件中自动插入对应的 CDN 资源链接
// 4. 使用 copy-webpack-plugin,若存在一些与打包无关的静态资源,此插件将其从源码目录复制到
// 打包后的目录中,同时修改 HTML 文件中的引用路径为 CDN 地址
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: './src/index.html',
minify: true,
publicPath: 'https://cdn.example.com/'
}),
new CopyWebpackPlugin({
patterns: [
{
from: 'src/assets',
to: 'assets',
transformPath(targetPath) {
return `${PUBLIC_PATH}${targetPath}`;
}
}
]
})
]
...
}

// 5. 设置 Cache-Control 和 Expires 响应头,可以让浏览器在第一次请求时缓存资源,
// 并在过期前使用本地缓存,从而减少重复请求数据
location /static/ {
expires 1d;
add_header Cache-Control "public";
alias /www/static/;
}

优化打包速度

  1. 使用 HappyPack : 可以将 loader 进行多线程并行处理,加快速度 (thread-loader 也可以启用多线程)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const HappyPack = require("happypack");

module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: "happypack/loader?id=js",
exclude: /node_modules/,
},
],
},
plugins: [
new HappyPack({
id: "js", // 代表唯一标识符
threads: 4, // 代表启动的线程数
loaders: ["babel-loader"], // 要处理的loader
}),
],
};
  1. 使用 DLLPlugin 进行预编译: 可以将第三方库先进行打包,然后在开发时直接使用已经打包好的 dll 文件,从而减少重复打包的时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require("path");
const webpack = require("webpack");

module.exports = {
entry: {
vendors: ["react", "react-dom", "lodash"],
},
output: {
filename: "[name].dll.js",
path: path.resolve(__dirname, "dist"),
publicPath: "/dll",
library: "[name]_lib",
},
plugins: [
new webpack.DllPlugin({
context: __dirname,
name: "[name]_lib",
path: path.join(__dirname, "dist", "[name].manifest.json"),
}),
],
};
  1. 使用 IgnorePlugin 忽略部分模块: 忽略掉不需要的模块,在打包时跳过它们,从而提升打包速度。

也可以使用 exclude 、include 来指定需要特定 loader 编译的文件,并非不编译,而是使用默认的 loader 还是指定 loader 的区别。exclude 的优先级高于 include

Webpack 配置中的 module.noParse 则是在 include/exclude 的基础上,进一步省略了使用默认 js 模块编译器进行编译的时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const webpack = require('webpack');

module.exports = {
{
test: /\.js$/,
include: path.resolve(__dirname, '../src'),
exclude: /node_modules/,
use: [
'babel-loader'
]
},

plugins: [
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]
}
  1. 设置 resolve.alias 配置: 可以将一些常用的模块路径映射为绝对路径,从而缩短 Webpack 查找模块的时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
react: path.resolve(
__dirname,
"node_modules/react/cjs/react.production.min.js"
),
"react-dom": path.resolve(
__dirname,
"node_modules/react-dom/cjs/react-dom.production.min.js"
),
},
},
};
  1. TypeScript 编译优化
  • 使用 ts-loader: 默认在编译前进行类型检查,因此编译时间往往比较慢,通过加上配置项 transpileOnly: true,可以在编译时忽略类型检查
1
2
3
4
5
6
7
8
9
10
11
rules: [
{
test: /\.ts$/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
},
],
  • 使用 babel-loader: 需要单独安装 @babel/preset-typescript 来支持编译 TS,配合 ForkTsCheckerWebpackPlugin 使用类型检查功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
module: {
rules: [
{
test: /\.ts$/,
use: ["babel-loader"],
},
],
},
plugins: [
new TSCheckerPlugin({
typescript: {
diagnosticOptions: {
semantic: true,
syntactic: true,
},
},
}),
],
};

其他优化项

结合其他插件: MiniCssExtractPlugin、Webpack Shell Plugin、Autoprefixer

  1. Scope Hoisting (作用域提升)

普通打包只是将一个模块最终放入一个单独的函数中,如果模块很多,就意味着在输出结果中会有很多的模块函数。concatenateModules 配置的作用,尽可能将所有模块合并到一起输出到一个函数中,既提升了运行效率,又减少了代码的体积

1
2
3
4
5
6
7
8
9
module.exports = {
// ... 其他配置项
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能合并每一个模块到一个函数中
concatenateModules: true,
},
};
  1. parallel-webpack

并发构建的第二种场景是针对与多配置构建。Webpack 的配置文件可以是一个包含多个子配置对象的数组,在执行这类多配置构建时,默认串行执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var path = require('path');
module.exports = [
{
entry: './pageA.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'pageA.bundle.js'
}
},
{
entry: './pageB.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'pageB.bundle.js'
}
}];

// 多入口并发构建
"build:parallel": "parallel-webpack --config webpack.parallel.config.js"

示例

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
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");

module.exports = {
mode: "development", // 'production', //'development',
entry: "./src/index.js",
output: {
filename: "[name].js",
path: path.resolve(__dirname, "./dist"),
publicPath: "./",
},
resolve: {
extensions: [".js"],
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
],
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.jpg$/,
use: ["file-loader"],
},
{
test: /\.less$/,
include: {
and: [path.join(__dirname, "./src/")],
},
// './lib/style-loader',
use: ["./lib/style-loader", "./lib/less-loader"],
},
],
},
optimization: {
splitChunks: {
chunks: "all",
},
},

plugins: [
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: "./index.html",
}
)
),
],
};