Skip to content

工具链和浏览器

Webpack 是一个现代 JavaScript 应用程序的静态模块打包器。它的主要作用是将前端项目中的各种资源文件(如 JavaScript、CSS、图片等)视为模块,然后根据依赖关系将这些模块打包成浏览器可以识别的静态资源。

核心思想:“一切皆模块”

  • 从入口点开始,递归地构建依赖图
  • 将所有模块打包成一个或多个 bundle 文件
  • 模块打包:将分散的模块文件按照依赖关系打包成一个或多个文件
  • 代码转换:通过加载器(loaders)将 TypeScript、JSX、ES6 等转换为浏览器兼容的 JavaScript
  • 代码优化:提供压缩、混淆、Tree Shaking 等优化功能
  • 开发辅助:提供开发服务器和热模块替换(HMR)功能,提高开发效率
  • 资源管理:能够处理和打包各种类型的资源文件,包括 CSS、图片、字体等
  • 启动速度慢:开发服务器启动时,需要完成对整个项目的遍历、依赖分析、编译和打包。对于大型项目,这个过程非常缓慢(数秒到数十秒)
  • HMR 效率不佳:文件变动时,需判断影响模块并重新构建。在复杂项目中,HMR 的响应速度不尽如人意
  • 入口(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):通过设置 developmentproductionnone 来启用相应的内置优化。例如,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 中,实现代码分割有多种方式:

  1. 使用 import() 语法进行动态导入
  2. 使用 webpack.optimize.SplitChunksPlugin 插件
  3. 通过 entry 配置多个入口点

代码分割是现代前端性能优化的重要手段,特别是在构建大型单页应用时,它可以帮助我们显著提升应用性能。

  1. 缩小构建范围

    • 使用 includeexclude 明确指定 loader 的作用范围,避免对不必要的文件进行处理
    • 配置 resolve.extensions 减少文件查找
    • 使用 resolve.modules 指定模块查找目录,减少搜索时间
  2. 利用缓存

    • Webpack 5 默认开启了持久化缓存,通过 cache: { type: 'filesystem' } 配置
    • 使用 cache-loader 为性能开销较大的 loader 添加缓存
    • 使用 babel-loader 时开启 cacheDirectory 选项
  3. 多进程/多线程构建

    • 使用 thread-loader 将耗时的 loader 放在单独的 worker 池中运行
    • 使用 terser-webpack-plugin 的多进程并行压缩选项
    • 考虑使用 parallel-webpackhappy-pack(虽然 happy-pack 已不再维护,但理念值得借鉴)
  4. 减少不必要的操作

    • 开发环境关闭不必要的功能,如代码压缩、Tree Shaking 等
    • 使用 webpack-bundle-analyzer 分析打包结果,移除不必要的依赖
    • 避免在生产环境使用开发工具,如 source map
  5. 优化插件和 loader

    • 选择性能更好的替代品,如用 esbuild-loader 替代 babel-loader
    • 减少插件使用,特别是那些会遍历所有文件的插件
    • 使用 DLL 技术将不常变化的代码预先打包
  6. 合理使用 source map

    • 开发环境使用 evaleval-source-map
    • 生产环境使用 source-maphidden-source-map
    • 避免使用 inline-source-mapcheap-module-eval-source-map,它们会增加构建时间
  7. 拆分配置

    • 为开发和生产环境创建不同的配置文件
    • 使用 webpack-merge 合并公共配置
    • 根据环境变量动态加载所需的插件和 loader

通过以上优化方法,可以显著提高 Webpack 的构建速度,特别是在大型项目中,这些优化措施可以节省大量的构建时间,提高开发效率。需要注意的是,不同的项目可能需要不同的优化策略,应根据项目具体情况选择合适的优化方法。

Source map 是一个信息文件,里面储存着位置信息。它建立了 打包压缩后的代码源代码 之间的对应关系。当生产环境中出现错误时,浏览器可以通过 source map 准确地显示错误在原始代码中的位置,而不是在打包后的代码中,这极大地提高了调试效率。

什么是 Webpack 的热模块替换(HMR)?它是如何工作的?

Section titled “什么是 Webpack 的热模块替换(HMR)?它是如何工作的?”

热模块替换(Hot Module Replacement, HMR) 是 Webpack 提供的一项功能,它允许在运行时更新所有类型的模块,而无需完全刷新页面。这意味着当开发者修改代码后,可以在不丢失应用程序状态的情况下看到更改,极大地提高了开发效率。

