likes
comments
collection
share

【2024前端复盘复习计划】打包优化篇

作者站长头像
站长
· 阅读数 10

一、webpack

(一) 打包流程

  1. 初始化:Webpack 从配置文件(webpack.config.js 或命令行参数)中读取配置信息,包括入口起点(entry)、输出配置(output)、加载器(loader)、插件(plugin)等。
  2. 启动编译过程:当接收到构建指令后,Webpack 启动一个新的编译周期。在此阶段,它会创建一个 Compiler 对象,该对象负责整个项目的构建流程。
  3. 读取并解析入口: Compiler 对象根据配置的入口起点开始读取和解析模块。首先处理 JavaScript 文件,并递归地找出所有依赖模块。
  4. 构建依赖图: Webpack 通过静态分析生成一个详细的模块依赖关系图,其中包含每个模块及其对应的导入和导出信息。
  5. 预处理与转换模块(loader): 在遍历依赖图的过程中,Webpack 使用配置好的加载器对不同类型的模块进行预处理和转换,例如将 SASS/SCSS 转换为 CSS,将 TypeScript 转换为 JavaScript 等。
  6. 执行插件钩子(plugin) : 插件在整个构建过程中扮演着关键角色。它们可以在不同的生命周期钩子函数中执行自定义操作,如资源优化、代码分割、环境变量注入等。
  7. 代码生成与优化: 根据模块间的依赖关系,Webpack 将各个模块的内容合并成最终的 bundle 文件,并在生产模式下执行 Tree Shaking、Scope Hoisting、UglifyJS 压缩等优化操作。
  8. 生成输出文件: Webpack 根据配置中的 output 参数生成打包后的文件,包括 JS bundle、CSS bundle、图片和其他资源,并将它们放置到指定的目录结构中。
  9. 完成编译 : 所有资源打包完成后,Webpack 结束当前编译周期,并输出结果。如果配置了监听模式(watch mode),则会持续监听文件变化并重新触发构建流程。

简化版

  • Webpack CLI 启动打包流程;
  • 载入 Webpack 核心模块,创建 Compiler 对象;
  • 使用 Compiler 对象开始编译整个项目;
  • 从入口文件开始,解析模块依赖,形成依赖关系树;
  • 递归依赖树,将每个模块交给对应的 Loader 处理;
  • 合并 Loader 处理完的结果,将打包结果输出到 dist 目录。

通俗版

Webpack 启动后,会根据我们的配置,找到项目配置的入口起点(entry),然后顺着入口文件中的代码,根据代码中出现的 import(ES Modules)或者是 require(CommonJS)之类的语句,解析推断出来这个文件所依赖的资源模块,然后再分别去解析每个资源模块的依赖,周而复始,最后形成整个项目中所有用到的文件之间的依赖关系树

有了这个依赖关系树过后, Webpack 会遍历(递归)这个依赖树,找到每个节点对应的资源文件,然后根据配置选项中的 Loader 配置,交给对应的 Loader 去加载这个模块,最后将加载的结果放入 bundle.js(打包结果)中,从而实现整个项目的打包

(二) 核心概念

  • 入口(Entry) :入口是 Webpack 构建流程的起点。在配置文件中,开发者指定一个或多个入口模块, webpack从入口开始递归地找出所有依赖关系,并构建一个完整的模块依赖图。
  • 输出(Output) :输出配置告诉 Webpack 打包后的资源应该放在哪里以及如何命名。你可以设置输出目录、公共路径、打包后文件的名称等信息。Webpack 会根据模块依赖关系图将所有经过处理的模块合并成一个或多个 bundle 。
  • 模块(Module) : 任何类型的文件都可以视为模块。它可以是 JavaScript 文件、CSS 样式表、图片、字体等。Webpack 使用各种加载器(Loader)来解析和转换不同类型的模块。
  • 加载器(Loader): 加载器用于预处理文件,它们负责将非 JavaScript 模块转换为有效的模块,或者对现有模块进行编译、压缩等操作。
  • 插件(Plugin): 插件提供了 Webpack 更加灵活且强大的功能扩展。它们在 Webpack 构建生命周期的不同阶段执行任务,如代码优化、资源管理、环境变量注入、资产管理和性能分析等。
  • 模式(Mode): 提供了两种模式:development(开发模式)和 production(生产模式)。不同的模式下,Webpack 会有不同的默认优化策略和行为。
  • Tree Shaking: 通过静态分析移除未使用的模块或函数的技术,旨在减少最终生成的 bundle 大小,提升代码利用率。

