工具链和浏览器
什么是 Webpack?
Section titled “什么是 Webpack?”Webpack 是一个现代 JavaScript 应用程序的静态模块打包器。它的主要作用是将前端项目中的各种资源文件(如 JavaScript、CSS、图片等)视为模块,然后根据依赖关系将这些模块打包成浏览器可以识别的静态资源。
核心思想:“一切皆模块”
- 从入口点开始,递归地构建依赖图
- 将所有模块打包成一个或多个 bundle 文件
Webpack 的主要作用是什么?
Section titled “Webpack 的主要作用是什么?”- 模块打包:将分散的模块文件按照依赖关系打包成一个或多个文件
- 代码转换:通过加载器(loaders)将 TypeScript、JSX、ES6 等转换为浏览器兼容的 JavaScript
- 代码优化:提供压缩、混淆、Tree Shaking 等优化功能
- 开发辅助:提供开发服务器和热模块替换(HMR)功能,提高开发效率
- 资源管理:能够处理和打包各种类型的资源文件,包括 CSS、图片、字体等
Webpack 存在哪些“瓶颈”?
Section titled “Webpack 存在哪些“瓶颈”?”- 启动速度慢:开发服务器启动时,需要完成对整个项目的遍历、依赖分析、编译和打包。对于大型项目,这个过程非常缓慢(数秒到数十秒)
- HMR 效率不佳:文件变动时,需判断影响模块并重新构建。在复杂项目中,HMR 的响应速度不尽如人意
Webpack 的核心概念有哪些?
Section titled “Webpack 的核心概念有哪些?”- 入口(Entry):这是 Webpack 构建依赖图的起点。Webpack 会从入口文件开始,递归地寻找和打包所有依赖的模块。可以配置单个入口,也可以配置多个入口。例如:
entry: './src/index.js'。 - 输出(Output):告诉 Webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件。可以指定输出路径和文件名。例如:
output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }。 - 加载器(Loaders):Webpack 自身只能理解 JavaScript 和 JSON 文件。Loaders 让 Webpack 能够去处理其他类型的文件,并将它们转换为有效模块。例如,
babel-loader用于转换 ES6 代码,css-loader用于处理 CSS 文件。 - 插件(Plugins):插件用于执行范围更广的任务,如打包优化、资源管理和环境变量注入等。与 Loaders 不同,Plugins 可以在整个构建周期中执行操作。例如,
HtmlWebpackPlugin用于自动生成 HTML 文件。 - 模式(Mode):通过设置
development、production或none来启用相应的内置优化。例如,mode: 'production'会启用代码压缩等优化。
Webpack 中的 Loaders 和 Plugins 有什么区别?
Section titled “Webpack 中的 Loaders 和 Plugins 有什么区别?”简单来说,Loaders 负责转换文件,而 Plugins 负责扩展 Webpack 功能。Loaders 处理的是单个文件,而 Plugins 处理的是整个构建过程。
Loaders(加载器):
- Loaders 主要用于转换模块源代码,它们将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图片转换为 data URL。
- Loaders 在模块级别上工作,对单个文件进行转换。
- Loaders 可以链式调用,它们会按照从右到左或从下到上的顺序执行。
- Loaders 通常在
module.rules中配置,通过test正则表达式匹配文件类型。 - 常见的 Loaders 包括:
babel-loader(转换 ES6+ 代码)、css-loader(处理 CSS 文件)、style-loader(将 CSS 插入到 DOM 中)等。
Plugins(插件):
- Plugins 用于执行更广泛的任务,如打包优化、资源管理和环境变量注入等。
- Plugins 在整个构建过程中工作,可以监听构建过程中的事件,并在合适的时机执行操作。
- Plugins 不能链式调用,每个插件都是独立的。
- Plugins 在
plugins数组中直接实例化。 - 常见的 Plugins 包括:
HtmlWebpackPlugin(生成 HTML 文件)、MiniCssExtractPlugin(提取 CSS 到单独文件)、TerserWebpackPlugin(压缩 JavaScript)等。
什么是 Webpack 的代码分割(Code Splitting)?它有什么好处?
Section titled “什么是 Webpack 的代码分割(Code Splitting)?它有什么好处?”代码分割(Code Splitting) 是 Webpack 中一个非常重要的优化技术,它允许将代码分割成多个 bundle,然后可以按需加载或并行加载这些文件。这种技术可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
代码分割的主要好处包括:
- 减少初始加载时间:通过将应用分割成多个小块,用户只需要下载当前页面所需的代码,而不是整个应用。这可以显著减少首次加载时间,提升用户体验。
- 更好的缓存利用:当代码被分割成多个文件时,如果只修改了其中一个文件,用户再次访问时只需要重新下载这个修改过的文件,其他未修改的文件可以从缓存中读取,提高加载速度。
- 按需加载:可以实现按需加载某些功能模块,例如当用户点击某个按钮或导航到特定路由时才加载相应的代码。这对于大型单页应用特别有用。
- 并行加载:多个小文件可以并行加载,这比加载一个大文件更高效,特别是在 HTTP/1.1 协议下。
在 Webpack 中,实现代码分割有多种方式:
- 使用
import()语法进行动态导入 - 使用
webpack.optimize.SplitChunksPlugin插件 - 通过
entry配置多个入口点
代码分割是现代前端性能优化的重要手段,特别是在构建大型单页应用时,它可以帮助我们显著提升应用性能。
Webpack 中如何优化构建速度?
Section titled “Webpack 中如何优化构建速度?”-
缩小构建范围
- 使用
include和exclude明确指定 loader 的作用范围,避免对不必要的文件进行处理 - 配置
resolve.extensions减少文件查找 - 使用
resolve.modules指定模块查找目录,减少搜索时间
- 使用
-
利用缓存
- Webpack 5 默认开启了持久化缓存,通过
cache: { type: 'filesystem' }配置 - 使用
cache-loader为性能开销较大的 loader 添加缓存 - 使用
babel-loader时开启cacheDirectory选项
- Webpack 5 默认开启了持久化缓存,通过
-
多进程/多线程构建
- 使用
thread-loader将耗时的 loader 放在单独的 worker 池中运行 - 使用
terser-webpack-plugin的多进程并行压缩选项 - 考虑使用
parallel-webpack或happy-pack(虽然 happy-pack 已不再维护,但理念值得借鉴)
- 使用
-
减少不必要的操作
- 开发环境关闭不必要的功能,如代码压缩、Tree Shaking 等
- 使用
webpack-bundle-analyzer分析打包结果,移除不必要的依赖 - 避免在生产环境使用开发工具,如
source map
-
优化插件和 loader
- 选择性能更好的替代品,如用
esbuild-loader替代babel-loader - 减少插件使用,特别是那些会遍历所有文件的插件
- 使用 DLL 技术将不常变化的代码预先打包
- 选择性能更好的替代品,如用
-
合理使用 source map
- 开发环境使用
eval或eval-source-map - 生产环境使用
source-map或hidden-source-map - 避免使用
inline-source-map和cheap-module-eval-source-map,它们会增加构建时间
- 开发环境使用
-
拆分配置
- 为开发和生产环境创建不同的配置文件
- 使用
webpack-merge合并公共配置 - 根据环境变量动态加载所需的插件和 loader
通过以上优化方法,可以显著提高 Webpack 的构建速度,特别是在大型项目中,这些优化措施可以节省大量的构建时间,提高开发效率。需要注意的是,不同的项目可能需要不同的优化策略,应根据项目具体情况选择合适的优化方法。
什么是 Source map?
Section titled “什么是 Source map?”Source map 是一个信息文件,里面储存着位置信息。它建立了 打包压缩后的代码 与 源代码 之间的对应关系。当生产环境中出现错误时,浏览器可以通过 source map 准确地显示错误在原始代码中的位置,而不是在打包后的代码中,这极大地提高了调试效率。
什么是 Webpack 的热模块替换(HMR)?它是如何工作的?
Section titled “什么是 Webpack 的热模块替换(HMR)?它是如何工作的?”热模块替换(Hot Module Replacement, HMR) 是 Webpack 提供的一项功能,它允许在运行时更新所有类型的模块,而无需完全刷新页面。这意味着当开发者修改代码后,可以在不丢失应用程序状态的情况下看到更改,极大地提高了开发效率。
HMR 的工作原理如下:
- 应用程序启动:当使用
webpack-dev-server启动开发服务器时,它会生成一个客户端运行时,这个运行时会注入到浏览器中,与开发服务器建立 WebSocket 连接。 - 文件变更检测:当开发者修改并保存文件时,Webpack 会重新编译变更的模块。
- 变更通知:编译完成后,Webpack 通过 WebSocket 向浏览器发送更新消息,包含哪些模块发生了变化。
- 模块热更新:浏览器中的 HMR 运行时接收到更新消息后,会下载新的模块代码。
- 替换旧模块:HMR 运行时会用新模块替换旧模块,同时保留应用程序的状态。
- 执行回调:如果模块或其父模块定义了 HMR 回调(通过
module.hot.accept),这些回调将被执行,允许开发者处理模块替换后的自定义逻辑。
HMR 的优势包括:
- 保留应用程序状态:例如,在 React 应用中,组件的状态不会因为代码更新而丢失。
- 节省开发时间:不需要重新加载整个页面,只需要更新变更的部分。
- 即时反馈:修改代码后可以立即看到效果,无需等待页面刷新。
要启用 HMR,需要在 Webpack 配置中添加 webpack-dev-server 和 HotModuleReplacementPlugin,并在客户端代码中处理模块替换逻辑。对于不同的框架(如 React、Vue),通常有相应的加载器(如 react-refresh-webpack-plugin)来简化 HMR 的实现。
Vite 和 Webpack 的区别有哪些?
Section titled “Vite 和 Webpack 的区别有哪些?”Webpack 的构建方式是基于 打包(Bundle) 的。通过依赖分析将所有资源(JS、CSS、图片等)打包成静态文件,将其中的 JS 模块打包合并为一个大文件。在开发环境下,需要预先打包所有文件,导致大型项目启动和热更新较慢。
Vite 的构建方式是基于 浏览器原生 ES 模块 的,利用现代浏览器特性(ES Module 支持)实现按需编译。开发环境下不打包(启动速度较快),生产环境使用 Rollup 打包。
NOTE
构建方式指的是前端工具如何处理和组织代码模块的策略。简单来说,就是工具如何把你写的多个文件转换成浏览器能够运行的代码。
在启动时,Webpack 冷启动时需构建完整的依赖关系,启动较慢(几秒甚至几十秒);
而 Vite 在开发冷启时仅 预构建 第三方依赖(esbuild),源代码按需编译(借助 <script type="module">),提供了极速的冷启动体验(毫秒级启动)。
Webpack 的 HMR 依赖于打包,而 Vite 基于浏览器原生 ESM 支持实现了高效的 HMR。
介绍一下 Vite
Section titled “介绍一下 Vite”介绍一下 Vite 的依赖预构建
Section titled “介绍一下 Vite 的依赖预构建”在处理第三方库(CommonJS/UMD 格式,或像 lodash-es 这样包含大量小模块的库)时,Vite 会在首次启动时使用 esbuild 进行依赖预构建,实现:
- 格式转换:将非 ESM 依赖转换为 ESM
- 性能优化:将大型依赖内部众多小模块打包成单一模块
esbuild 使用 golang 编写,效率极高,速度极快。
什么场景下使用 Webpack 仍有优势?
Section titled “什么场景下使用 Webpack 仍有优势?”Vite 适合新项目、中小型项目;
Webpack:
- 大型、复杂存量项目:迁移成本较高
- 对特定 Webpack 插件强依赖的项目:Vite 生态尚无成熟替代品
- 需极度精细化控制打包的特殊需求
Vite 的生产构建为何使用 Rollup 而非 esbuild?
Section titled “Vite 的生产构建为何使用 Rollup 而非 esbuild?”esbuild 的核心优势在于极致的构建速度,它非常适合开发阶段的预构建和代码转换; Rollup 相对更加成熟,提供了更为强大的功能和更精细化的控制。在生产构建时,可以生成高质量的、充分优化的 JS Bundle。
如何理解 Vite 的“开发、生产不一致”问题?
Section titled “如何理解 Vite 的“开发、生产不一致”问题?”Vite 开发和生产环境不一致的根本原因是 两个环境采用了完全不同的构建策略:
- 开发环境:使用 ESM 原生模块 + esbuild 快速转译,按需编译
- 生产环境:使用 Rollup 打包 + 各种优化处理(tree-shaking、代码分割、压缩等)
什么是 HMR?Vite 是如何实现 HMR 的?
Section titled “什么是 HMR?Vite 是如何实现 HMR 的?”Hot Module Replacement 就是 只更新修改了的文件,而不刷新整个页面。
Vite 实现 HMR 的三步:
- 监听文件变化:Vite 的开发服务器会监听你的源代码文件。当你保存一个文件时,它立刻就知道哪个文件变了。
- 推送更新消息:服务器会立刻把这个变化的文件和一个唯一的标识(通常是路径)推送给浏览器。
- 执行更新:浏览器收到消息后,会替换掉老模块,并执行新的模块代码。如果这个模块导出了
accept方法(比如 Vue/Svelte 组件),它就会智能地重新渲染那一小部分。
一个比喻:
就像修一块墙上的砖,Vite 的 HMR 会只换掉那块坏砖(变化的模块)。而传统的刷新则是把整面墙都推倒重砌(刷新整个页面)。
介绍一下 Webpack 中的 HMR,和 Vite 中的相比,有什么区别?
Section titled “介绍一下 Webpack 中的 HMR,和 Vite 中的相比,有什么区别?”Webpack 当然有 HMR,它和 Vite 的目标一样:只更新修改的代码,不刷新页面。但它们“送货”的方式很不一样。
Webpack 实现 HMR 的三步:
- 重新打包:你一保存文件,Webpack 就得重新检查哪些模块变了,并快速打包生成一个更新的“包裹”(补丁文件)。
- 消息推送:服务器通过 WebSocket 连接告诉浏览器:“你的包裹到了”,并把新包裹送过去。
- 拆包替换:浏览器收到新包裹,拆开它,然后用里面的新模块替换掉旧的。
和 Vite 的最大区别(一个比喻):
- Webpack 像 中央厨房:你只要改了一棵葱,厨房就得重新打包整个菜盒(构建),再把新菜盒送过来。
- Vite 像 按需现炒的灶台:你改了一棵葱,它就直接递给你一棵新葱,几乎不用等待。
所以,Webpack 能办到同样的事,但通常 Vite “递菜”更快。
Tree Shaking 的本质是什么?它是如何实现的?ES Module 和 CommonJS 模块在 Tree Shaking 上为什么有差异?
Section titled “Tree Shaking 的本质是什么?它是如何实现的?ES Module 和 CommonJS 模块在 Tree Shaking 上为什么有差异?”Tree Shaking 的本质是通过 静态分析 来移除 JavaScript 中未被使用的代码,从而减小最终打包体积。
它的实现依赖于 ES Module 的静态结构(import/export)。因为 ES Module 的依赖关系在代码执行前就可以确定,打包工具(如 Webpack、Rollup)能够分析出哪些导出没有被使用,然后安全地删除这些代码。
ES Module 和 CommonJS 在 Tree Shaking 上的主要差异:
- ES Module 是静态的,依赖关系明确,便于工具分析,因此容易做 Tree Shaking。
- CommonJS 是动态的,
require()可以在任何地方调用,甚至条件触发,导致无法在构建时确定依赖,所以很难进行 Tree Shaking。
实际开发建议:
- 使用 ES Module 语法(
import/export)。 - 确保打包工具处于生产模式(如 Webpack 的
mode: 'production')。 - 尽量避免编写有副作用的代码,否则需通过
/*#__PURE__*/注释或package.json的sideEffects字段告知工具。
Rollup 中的 Tree Shaking 是怎样的?
Section titled “Rollup 中的 Tree Shaking 是怎样的?”在 Rollup 中,Tree Shaking 直接发生在打包阶段,过程如下:
- 解析模块:Rollup 将每个文件解析为抽象语法树(AST),分析模块间的依赖关系。
- 建立依赖图:从入口文件开始,递归追踪所有依赖模块,构建模块依赖图。每个模块的导出会被记录,并分析其被哪些模块使用。
- 标记未使用导出:根据依赖图,标记未被引用的导出(变量、函数、类等)。
- 移除未使用代码:根据标记,删除未使用的代码,生成精简的输出文件。
Webpack 中的 Tree Shaking 是怎样的?
Section titled “Webpack 中的 Tree Shaking 是怎样的?”在 Webpack 中,Tree Shaking 通过 标记未使用导出(usedExports)结合 代码压缩工具(如 Terser) 分两步完成,需配置 mode: 'production'(生产模式) 并配合 sideEffects 字段声明模块副作用。具体而言,步骤如下:
- 解析模块与依赖分析:类似于 Rollup,Webpack 将模块解析为 AST,分析依赖关系。
- 标记未使用导出:Webpack 通过
usedExports标记模块中未被使用的导出,但此时并不会删除。 - 代码压缩:通过 Terser 等代码压缩工具,删除未使用的代码,生成最终输出。
Vite 中的 Tree Shaking 是怎样的?
Section titled “Vite 中的 Tree Shaking 是怎样的?”Vite 的 Tree Shaking 发生在生产环境下基于 Rollup 的能力。
- 开发环境 (Dev):Vite 本身不进行传统的打包和 Tree Shaking。它利用浏览器原生支持 ES 模块的特性,按需提供源代码文件。这虽然启动极快,但未使用的代码也会被发送到浏览器。不过,得益于按需编译,实际效果上用户只收到了他们当前页面所需的模块。
- 生产环境 (Build):Vite 直接使用 Rollup 进行打包和优化。因此,Vite 生产环境的 Tree Shaking 行为与 Rollup 完全一致,非常高效和彻底。它会构建完整的依赖图,静态分析并移除所有未使用的代码。
什么是模块副作用?
Section titled “什么是模块副作用?”模块副作用(Module Side Effects) 指的是一个模块在导入或执行时,除了导出内容外,还会对程序产生额外的、不可预知的影响,例如修改全局变量、触发网络请求、初始化配置或操作 DOM 等。这类副作用会导致 Tree Shaking 优化失效,因为构建工具(如 Webpack、Rollup)默认假设模块是“纯”的(即无副作用),若检测到模块未被使用,会直接移除它。但若模块包含副作用,移除后可能导致功能异常(如全局配置丢失或插件未注册)。因此,正确声明模块副作用是确保 Tree Shaking 安全的关键。
通过在 package.json 中设置 sideEffects 字段,可以明确标记哪些模块有副作用(例如 ["*.css", "./polyfill.js"]),或声明整个包无副作用(false)。对于函数级别的副作用,可用 /*@__PURE__*/ 注释标记纯函数(Rollup 支持),帮助工具判断是否可删除。开发中应尽量避免在模块顶层编写副作用代码,而是将其封装到函数或类中,按需触发。
总结来说,模块副作用是 Tree Shaking 的“隐形陷阱”,需通过声明、注释和代码设计三管齐下,才能在优化包体积的同时保证功能完整性。
什么是 AST?在前端领域有哪些应用?
Section titled “什么是 AST?在前端领域有哪些应用?”AST 是 抽象语法树(Abstract Syntax Tree) 的缩写,是源代码的抽象语法结构的树状表示,用于编程语言的分析和处理。AST 通常由节点构成,每个节点代表源代码中的一个结构,如变量、函数、表达式等,通过节点间的关系(如父子关系、兄弟关系)描述源代码的层次结构。AST 通常是编译器的中间表示,用于语法分析、代码转换、代码生成等编译过程。
具体的应用包括但不限于:
- 代码转换:通过 AST 可以实现代码转换,例如 Babel 将 ES6 代码转换为 ES5 代码,或 TypeScript 编译器将 TypeScript 转换为 JavaScript。
- 代码分析:通过 AST 可以分析代码结构,实现代码检查、代码高亮、代码格式化等功能,如 ESLint、Prettier 等工具。
- 代码压缩和优化:通过 AST 可以实现代码压缩和优化,例如 UglifyJS、Terser 等工具可以通过 AST 删除无用代码、压缩代码。
- 模板编译:将模板(如 Vue 的单文件组件、React 的 JSX)转换为 AST,进而转换为可执行 JavaScript 代码。过程中,还可进行分析、优化等操作。
- 代码高亮和智能提示:通过 AST 可以实现代码高亮和智能提示功能,如编辑器中的代码高亮、自动补全、错误提示等。
Rolldown 对比 Rollup 的优势有哪些?
Section titled “Rolldown 对比 Rollup 的优势有哪些?”- 性能飞跃:Rolldown 使用 Rust 实现,效率远高于基于 JS 的 Rollup。同时并行化更好,内存占用更低。
- 更健康的插件系统:标准化插件接口、类型安全的插件开发。
- 对现代特性更广泛的支持。
介绍一下前端领域的各种缓存及其最佳实践
Section titled “介绍一下前端领域的各种缓存及其最佳实践”前端缓存是 Web 性能优化的核心基石,它通过在不同层面复用已获取的资源,显著减少网络请求、降低服务器负载并加快页面加载速度。前端领域的缓存可以大致分为三大类:HTTP 缓存、浏览器缓存和应用层缓存。
1. HTTP 缓存(网络层)
Section titled “1. HTTP 缓存(网络层)”这是由浏览器和服务器通过 HTTP 协议头控制的缓存机制,是性能优化的第一道防线。
-
强缓存 (Strong Cache): 直接从本地副本加载资源,无需发送任何网络请求。由响应头中的
Expires(HTTP/1.0) 和Cache-Control: max-age=...(HTTP/1.1, 优先级更高) 控制。- 最佳实践: 适用于版本化、内容不会改变的静态资源,如带哈希值的 JS/CSS 文件(
app.[contenthash].js)。为其设置一个非常长的max-age(如一年),Cache-Control: public, max-age=31536000, immutable。immutable告诉浏览器该文件内容绝对不会改变,可以更有信心地使用强缓存。
- 最佳实践: 适用于版本化、内容不会改变的静态资源,如带哈希值的 JS/CSS 文件(
-
协商缓存 (Negotiation Cache): 强缓存失效后,浏览器会向服务器发送一个“验证请求”。如果资源未改变,服务器返回
304 Not Modified状态码(响应体为空),浏览器继续使用本地副本;如果资源已改变,则返回200和新的资源内容。- ETag / If-None-Match: 基于资源内容的唯一标识(哈希值),比基于时间的
Last-Modified更准确,是首选的验证方式。 - Last-Modified / If-Modified-Since: 基于文件的最后修改时间。在某些场景下(如 1 秒内多次修改、分布式系统中时间不一致)可能不准确。
- 最佳实践: 适用于频繁变动或需要保证最新的资源,如
index.html。通常设置为Cache-Control: no-cache,意为“可以缓存,但每次使用前必须回源验证”,确保用户总能拿到最新的入口文件,进而加载到最新的带哈希的静态资源。
- ETag / If-None-Match: 基于资源内容的唯一标识(哈希值),比基于时间的
2. 浏览器缓存(客户端存储)
Section titled “2. 浏览器缓存(客户端存储)”除了 HTTP 协议定义的缓存,浏览器还提供了多种客户端存储机制,让 Web 应用可以主动缓存数据。
-
LocalStorage / SessionStorage: 键值对存储。
LocalStorage持久存储(除非手动清除),SessionStorage则与会话绑定(标签页关闭即清除)。- 最佳实践: 适合存储少量、非敏感、结构简单的数据,如用户偏好设置(主题色)、未登录时的购物车信息、JWT Token 等。切勿存储敏感信息。
-
IndexedDB: 一个功能强大的客户端事务型数据库。支持索引、事务和存储大量结构化数据(包括
File和Blob对象)。- 最佳实践: 适用于需要离线访问的复杂 Web 应用(PWA),如缓存 API 响应、文章内容、用户数据等。通常会用
localForage或Dexie.js这样的库来简化其繁琐的 API。
- 最佳实践: 适用于需要离线访问的复杂 Web 应用(PWA),如缓存 API 响应、文章内容、用户数据等。通常会用
-
Cache API (Service Worker): Service Worker 的核心能力之一。它允许你拦截网络请求,并从一个程序化管理的
Cache对象中返回响应。这是实现 PWA 离线体验和高级缓存策略的基石。- 最佳实践: 实现网络请求级别的缓存。可以定义灵活的缓存策略,如 Stale-While-Revalidate(先用缓存,再去后台更新)、Cache First(缓存优先)、Network First(网络优先)等,为单页应用(SPA)的 API 数据和应用外壳(App Shell)提供无缝的离线体验。
3. 应用层缓存(代码逻辑层)
Section titled “3. 应用层缓存(代码逻辑层)”这是在前端应用逻辑内部实现的缓存,通常是内存缓存。
- 内存缓存 (In-Memory Cache): 使用 JavaScript 对象或
Map在内存中缓存数据。它的生命周期与页面一致,刷新即丢失。- 最佳实践: 适用于缓存那些生命周期短、频繁访问、计算成本高的数据。例如:
- 数据请求去重: 在短时间内对同一 API 的重复请求,可以直接返回内存中的 Promise 对象,避免发送冗余的网络请求。
- 计算结果记忆化 (Memoization): 对于纯函数的昂贵计算,可以使用
useMemo(React) 或computed(Vue),或者自己实现一个 memoize 函数,将输入和结果缓存起来。 - 状态管理库: 像
React Query(TanStack Query) 和SWR这样的现代数据获取库,内部就维护了一套复杂的内存缓存系统,自动处理缓存数据的过期、后台更新、乐观更新等,是现代 Web 应用进行服务端状态管理的最佳实践。
- 最佳实践: 适用于缓存那些生命周期短、频繁访问、计算成本高的数据。例如:
总结:缓存策略金字塔
Section titled “总结:缓存策略金字塔”一个健壮的前端缓存策略是一个分层的金字塔:
- 塔基(最广泛): HTTP 缓存,面向所有静态资源,是基础和必备。
- 塔中: 浏览器缓存(特别是 Cache API),为应用提供离线能力和对网络请求的精细控制。
- 塔尖(最精细): 应用层缓存,针对具体业务逻辑和数据状态,进行去重、记忆化等微操,提升应用运行时的流畅度。
介绍一下从输入 URL 到页面最终可交互的过程中,具体发生了什么
Section titled “介绍一下从输入 URL 到页面最终可交互的过程中,具体发生了什么”- 用户输入 URL 并解析:用户在浏览器地址栏输入 URL,浏览器会解析这个 URL,判断是合法的 URL 还是搜索词。如果是搜索词,会使用默认搜索引擎进行搜索。
- DNS 查询:浏览器缓存 -> 系统缓存 -> 路由器缓存 -> ISP DNS 缓存 -> 根域名服务器。浏览器会依次查找各级缓存,直到找到域名对应的 IP 地址。
- 建立 TCP 连接:浏览器与服务器通过三次握手建立 TCP 连接。
- 发送 HTTP 请求:浏览器向服务器发送 HTTP 请求,请求获取页面资源。
- 服务器处理请求并返回响应:服务器接收到请求后,会处理请求并返回 HTTP 响应,响应中包含了页面的 HTML 内容。
- 浏览器接收并解析响应:浏览器接收到服务器的响应后,会开始解析 HTML 文档。
- 渲染过程:
- 构建 DOM 树:将 HTML 文本内容 parse 为 DOM。
- 构建 CSSOM 树:将 CSS 文本内容 parse 为 CSSOM(与 DOM 并行,但会阻塞 render tree 的生成,因为样式必须全量)。
- 合并生成 Render Tree(Attachment): DOM 节点 + 对应计算后样式 → 只包含可见节点(display:none 的不会进入)。
- Layout(Reflow): 根据 Render Tree 计算每个节点的几何信息,例如位置、尺寸等。
- Layerize(分层): 浏览器把页面拆成多个合成层(compositor layers)。
- Paint(绘制): 把每个层拆成绘制指令(draw calls),记录为显示列表。
- Raster(光栅化): 合成线程把指令转交给 GPU 进程,在 GPU 或 CPU 上把矢量指令变成位图。
- Composite(合成): GPU 把各层位图按正确顺序(z-index、transform)合成为最终帧,送到显示器。
- JavaScript 执行:在解析 HTML 的过程中,如果遇到
<script>标签,会阻塞 DOM 的解析,先执行 JavaScript 代码。JavaScript 可能会修改 DOM 和 CSSOM。 - 页面可交互:当 DOM 解析完成,并且所有延迟执行的脚本执行完毕后,页面进入可交互状态。
重排和重绘分别是什么?有什么区别?针对各种 CSS 属性的变化说一说
Section titled “重排和重绘分别是什么?有什么区别?针对各种 CSS 属性的变化说一说”重排(Reflow) 发生在元素的几何属性(如尺寸、位置)发生变化时,例如修改 width、height、margin 或 position 等属性,或者修改 font-size、font-family 导致几何尺寸变化时,浏览器需要重新计算布局,确定所有元素的位置和大小,这一过程计算成本较高,可能波及整个页面布局。
而 重绘(Paint) 则是元素外观(如颜色、背景)改变但不影响布局时触发的像素重新绘制,例如调整 color、background-color、outline 或 box-shadow,虽然成本低于重排,但仍需避免频繁触发。两者的核心区别在于:重排必然导致后续重绘,而重绘不一定伴随重排。
此外,修改 opacity 或 transform 时,若元素处于独立图层(如通过 will-change 优化),浏览器会跳过重排重绘,直接通过 合成(Composite) 操作完成,这类属性(如 transform、opacity)因 GPU 加速成为性能优化的首选。
介绍一下 Cookie 和 Session
Section titled “介绍一下 Cookie 和 Session”Cookie 和 Session 是 Web 开发中最常用的两种“状态保持”技术,它们都能让服务器在多次 HTTP 请求之间识别出同一个客户端。
Cookie
Section titled “Cookie”Cookie 是服务器通过 Set-Cookie 响应头“种”在浏览器里的一小段文本(key-value 及其他属性),浏览器在后续同源请求中通过 Cookie 请求头自动回传。
关键属性:
- Name / Value
- Expires / Max-Age
- Domain / Path(作用域)
- Secure(仅 HTTPS)
- HttpOnly(禁止 JS 读取,防 XSS)
- SameSite(防 CSRF)
Session
Section titled “Session”Session 指“服务器端的会话存储”。浏览器首次访问时,服务器创建一个会话对象并生成一个全局唯一的 sessionId(通常是一串随机字符串),通过 Cookie(或 URL 重写)返回给浏览器。以后浏览器每次请求都携带此 sessionId,服务器据此取出对应的会话数据。
介绍一下进程的概念,以及浏览器进程
Section titled “介绍一下进程的概念,以及浏览器进程”进程(Process) 是操作系统进行资源分配和调度的基本单位。简单来说,进程就是正在运行的程序的实例。每个进程都有独立的内存空间、系统资源(如文件句柄、网络连接等)以及一个或多个执行线程。
现代浏览器(如 Chrome、Edge、Firefox 等)普遍采用多进程架构(Multi-process Architecture),将不同的功能模块运行在不同的进程中,以提高稳定性、安全性和性能。以 Chrome 为例,其典型进程包括:
- 浏览器进程(Browser Process): 负责浏览器的主界面管理,如地址栏、书签、前进后退按钮等;管理其他所有子进程(渲染进程、GPU 进程等)。
- 渲染进程(Renderer Process): 每个标签页(或 iframe)通常由一个独立的渲染进程负责。负责解析 HTML、CSS,执行 JavaScript,布局和绘制页面。使用 Blink(Chromium 的渲染引擎)和 V8(JavaScript 引擎)。沙箱化运行,限制其对系统资源的直接访问,提升安全性。
- GPU 进程(GPU Process): 负责处理与图形相关的操作,如 3D 图形、WebGL、硬件加速的页面合成。将渲染任务提交给 GPU 执行,避免阻塞主进程。
- 插件进程(Plugin Process)
- 扩展进程(Extension Process): 每个浏览器扩展(如广告拦截器、密码管理器)可能运行在独立的进程中。
- 实用工具进程(Utility Process): 执行特定的辅助任务,如音视频解码、网络服务、文件解压缩等。
进程和线程有何区别?
Section titled “进程和线程有何区别?”进程是资源分配的单位,线程是执行调度的单位;一个进程可包含多个线程,这些线程共享进程资源(例如内存资源),但各自独立运行。
介绍一下进程间通信
Section titled “介绍一下进程间通信”进程间通信(Inter-Process Communication, IPC) 是指在不同进程之间传递数据或信号的机制。由于每个进程拥有独立的内存空间,它们不能像线程那样直接共享变量,因此必须通过特定的 IPC 机制来实现信息交换。
常用 IPC 机制包括:
- 管道(Pipe)
- 消息队列(Message Queue)
- 共享内存(Shared Memory)
- 信号(Signal)
- 套接字(Socket)
- 文件(File)
Chromium 使用了名为 Mojo 的 IPC 抽象层。它的工作方式是:浏览器进程(Browser Process)暴露某些“服务接口”。渲染进程(Renderer Process)通过 Mojo 连接并调用这些接口(如请求打开新窗口、读取文件、访问网络等)。
Mojo 的底层机制在不同系统上有所差异,例如 Windows 上使用管道和共享内存实现,而在 Linux、macOS 上可能使用包括套接字在内的多种手段
介绍一下各种构建工具
Section titled “介绍一下各种构建工具”| 工具 | 一句话总结 | 典型适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| Babel | JavaScript 编译器,主要用于将新语法转换为旧语法以兼容旧浏览器。 | 中小型项目,需要兼容旧浏览器的场景。 | 插件系统丰富,社区支持强大,兼容性极佳。 | 配置复杂,编译速度较慢。 |
| tsc | TypeScript 官方编译器,将 TS 代码转换为指定版本的 JS 代码。 | 任何使用 TypeScript 的项目,尤其是需要严格类型检查的场景。 | 官方支持,类型检查严格,与 TS 生态无缝集成。 | 编译速度较慢,功能相对单一(仅编译 TS)。 |
| Rollup | JavaScript 模块打包器,专注于库的打包,支持 Tree-shaking。 | 库或框架的开发(如 React、Vue),需要生成高效、精简的代码。 | 输出代码更小,Tree-shaking 效果好,适合库开发。 | 配置复杂,对代码拆分和动态导入支持较弱。 |
| esbuild | 极快的 JavaScript/TypeScript 打包器和压缩器,基于 Go 编写。 | 大型项目或需要快速构建的场景(如开发环境热更新)。 | 速度极快,支持 TS 和 JSX,零配置即可使用。 | 功能相对较少,插件生态不如 Webpack/Rollup 丰富。 |
| tsup | 基于 esbuild 的零配置 TypeScript 打包工具,简化构建流程。 | 中小型 TypeScript 项目,希望快速上手且无需复杂配置。 | 零配置,速度快,支持 TS 和 ES Modules。 | 灵活性较低,适合简单场景,复杂需求需扩展。 |
| Vite | 基于 esbuild 和 Rollup 的现代前端构建工具,主打开发体验和快速热更新。 | 中小型到大型项目,尤其是现代前端框架(如 Vue/React)的开发和生产构建。 | 开发服务器启动快,热更新迅速,支持多种前端框架。 | 生产构建依赖 Rollup,大型项目可能需优化配置。 |
| SWC | 基于 Rust 的快速 JavaScript/TypeScript 编译器和打包器。 | 大型项目,需要替代 Babel 或 tsc 以提高速度的场景(如 Next.js)。 | 速度极快(比 Babel 快 20 倍),支持 TS 和 JSX。 | 插件生态不如 Babel 成熟,某些边缘场景兼容性可能不足。 |
| Turbo | Turborepo 是高性能的 monorepo 构建工具,优化多包管理任务。 | 大型 monorepo 项目(如多包管理的企业级应用)。 | 并行构建和缓存优化,大幅提升 monorepo 构建速度。 | 需要一定学习成本,更适合复杂项目而非小型应用。 |
| OXC | 新兴的 JavaScript 工具链,旨在提供高性能的解析、编译和优化。 | 实验性或对性能要求极高的场景,未来可能替代部分 Babel/SWC 的功能。 | 基于 Rust,性能高,设计现代。 | 目前生态不成熟,文档和社区支持较少。 |
| Webpack | ||||
| rspack | ||||
| parcel |
拥抱新兴构建工具的目的:
- 性能
- 单线程瓶颈
- 大型工程
- 统一化
- OXC 编译、lint、format
- 内存安全性
- 社区繁荣
介绍一下什么是 Blob
Section titled “介绍一下什么是 Blob”Blob 是一种在浏览器中表示二进制数据的对象,全称是 Binary Large Object (二进制大对象)。它常用于处理文件、图片、视频等非文本数据。简单来说,Blob 是一段原始的二进制数据,可以用来存储任意类型的数据,并且支持以文件的形式操作这些数据。
Blob 表示的是不可变的、原始的二进制数据。它的内容可以是任何形式的文件(如图片、音频、视频、PDF 等),或者只是普通的字节流。每个 Blob 对象都有一个 MIME 类型(如 image/jpeg、application/pdf),用于描述数据的格式。Blob 的内容是只读的,不能直接修改。如果需要修改,可以通过切片(slice 方法)创建新的 Blob。
介绍一下什么是 Web Worker
Section titled “介绍一下什么是 Web Worker”Web Worker 是一种浏览器提供的技术,允许我们在 JavaScript 中创建一个独立的线程来运行脚本,从而避免阻塞主线程(通常是 UI 线程)。它的核心作用是解决耗时任务(如大量计算、数据处理、图片压缩等)对页面性能的影响,让页面保持流畅和响应。
// 主线程代码const worker = new Worker('worker.js');
// 向 Worker 发送消息worker.postMessage({ action: 'compress', data: largeImageData });
// 接收 Worker 返回的消息worker.onmessage = (event) => { console.log('收到 Worker 的结果:', event.data);};