HMR 的工作原理如下:

  1. 应用程序启动:当使用 webpack-dev-server 启动开发服务器时,它会生成一个客户端运行时,这个运行时会注入到浏览器中,与开发服务器建立 WebSocket 连接。
  2. 文件变更检测:当开发者修改并保存文件时,Webpack 会重新编译变更的模块。
  3. 变更通知:编译完成后,Webpack 通过 WebSocket 向浏览器发送更新消息,包含哪些模块发生了变化。
  4. 模块热更新:浏览器中的 HMR 运行时接收到更新消息后,会下载新的模块代码。
  5. 替换旧模块:HMR 运行时会用新模块替换旧模块,同时保留应用程序的状态。
  6. 执行回调:如果模块或其父模块定义了 HMR 回调(通过 module.hot.accept),这些回调将被执行,允许开发者处理模块替换后的自定义逻辑。

HMR 的优势包括:

  • 保留应用程序状态:例如,在 React 应用中,组件的状态不会因为代码更新而丢失。
  • 节省开发时间:不需要重新加载整个页面,只需要更新变更的部分。
  • 即时反馈:修改代码后可以立即看到效果,无需等待页面刷新。

要启用 HMR,需要在 Webpack 配置中添加 webpack-dev-serverHotModuleReplacementPlugin,并在客户端代码中处理模块替换逻辑。对于不同的框架(如 React、Vue),通常有相应的加载器(如 react-refresh-webpack-plugin)来简化 HMR 的实现。

Webpack 的构建方式是基于 打包(Bundle) 的。通过依赖分析将所有资源(JS、CSS、图片等)打包成静态文件,将其中的 JS 模块打包合并为一个大文件。在开发环境下,需要预先打包所有文件,导致大型项目启动和热更新较慢。

Vite 的构建方式是基于 浏览器原生 ES 模块 的,利用现代浏览器特性(ES Module 支持)实现按需编译。开发环境下不打包(启动速度较快),生产环境使用 Rollup 打包。

NOTE

构建方式指的是前端工具如何处理和组织代码模块的策略。简单来说,就是工具如何把你写的多个文件转换成浏览器能够运行的代码。


在启动时,Webpack 冷启动时需构建完整的依赖关系,启动较慢(几秒甚至几十秒); 而 Vite 在开发冷启时仅 预构建 第三方依赖(esbuild),源代码按需编译(借助 <script type="module">),提供了极速的冷启动体验(毫秒级启动)。

Webpack 的 HMR 依赖于打包,而 Vite 基于浏览器原生 ESM 支持实现了高效的 HMR。

在处理第三方库(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 的三步:

  1. 监听文件变化:Vite 的开发服务器会监听你的源代码文件。当你保存一个文件时,它立刻就知道哪个文件变了。
  2. 推送更新消息:服务器会立刻把这个变化的文件和一个唯一的标识(通常是路径)推送给浏览器。
  3. 执行更新:浏览器收到消息后,会替换掉老模块,并执行新的模块代码。如果这个模块导出了 accept 方法(比如 Vue/Svelte 组件),它就会智能地重新渲染那一小部分。

一个比喻:

就像修一块墙上的砖,Vite 的 HMR 会只换掉那块坏砖(变化的模块)。而传统的刷新则是把整面墙都推倒重砌(刷新整个页面)。

介绍一下 Webpack 中的 HMR,和 Vite 中的相比,有什么区别?

Section titled “介绍一下 Webpack 中的 HMR,和 Vite 中的相比,有什么区别?”

Webpack 当然有 HMR,它和 Vite 的目标一样:只更新修改的代码,不刷新页面。但它们“送货”的方式很不一样。

Webpack 实现 HMR 的三步:

  1. 重新打包:你一保存文件,Webpack 就得重新检查哪些模块变了,并快速打包生成一个更新的“包裹”(补丁文件)。
  2. 消息推送:服务器通过 WebSocket 连接告诉浏览器:“你的包裹到了”,并把新包裹送过去。
  3. 拆包替换:浏览器收到新包裹,拆开它,然后用里面的新模块替换掉旧的。

和 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。

实际开发建议:

  1. 使用 ES Module 语法(import/export)。
  2. 确保打包工具处于生产模式(如 Webpack 的 mode: 'production')。
  3. 尽量避免编写有副作用的代码,否则需通过 /*#__PURE__*/ 注释或 package.jsonsideEffects 字段告知工具。

Rollup 中的 Tree Shaking 是怎样的?

Section titled “Rollup 中的 Tree Shaking 是怎样的?”

在 Rollup 中,Tree Shaking 直接发生在打包阶段,过程如下:

  1. 解析模块:Rollup 将每个文件解析为抽象语法树(AST),分析模块间的依赖关系。
  2. 建立依赖图:从入口文件开始,递归追踪所有依赖模块,构建模块依赖图。每个模块的导出会被记录,并分析其被哪些模块使用。
  3. 标记未使用导出:根据依赖图,标记未被引用的导出(变量、函数、类等)。
  4. 移除未使用代码:根据标记,删除未使用的代码,生成精简的输出文件。

Webpack 中的 Tree Shaking 是怎样的?

Section titled “Webpack 中的 Tree Shaking 是怎样的?”

在 Webpack 中,Tree Shaking 通过 标记未使用导出usedExports)结合 代码压缩工具(如 Terser) 分两步完成,需配置 mode: 'production'(生产模式) 并配合 sideEffects 字段声明模块副作用。具体而言,步骤如下:

  1. 解析模块与依赖分析:类似于 Rollup,Webpack 将模块解析为 AST,分析依赖关系。
  2. 标记未使用导出:Webpack 通过 usedExports 标记模块中未被使用的导出,但此时并不会删除
  3. 代码压缩:通过 Terser 等代码压缩工具,删除未使用的代码,生成最终输出。