(三) Loader

  • 单一原则: 每个 Loader 只做一件事;
  • 链式调用: Webpack 会按顺序链式调用每个 Loader;
  • 统一原则: 遵循 Webpack制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;

常用Loader:

  1. babel-loader:用于将 ES6 及以上版本的 JavaScript 代码转译成向后兼容的 ES5 代码,同时也支持 JSX(React)和其他 Babel 转换插件。
  2. css-loader:处理 CSS 文件,允许你像导入 JavaScript 模块一样 import 或 require() CSS 文件,并且可以解析 CSS 中的 @import 和 url() 引用。
  3. style-loader:将 CSS 作为内联样式直接注入到 HTML 文档中,或者创建 标签插入到 DOM 中。通常与 css-loader 结合使用以实现样式表的加载和应用。
  4. postcss-loader:在 CSS 后处理阶段添加对自动前缀、变量替换、CSS Modules 等特性的支持,配合不同的 PostCSS 插件使用。
  5. less-loader / sass-loader:分别用于编译 Less 和 Sass/SCSS 文件为 CSS。
  6. file-loader / url-loader:对于图片或其他资源文件,file-loader 会复制它们到输出目录并返回一个指向新路径的 URL,而 url-loader 允许设置一个大小限制,当文件大小小于该限制时,会将其转为 Data URI 方式内联到 JavaScript 或 CSS 中。
  7. ts-loader / awesome-typescript-loader:这两个 Loader 都用于将 TypeScript 文件转换为 JavaScript。
  8. eslint-loader:在编译期间执行 ESLint 检查,确保代码符合特定的编码规范。

(四) plugin

Webpack 插件是基于事件驱动机制和 Tapable 系统,核心原理是通过监听和响应编译过程中的事件钩子,在恰当的时间点介入并控制构建流程,从而扩展和定制 Webpack 的功能

Webpack 事件流编程范式的核心是基础类 Tapable,是一种 观察者模式 的实现事件的订阅与广播

  1. Webpack 插件是一个具有 apply 方法的 JavaScript 类或函数对象。当用户在 Webpack 配置文件中引入并实例化一个插件时(例如 new MyWebpackPlugin(options)),这个实例会被添加到配置的 plugins 数组中。在 Webpack 启动编译过程时,它会遍历这个数组,并对每个插件调用其 apply 方法,传入一个 compiler 对象。
  2. 插件通过调用 compiler.hooks 或 compilation.hooks 上的 .tap、.tapAsync 或 .tapPromise 方法来监听特定的事件(即钩子)。当这些事件在编译过程中触发时,相应的插件回调函数就会被执行。
  3. 当对应的编译阶段触发钩子时,Webapck 会按照注册顺序依次调用已挂载到该钩子上的所有插件方法。这些方法可以在适当的时机修改输出资源、处理额外任务、优化构建结果等
  4. 所有插件完成自己的工作后,Webpack 继续进行后续的编译和打包流程,直到最终生成目标文件。
class MyWebpackPlugin{
  	// 注册插件时,会调用 apply 方法
  	// apply 方法接收 compiler 对象
  	// 通过 compiler 上提供的 Api,可以对事件进行监听,执行相应的操作
  	apply(compiler){
  		// compilation 是监听每次编译循环
  		// 每次文件变化,都会生成新的 compilation 对象并触发该事件
    	compiler.plugin('compilation',function(compilation) {})
  	}
}

// webpack.config.js 注册插件
module.export = {
	plugins:[
		new MyWebpackPlugin(options),
	]
}

Compiler 与 Compilation 对象

  • compiler: compiler 对象代表整个 Webpack 环境,可以简单的理解为 Webpack 实例,它包含了当前 Webpack 中的所有配置信息,全局唯一,只在启动时完成初始化创建,随着生命周期逐一传递。
  • Compilation: compilation 对象则是在每次构建过程中生成的一个新实例,包含了当前模块依赖图、编译生成的资源等信息,并且提供了更细粒度的构建步骤相关的钩子函数,同时通过它提供的 api,可以监听每次编译过程中触发的事件钩子,Compilation对应每次编译,每轮编译循环均会重新创建。