Vite 的 Tree Shaking 发生在生产环境下基于 Rollup 的能力。

  • 开发环境 (Dev):Vite 本身不进行传统的打包和 Tree Shaking。它利用浏览器原生支持 ES 模块的特性,按需提供源代码文件。这虽然启动极快,但未使用的代码也会被发送到浏览器。不过,得益于按需编译,实际效果上用户只收到了他们当前页面所需的模块。
  • 生产环境 (Build):Vite 直接使用 Rollup 进行打包和优化。因此,Vite 生产环境的 Tree Shaking 行为与 Rollup 完全一致,非常高效和彻底。它会构建完整的依赖图,静态分析并移除所有未使用的代码。

模块副作用(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 通常是编译器的中间表示,用于语法分析、代码转换、代码生成等编译过程。

具体的应用包括但不限于:

  1. 代码转换:通过 AST 可以实现代码转换,例如 Babel 将 ES6 代码转换为 ES5 代码,或 TypeScript 编译器将 TypeScript 转换为 JavaScript。
  2. 代码分析:通过 AST 可以分析代码结构,实现代码检查、代码高亮、代码格式化等功能,如 ESLint、Prettier 等工具。
  3. 代码压缩和优化:通过 AST 可以实现代码压缩和优化,例如 UglifyJS、Terser 等工具可以通过 AST 删除无用代码、压缩代码。
  4. 模板编译:将模板(如 Vue 的单文件组件、React 的 JSX)转换为 AST,进而转换为可执行 JavaScript 代码。过程中,还可进行分析、优化等操作。
  5. 代码高亮和智能提示:通过 AST 可以实现代码高亮和智能提示功能,如编辑器中的代码高亮、自动补全、错误提示等。

Rolldown 对比 Rollup 的优势有哪些?

Section titled “Rolldown 对比 Rollup 的优势有哪些?”
  1. 性能飞跃:Rolldown 使用 Rust 实现,效率远高于基于 JS 的 Rollup。同时并行化更好,内存占用更低。
  2. 更健康的插件系统:标准化插件接口、类型安全的插件开发。
  3. 对现代特性更广泛的支持。

介绍一下前端领域的各种缓存及其最佳实践

Section titled “介绍一下前端领域的各种缓存及其最佳实践”

前端缓存是 Web 性能优化的核心基石,它通过在不同层面复用已获取的资源,显著减少网络请求、降低服务器负载并加快页面加载速度。前端领域的缓存可以大致分为三大类: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, immutableimmutable 告诉浏览器该文件内容绝对不会改变,可以更有信心地使用强缓存。
  • 协商缓存 (Negotiation Cache): 强缓存失效后,浏览器会向服务器发送一个“验证请求”。如果资源未改变,服务器返回 304 Not Modified 状态码(响应体为空),浏览器继续使用本地副本;如果资源已改变,则返回 200 和新的资源内容。

    • ETag / If-None-Match: 基于资源内容的唯一标识(哈希值),比基于时间的 Last-Modified 更准确,是首选的验证方式
    • Last-Modified / If-Modified-Since: 基于文件的最后修改时间。在某些场景下(如 1 秒内多次修改、分布式系统中时间不一致)可能不准确。
    • 最佳实践: 适用于频繁变动或需要保证最新的资源,如 index.html。通常设置为 Cache-Control: no-cache,意为“可以缓存,但每次使用前必须回源验证”,确保用户总能拿到最新的入口文件,进而加载到最新的带哈希的静态资源。

除了 HTTP 协议定义的缓存,浏览器还提供了多种客户端存储机制,让 Web 应用可以主动缓存数据。

  • LocalStorage / SessionStorage: 键值对存储。LocalStorage 持久存储(除非手动清除),SessionStorage 则与会话绑定(标签页关闭即清除)。

    • 最佳实践: 适合存储少量、非敏感、结构简单的数据,如用户偏好设置(主题色)、未登录时的购物车信息、JWT Token 等。切勿存储敏感信息
  • IndexedDB: 一个功能强大的客户端事务型数据库。支持索引、事务和存储大量结构化数据(包括 FileBlob 对象)。

    • 最佳实践: 适用于需要离线访问的复杂 Web 应用(PWA),如缓存 API 响应、文章内容、用户数据等。通常会用 localForageDexie.js 这样的库来简化其繁琐的 API。
  • Cache API (Service Worker): Service Worker 的核心能力之一。它允许你拦截网络请求,并从一个程序化管理的 Cache 对象中返回响应。这是实现 PWA 离线体验和高级缓存策略的基石。

    • 最佳实践: 实现网络请求级别的缓存。可以定义灵活的缓存策略,如 Stale-While-Revalidate(先用缓存,再去后台更新)、Cache First(缓存优先)、Network First(网络优先)等,为单页应用(SPA)的 API 数据和应用外壳(App Shell)提供无缝的离线体验。

这是在前端应用逻辑内部实现的缓存,通常是内存缓存。

  • 内存缓存 (In-Memory Cache): 使用 JavaScript 对象或 Map 在内存中缓存数据。它的生命周期与页面一致,刷新即丢失。
    • 最佳实践: 适用于缓存那些生命周期短、频繁访问、计算成本高的数据。例如:
      • 数据请求去重: 在短时间内对同一 API 的重复请求,可以直接返回内存中的 Promise 对象,避免发送冗余的网络请求。
      • 计算结果记忆化 (Memoization): 对于纯函数的昂贵计算,可以使用 useMemo (React) 或 computed (Vue),或者自己实现一个 memoize 函数,将输入和结果缓存起来。
      • 状态管理库: 像 React Query (TanStack Query) 和 SWR 这样的现代数据获取库,内部就维护了一套复杂的内存缓存系统,自动处理缓存数据的过期、后台更新、乐观更新等,是现代 Web 应用进行服务端状态管理的最佳实践。

一个健壮的前端缓存策略是一个分层的金字塔:

  • 塔基(最广泛): HTTP 缓存,面向所有静态资源,是基础和必备。
  • 塔中: 浏览器缓存(特别是 Cache API),为应用提供离线能力和对网络请求的精细控制。
  • 塔尖(最精细): 应用层缓存,针对具体业务逻辑和数据状态,进行去重、记忆化等微操,提升应用运行时的流畅度。

介绍一下从输入 URL 到页面最终可交互的过程中,具体发生了什么

Section titled “介绍一下从输入 URL 到页面最终可交互的过程中,具体发生了什么”
  1. 用户输入 URL 并解析:用户在浏览器地址栏输入 URL,浏览器会解析这个 URL,判断是合法的 URL 还是搜索词。如果是搜索词,会使用默认搜索引擎进行搜索。
  2. DNS 查询:浏览器缓存 -> 系统缓存 -> 路由器缓存 -> ISP DNS 缓存 -> 根域名服务器。浏览器会依次查找各级缓存,直到找到域名对应的 IP 地址。
  3. 建立 TCP 连接:浏览器与服务器通过三次握手建立 TCP 连接。
  4. 发送 HTTP 请求:浏览器向服务器发送 HTTP 请求,请求获取页面资源。
  5. 服务器处理请求并返回响应:服务器接收到请求后,会处理请求并返回 HTTP 响应,响应中包含了页面的 HTML 内容。
  6. 浏览器接收并解析响应:浏览器接收到服务器的响应后,会开始解析 HTML 文档。
  7. 渲染过程
    • 构建 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)合成为最终帧,送到显示器。
  8. JavaScript 执行:在解析 HTML 的过程中,如果遇到 <script> 标签,会阻塞 DOM 的解析,先执行 JavaScript 代码。JavaScript 可能会修改 DOM 和 CSSOM。
  9. 页面可交互:当 DOM 解析完成,并且所有延迟执行的脚本执行完毕后,页面进入可交互状态。

重排和重绘分别是什么?有什么区别?针对各种 CSS 属性的变化说一说

Section titled “重排和重绘分别是什么?有什么区别?针对各种 CSS 属性的变化说一说”

重排(Reflow) 发生在元素的几何属性(如尺寸、位置)发生变化时,例如修改 widthheightmarginposition 等属性,或者修改 font-sizefont-family 导致几何尺寸变化时,浏览器需要重新计算布局,确定所有元素的位置和大小,这一过程计算成本较高,可能波及整个页面布局。

重绘(Paint) 则是元素外观(如颜色、背景)改变但不影响布局时触发的像素重新绘制,例如调整 colorbackground-coloroutlinebox-shadow,虽然成本低于重排,但仍需避免频繁触发。两者的核心区别在于:重排必然导致后续重绘,而重绘不一定伴随重排。

此外,修改 opacitytransform 时,若元素处于独立图层(如通过 will-change 优化),浏览器会跳过重排重绘,直接通过 合成(Composite) 操作完成,这类属性(如 transformopacity)因 GPU 加速成为性能优化的首选。

Cookie 和 Session 是 Web 开发中最常用的两种“状态保持”技术,它们都能让服务器在多次 HTTP 请求之间识别出同一个客户端。

Cookie 是服务器通过 Set-Cookie 响应头“种”在浏览器里的一小段文本(key-value 及其他属性),浏览器在后续同源请求中通过 Cookie 请求头自动回传。