常用插件

  • UglifyjsWebpackPlugin : 压缩、混淆代码;
  • CommonsChunkPlugin: 代码分割;
  • ProvidePlugin: 自动加载模块;
  • html-webpack-plugin: 生成 HTML 文件,通常用来注入编译后的 JavaScript 和 CSS 文件链接。它可以根据模板文件(如 index.html)自动生成带有所有 bundle 资源引用的 HTML 页面;
  • extract-text-webpack-plugin / mini-css-extract-plugin: 抽离样式,生成 css 文件; DefinePlugin: 定义全局变量;
  • optimize-css-assets-webpack-plugin: 进一步优化 CSS 资源,去除重复样式或进行压缩;
  • webpack-bundle-analyzer: 代码分析;
  • compression-webpack-plugin: 对输出的资源文件进行 Gzip 压缩,从而减小传输体积;
  • happypack: 使用多进程,加速代码构建;
  • EnvironmentPlugin: 定义环境变量;

(五) webpack 热更新实现原理

HMR(Hot Module Replacement)

  • 当修改了一个或多个文件;
  • 文件系统接收更改并通知 webpack;
  • webpack 重新编译构建一个或多个模块,并通知 HMR 服务器进行更新;
  • HMR Server 使用 webSocket 通知 HMR runtime 需要更新,HMR 运行时通过 HTTP 请求更新 jsonp
  • HMR 运行时替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新

(六) webpack 层面如何做性能优化

  1. 缩小编译范围
  • 针对特定文件类型使用loader,避免全局匹配。
  • include/exclude: 指定搜索范围/排除不必要的搜索范围
  • modules: 指定模块路径,减少递归搜索
  • alias:缓存目录,避免重复寻址
  1. 缓存利用
  • 使用cache-loader等缓存机制,避免重复编译未改变的模块
  • DLLPlugin 和 DLLReferencePlugin 也可以提前进行打包并缓存,避免每次都重新编译
  1. 忽略node_moudles
  • babel-loader,忽略node_moudles,避免编译第三方库中已经被编译过的代码。babel也可以缓存编译
  1. 多进程并发
  • 使用thread-loader或者HappyPack(现已不再维护,推荐使用thread-loader)来并发处理loader任务
  • 使用webpack-parallel-uglify-plugin或terser-webpack-plugin的并行压缩选项
  1. source-map
  • 开发使用cheap-module-eval-source-map 生产使用hidden-source-map
  1. Tree Shaking
  • 启用mode: 'production'以激活webpack的最小化模式和tree shaking特性。
  • 使用ES6模块导入导出,确保静态分析能够剔除未使用的代码。
  1. Scope Hoisting
  • 开启后,体积更小,创建函数作用域更小,代码可读性更好
  1. 资源压缩和优化
  • 对JS、CSS资源启用压缩,如使用TerserWebpackPlugin压缩JavaScript,MiniCssExtractPlugin配合CSS压缩工具压缩CSS。
  • 压缩图片和其他资源,如使用image-webpack-loader或file-loader配合compression-webpack-plugin压缩图片和gzip压缩输出文件

(七) vite为什么比webpack快

  1. Vite 利用了现代浏览器对原生 ES 模块(ESM)的支持,在开发环境下,它通过 HTTP 服务直接提供源代码 ,在启动项目时,Vite 只需加载并转换入口模块以及所需的直接依赖,而不是一次性编译整个项目,从而实现了快速的启动速度和热更新。
  2. Vite 采取的是即时编译策略,只有当浏览器请求某个模块时,Vite 才会去编译那个模块。
  3. Vite 使用 esbuild 进行依赖预构建,Webpack 中使用的 Babel,esbuild 在解析和转换代码的速度上有显著优势
  4. Vite 的开发模式下尽量避免不必要的优化,例如在开发阶段暂不进行 Tree Shaking 和 Scope Hoisting,只关注快速的开发反馈循环,这也加快了开发构建的速度