关键属性:

  • Name / Value
  • Expires / Max-Age
  • Domain / Path(作用域)
  • Secure(仅 HTTPS)
  • HttpOnly(禁止 JS 读取,防 XSS)
  • SameSite(防 CSRF)

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): 执行特定的辅助任务,如音视频解码、网络服务、文件解压缩等。

进程是资源分配的单位,线程是执行调度的单位;一个进程可包含多个线程,这些线程共享进程资源(例如内存资源),但各自独立运行。

进程间通信(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 上可能使用包括套接字在内的多种手段

工具一句话总结典型适用场景优点缺点
BabelJavaScript 编译器,主要用于将新语法转换为旧语法以兼容旧浏览器。中小型项目,需要兼容旧浏览器的场景。插件系统丰富,社区支持强大,兼容性极佳。配置复杂,编译速度较慢。
tscTypeScript 官方编译器,将 TS 代码转换为指定版本的 JS 代码。任何使用 TypeScript 的项目,尤其是需要严格类型检查的场景。官方支持,类型检查严格,与 TS 生态无缝集成。编译速度较慢,功能相对单一(仅编译 TS)。
RollupJavaScript 模块打包器,专注于库的打包,支持 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 成熟,某些边缘场景兼容性可能不足。
TurboTurborepo 是高性能的 monorepo 构建工具,优化多包管理任务。大型 monorepo 项目(如多包管理的企业级应用)。并行构建和缓存优化,大幅提升 monorepo 构建速度。需要一定学习成本,更适合复杂项目而非小型应用。
OXC新兴的 JavaScript 工具链,旨在提供高性能的解析、编译和优化。实验性或对性能要求极高的场景,未来可能替代部分 Babel/SWC 的功能。基于 Rust,性能高,设计现代。目前生态不成熟,文档和社区支持较少。
Webpack
rspack
parcel

拥抱新兴构建工具的目的:

  • 性能
    • 单线程瓶颈
    • 大型工程
  • 统一化
    • OXC 编译、lint、format
  • 内存安全性
  • 社区繁荣

Blob 是一种在浏览器中表示二进制数据的对象,全称是 Binary Large Object (二进制大对象)。它常用于处理文件、图片、视频等非文本数据。简单来说,Blob 是一段原始的二进制数据,可以用来存储任意类型的数据,并且支持以文件的形式操作这些数据。

Blob 表示的是不可变的、原始的二进制数据。它的内容可以是任何形式的文件(如图片、音频、视频、PDF 等),或者只是普通的字节流。每个 Blob 对象都有一个 MIME 类型(如 image/jpeg、application/pdf),用于描述数据的格式。Blob 的内容是只读的,不能直接修改。如果需要修改,可以通过切片(slice 方法)创建新的 Blob。

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);
};