代码分割与缓存策略

共 21 题
📑 题目列表 21 题
#
★★★

1. HTTP 缓存(强缓存/协商缓存)与代码分割文件名的 hash 策略

HTTP 强缓存与协商缓存的机制是什么?与代码分割文件名的 hash 策略如何配合?

  • 强缓存(Cache-Control、Expires)与协商缓存(Last-Modified/ETag)的流程
  • contenthash 文件名与 immutable 缓存的配合原理
  • HTML 与静态资源的差异化缓存策略

强缓存指浏览器在过期时间内不发起请求直接使用本地副本(Cache-Control: max-age、immutable 等),协商缓存指缓存过期后携带 ETag/Last-Modified 向服务器验证,304 则复用本地(响应头 Last-Modified/ETag)。对前端静态资源,最有效的组合是"内容哈希文件名 + 永久强缓存":文件名含 contenthash,内容不变文件名不变,浏览器/CDN 可长期缓存(max-age=31536000, immutable);内容变化文件名变化,请求新文件,无需协商。

代码分割文件名 hash 策略要点:业务 chunk 与 vendor chunk 分开哈希(vendor 更新频率低,单独长缓存);hash 基于模块内容计算,避免"文件未变但 hash 变"导致缓存失效(如 webpack 的 contenthash 与 moduleIds 稳定性配置);HTML 是入口文件,不能长缓存(用 no-cache 协商或短缓存),因为其引用的文件名会随构建变化。整体策略即"入口短缓存、资源长缓存、变化靠文件名区分",配合 CDN 的缓存头与版本目录管理,实现发布即生效且缓存命中率最大化。

本题考察缓存体系的经典组合:机制(强/协商)是基础,hash 文件名是前端特有解法。回答应讲清两类缓存流程、hash 与 immutable 的配合原理、以及 HTML 入口的特殊处理,体现完整的缓存策略设计。

#
★★★

2. Webpack 5 Module Federation(与 Module Federation 2 / @module-federation/runtime)

Webpack 5 的 Module Federation 如何工作?它与 Module Federation 2 / @module-federation/runtime 有什么关系与演进?

  • Module Federation 的运行时共享与远程加载机制(shared、remotes、exposes)
  • @module-federation/runtime 将联邦能力运行时化、脱离构建期绑定
  • 版本协商、动态远程与跨框架应用的治理

Webpack 5 Module Federation 通过三个配置面实现运行时组合:exposes 声明本应用对外提供哪些模块,remotes 声明消费哪些远程应用(构建期写入 URL 映射),shared 声明共享依赖(react、react-dom 等)及其 semver 范围——运行时由联邦加载器协商共享版本,保证单例与版本兼容;远程模块以独立 chunk 在运行时按需加载,实现"独立部署、运行时组装"的微前端。局限是配置与运行时耦合在 webpack 构建期,远程必须在构建时声明,共享协商粒度较粗。

Module Federation 2(@module-federation/runtime 与 @module-federation/core)将联邦逻辑从打包器解耦为独立运行时库:支持动态注册远程(运行时添加/切换远程 URL,配合灰度)、打包器无关(webpack/Rspack/Vite 均可)、更细的共享粒度与增强的类型生成,并支持应用级嵌套联邦与版本热切换。工程价值:联邦边界更清晰(远程契约化、类型化),动态远程让"发布/回滚不重建宿主"成为可能;治理上需约定 exposes 面的 API 契约、共享依赖的版本策略与远程加载失败的降级方案。

本题考察联邦方案的演进脉络:1.0 是构建期耦合的运行时组合,2.0 是运行时化的联邦平台。回答应说明三配置面的机制、2.0 的动态远程与去构建期绑定,以及联邦治理要点。

#
★★★

3. esbuild、SWC、Rollup、Rspack 在构建性能与生态的取舍

esbuild、SWC、Rollup、Rspack 在构建性能与生态上如何取舍?如何按场景选型?

  • 四者的实现语言、性能与能力边界对比
  • 转译、打包、插件生态的覆盖差异
  • 按项目类型(应用/库/工具链)的选型矩阵

四者定位不同:esbuild 与 SWC 是"高性能转译器"(esbuild 用 Go、SWC 用 Rust),提供极快的 TS/JSX 转译与压缩,但 SWC 侧重转译 API(@swc/core、swc-loader),esbuild 侧重打包+转译一体且被 Vite 用作预构建引擎;Rollup 是"产物精细的 ESM 打包器",tree-shaking、多格式输出与插件体系成熟,是库打包与 Vite 生产构建的底座;Rspack 是"兼容 webpack 的高性能打包器"(Rust 内核 + JS loader/插件桥接),面向应用构建的 drop-in 替代。性能上 Rust/Go 系远超 JS 系,但生态深度不同:Babel/webpack 的插件数量最多,Rollup 插件体系成熟,esbuild/SWC 插件较浅。

选型矩阵:应用构建要"webpack 生态 + 性能"选 Rspack,要"开箱即用 + dev 体验"选 Vite(内部是 esbuild/Rolldown + Rollup 生态);库打包选 Rollup(+ tsc 出类型);纯转译需求(CLI 工具、按需编译)选 esbuild/SWC;追求一体化超快 Lint+Format 可叠加 oxlint/Biome。实践中常组合使用(Vite = esbuild 预构建 + Rollup/Rolldown 生产构建),关键是以"生态依赖度与性能需求"为轴做决策,而不是单一指标。

本题考察工具链选型的全景视图:先按"转译器/打包器"归类定位,再对比性能与生态两个维度,最后给出组合使用与选型矩阵。回答应避免"谁更好"的简单结论,突出按场景匹配。

#
★★★

4. Tree Shaking 的副作用(sideEffects)与 ESM 静态分析前提

Tree Shaking 依赖哪些 ESM 静态分析前提?sideEffects 字段如何参与摇树?

  • ESM 静态导入导出与死代码消除的前提
  • sideEffects 字段语义(false/数组)与误用风险
  • 打包器摇树粒度(模块级 vs 导出级)与限制

Tree Shaking 的前提是模块系统的静态性:ESM 的 import/export 在编译期确定(无运行时动态导出),打包器才能构建精确的"使用图",删除未被引用的导出及其关联代码;动态特性(CJS 的 module.exports 赋值、运行时计算导出名、通过变量间接访问)会破坏静态分析,导致摇树退化为保守保留。因此源码与依赖尽量保持 ESM 且导出命名静态可分析,是摇树有效性的第一前提。

sideEffects 是摇树的第二前提——"副作用声明":若包声明 sideEffects: false,表示所有模块执行无副作用,打包器可安全删除整模块(即便其部分导出被引用);用数组可声明"仅这些文件有副作用"(如 polyfill、CSS)。工程价值:库作者正确声明 sideEffects 可显著提升使用方摇树率;误用风险在于:模块顶层实际存在副作用(全局注册、样式导入、原型修改、初始化单例)却声明 false,会被错误裁剪导致线上缺失。实践:用 sideEffects 数组粒度化声明、避免一刀切 false、发布前用打包器产物验证摇树结果,并配合 import 语句不引用被摇导出的规范。

本题考察摇树的完整前提链:静态语法是基础,副作用声明是信任扩展。回答应强调"静态性 + 无副作用声明"两个前提,并重点说明 sideEffects 误用风险与粒度化治理。

#
★★★

5. ESM dynamic import 与 Route-based splitting 的实现

基于 ESM dynamic import 的路由级代码分割(Route-based splitting)如何实现?有哪些注意点?

  • 路由配置与动态 import 结合的分割模型
  • 框架层(React.lazy、Vue defineAsyncComponent)的封装
  • 预取、命名 chunk 与加载时序治理

Route-based splitting 的核心是"路由即分割边界":路由表中每个页面组件用 dynamic import(() => import('./pages/Home.vue'))引入,打包器据此把每个页面拆成独立 chunk,只有访问该路由时才加载对应代码,首屏只加载当前路由的产物。框架层封装了异步组件机制(React.lazy + Suspense、Vue defineAsyncComponent、Angular loadChildren),把"动态导入 + 加载状态 + 渲染切换"标准化,配合路由配置(Vue Router 的 component 函数形式、React Router 的 lazy 属性)即可零手工实现。

注意点:一是命名与预取——配合 chunk 命名(webpackChunkName / 文件路径名)提高可读性,用 prefetch(如 Vite 的 rollupOptions 或路由框架的预取提示)在空闲时预取高概率路由;二是加载态与错误态——Suspense fallback、路由级 loading 与失败重试;三是分割粒度——过细(每个路由独立 vendor 冗余)与过粗(单页 chunk 过大)的平衡,常用"路由 + 共享异步块"(splitChunks 异步复用)治理;四是 SSR 场景路由 chunk 需与客户端产物对应,保证水合一致;五是循环依赖路由的谨慎处理。

本题考察路由级分割的实现与治理:机制是 dynamic import 产生异步边界,框架层标准化封装。回答应覆盖实现方式、框架封装、预取与加载态、分割粒度与 SSR 一致性,体现端到端工程视角。

const routes = [
  { path: '/', component: () => import('./pages/Home.vue') },
  { path: '/about', component: () => import('./pages/About.vue') }
];
#
★★★

6. React.lazy 与 Suspense 在路由级懒加载与组件预取的工程取舍

React.lazy 与 Suspense 在路由级懒加载与组件预取中有哪些工程取舍?

  • React.lazy 的动态导入与 Suspense 边界语义
  • 预取策略(hover/空闲预取、preload)与首屏权衡
  • 懒加载的体验风险(闪烁、错误处理、SSR)与治理

React.lazy 接收返回 Promise 的动态导入函数,组件首次渲染时才请求并解析对应 chunk;Suspense 提供加载边界(fallback 展示 loading),二者配合实现组件级懒加载,常用于路由级分割(配合路由框架把页面包进 lazy)。工程价值是显著缩减首屏 JS 体积;取舍在于:懒加载引入加载延迟与体验不确定性——切换路由时出现 fallback 闪烁、弱网下白屏等待、加载失败无兜底。

治理手段:一是预取策略——在链接 hover/进入视口/空闲(requestIdleCallback)时提前触发动态导入(不渲染),把"切换时的加载"提前为"空闲时的预取",典型如 React Router 的 lazy 结合 prefetch 提示或自建预取注册表;二是 Suspense 边界粒度——页面级 vs 区块级 fallback 的切换体验差异;三是错误处理——Suspense 边界配合 Error Boundary 捕获 chunk 加载失败(网络/部署版本过期),实现重试与降级;四是 SSR 场景 React.lazy 需服务端流式渲染或预加载对应 chunk,保证水合一致性;五是分割粒度控制(页面/组件/第三方按需)与 React.memo/预取清单的配合,避免预取过度消耗带宽。

本题考察懒加载的完整工程闭环:机制(lazy+Suspense)之外,预取时机、错误兜底与 SSR 一致性是取舍重点。回答应体现"加载策略即体验设计"的思维,给出可执行治理清单。

#
★★★

7. 资源预加载, /prefetch 与 modulepreload 在首屏关键路径的工程协作

、prefetch 与 modulepreload 在首屏关键路径上如何协作?各自适用什么场景?
  • preload/prefetch/modulepreload 的语义与优先级差异
  • 首屏关键资源(CSS、字体、首屏 chunk)的预加载时机
  • 打包器自动注入 modulepreload 与手写预加载的边界

preload 用于"当前页面马上要用的关键资源":提前以高优先级发起请求(如首屏阻塞的 CSS、字体、首屏动态 chunk),告诉浏览器该资源很快会被使用,尽早开始下载;prefetch 用于"未来可能使用的资源"(下一路由、低优先级),在空闲带宽时以低优先级预取;modulepreload 专为 ESM 模块设计:预取模块脚本并同时触发其依赖图的抓取与解析,避免执行到 import 时才瀑布式请求,是"按需加载与首屏性能"的桥接——动态 import 的 chunk 配合 modulepreload 可提前建立依赖图。

协作模式:首屏关键路径用 preload 提升关键资源到达时间(CSS 与首屏 chunk 的 preload 可消除渲染阻塞与加载序列);动态分割的次级 chunk 用 prefetch 或空闲预取;对"即将执行的异步模块"用 modulepreload(打包器如 Vite 会在产物中自动注入 modulepreload 链接提升异步 chunk 加载);三者的资源优先级(preload 高、prefetch 低、modulepreload 中)决定带宽竞争结果,滥用 preload 会挤占首屏带宽。工程上应基于性能数据(LCP 资源、路由切换采样)选择预加载对象,避免"全量预加载"。

本题考察预加载家族的语义分工:preload(马上用)、prefetch(未来用)、modulepreload(模块依赖图提前抓取)。回答应对比优先级与场景,并给出打包器自动注入与手写策略的协作边界。

#
★★★

8. ESM 产物(output.module)与

ESM 产物(output.module)与

  • output.module 生成原生 ESM 产物与 script type=module 的加载
  • 浏览器模块级缓存与按需请求的边界(无 HTTP 合并)
  • 与打包产物、CDN 缓存的协作与降级

打包器输出原生 ESM 产物(webpack experiments.outputModule、Vite 默认 ESM)后,入口以

本题考察原生 ESM 产物的边界认知:收益是浏览器原生按需与模块级缓存,边界是请求数与聚合、URL 稳定性与降级。回答应把"产物格式-加载语义-缓存策略-降级"串成完整链路。

#
★★★

9. 动态 import() 的加载失败重试、超时降级与 Suspense fallback 的容错设计

动态 import() 加载失败时如何重试?超时降级与 Suspense fallback 的容错设计怎么做?

  • import() 失败的重试策略(指数退避、缓存回退)
  • 加载超时的判定与降级路径(缓存副本、降级组件)
  • Suspense fallback 与错误边界的协作

动态 import() 返回 Promise,失败(网络、CDN 故障、部署版本过期 404)时可用重试策略:封装 loadChunk 函数,记录失败次数并按指数退避(如 1s/2s/4s)重试,超过阈值进入降级路径;超时降级用 Promise.race 包裹超时控制(如 10s 未 resolve 视为超时),触发降级——加载本地缓存副本(如 service worker 缓存的上版 chunk)、降级 UI 组件或整页刷新;注意部署版本过期(HTML 已更新但异步 chunk 404)是线上常见失败,重试与回退到入口重载(location.reload 或跳转兜底页)结合。

Suspense fallback 的容错设计:fallback 是加载中的 UI 承诺,不能替代失败处理——失败必须由 Error Boundary 捕获(React)或组件级错误态兜底(Vue),把"加载中/失败/重试中"三态显式建模;加载失败后展示错误面板并提供"重试"按钮(重新触发 import,重置缓存状态);对路由级加载失败可提供"刷新页面"入口,配合监控上报(chunk 加载失败率)驱动运维介入。整体设计原则:加载失败可恢复、可观测、有兜底,不让用户停留在空白或无限 loading。

本题考察加载容错的完整设计:重试(退避)、超时(race 降级)、失败态(Error Boundary 三态建模)三层缺一不可。回答应结合版本过期这一真实场景,并强调"fallback 不是容错"的认知。

#
★★★

10. Webpack 5 SplitChunksPlugin 的 cacheGroups 策略在 vendoring 与异步分割的工程价值

Webpack 5 SplitChunksPlugin 的 cacheGroups 策略如何设计?在 vendoring 与异步分割上有什么工程价值?

  • cacheGroups 的匹配、优先级与复用(reuseExistingChunk)语义
  • vendors 分组与异步 chunk 的拆分平衡
  • 缓存命中、请求数与体积的权衡治理

SplitChunksPlugin 通过 cacheGroups 定义拆分规则:每个 group 有 test(模块匹配)、priority(优先级)、minSize/maxSize、chunks(initial/async/all)、reuseExistingChunk 等,模块按规则归组;常用模式是 vendors 组(node_modules 依赖按框架/工具库分组合并,如 react 组、antd 组)与 async 组(异步 chunk 间共享的模块提出为公共异步 chunk)。vendoring 的价值:第三方库变化频率低,单独成 chunk 后可长期缓存,业务发布不使 vendor 缓存失效,同时避免每个页面 chunk 重复打包同一依赖。

异步分割的价值:多路由共享的异步模块抽成公共 chunk,避免重复下载与加载冗余;cacheGroups 的粒度需要权衡——分组过粗导致单 chunk 过大(首屏加载慢),过细导致请求数爆炸(HTTP 开销与缓存碎片化);工程实践上用"框架级分组(稳定)+ 业务级公共块(复用高)+ minSize 兜底"的分层策略,配合产物分析(bundle analyzer)与性能预算(chunk 数、最大 chunk 体积)驱动调整;Rspack 兼容同套配置语义,迁移时行为基本一致。

本题考察拆分策略的工程化:cacheGroups 是"规则引擎",vendoring 与异步分割是两大目标。回答应覆盖分组语义(priority、reuseExistingChunk)、分层策略与体积/请求数权衡,体现数据驱动的治理。

#
★★

11. Rollup manualChunks 与 Vite build.rollupOptions.output.manualChunks 的代码分割策略

Rollup 的 manualChunks 与 Vite 的 build.rollupOptions.output.manualChunks 如何配置代码分割?有哪些策略与注意点?

  • manualChunks 的对象/函数两种形态与语义
  • 依赖分组与缓存优化、循环依赖与边界问题
  • 与自动分割(SplitChunks 语义)的差异与配合

manualChunks 是 Rollup 的手动分块配置:对象形态({ 'vendor-react': ['react', 'react-dom'] })直接指定模块归组,函数形态(id => chunkName)按模块 id 动态归类,返回 undefined 则保持默认分割;Vite 通过 build.rollupOptions.output.manualChunks 透传。策略上常用"框架依赖分组"(react/vue 等稳定依赖单独 chunk 长缓存)、"工具库分组"(体积大且更新少的库)与"业务公共块"(多入口共享的源码模块),结合 minSize/maxSize 控制粒度。

注意点:一是手动分组必须自洽——同一模块不能被规则映射到多个 chunk,函数形态返回冲突时 Rollup 以首次为准或报错;二是循环依赖——强制分组可能把循环依赖的模块拆开产生循环 chunk 引用,运行时顺序异常,需要验证产物加载顺序;三是与自动分割的关系——manualChunks 是"指定",Rollup 仍会对未指定模块自动分包,二者互补;四是函数形态中访问 this.getModuleInfo 可基于模块信息做精细分组,但性能开销随模块数增长;五是切换 Rolldown 后 manualChunks 的实现差异需回归验证(chunk 归属算法不同)。

本题考察手动分割的配置细节:两种形态的语义、分组策略与循环依赖等边界问题。回答应强调"手动指定与自动分割互补"的认知,并提醒强制分组带来的循环 chunk 风险。

#
★★

12. HMR(Hot Module Replacement)原理与 React Fast Refresh

HMR(Hot Module Replacement)的原理是什么?React Fast Refresh 与普通 HMR 有何区别?

  • HMR runtime 的模块替换链路(accept、dispose、冒泡)
  • React Fast Refresh 的组件状态保留机制
  • HMR 边界失效与整页刷新的触发条件

HMR 的原理是"模块级热替换":开发服务器编译变更模块后,通过 WebSocket 推送更新,浏览器端 HMR runtime 沿模块依赖图找到最近的 accept 边界(import.meta.hot.accept / module.hot.accept),执行旧模块的 dispose 清理、加载新模块并调用 accept 回调完成替换;若找不到 accept 边界,冒泡至入口触发整页刷新。替换粒度是模块,状态保持依赖 accept 回调里对"非模块状态"(如 DOM、全局对象)的显式迁移。

React Fast Refresh 是 React 生态的 HMR 增强:它分析组件的变更类型——只变更组件函数体时保留组件状态(useState 值)原地重渲染;变更 hooks 数量/顺序等"结构"或非组件导出时回退到整模块刷新(避免状态错乱)。相比普通 HMR 的"模块级替换",Fast Refresh 做到了"组件级状态保留 + 安全降级":通过 react-refresh/babel 插件在编译期标记组件边界,runtime 记录模块注册的组件与 hooks 规则,更新时校验结构一致性。工程注意点:Fast Refresh 只在开发生效(babel 插件有条件开启)、对高阶组件/非组件导出保守、导出组件方式(具名导出与默认导出)影响保留行为,错误边界内报错时整树回退以保证可调试。

本题考察 HMR 的机制层次:通用链路(accept/dispose/冒泡)是基础,Fast Refresh 是"状态保留 + 安全降级"的框架级增强。回答应对比两者粒度差异,并说明结构变更回退的规则。

#
★★

13. 产物分析与 Bundle Analyzer 的体积治理

产物分析(Bundle Analyzer)如何实施?基于分析结果如何做体积治理?

  • Bundle Analyzer 类工具(webpack-bundle-analyzer、rollup-plugin-visualizer)的使用
  • 体积构成分析(依赖占比、chunk 分布、重复模块)
  • 体积预算、CI 门禁与持续治理机制

产物分析分三层:一是工具层,用 webpack-bundle-analyzer、rollup-plugin-visualizer 或 Vite 的 build.rollupOptions 配合可视化插件,生成依赖树与 chunk 占比图,识别"哪些依赖占了大部分体积、哪些模块被重复打包、哪些 chunk 异常大";二是数据层,配合构建产物 JSON(stats.json)做结构化解构——chunk 数量、最大 chunk、gzip/brotli 后体积、重复模块清单;三是机制层,把体积度量纳入 CI 门禁(如最大 chunk 超过 300KB 告警、总包体预算、与基线对比的 diff 检查)。

体积治理手段:按分析结果分类处理——第三方库过大(换轻量替代、按需引入、external + CDN 引入)、重复模块(依赖去重、共享 chunk)、业务代码冗余(摇树失效排查 sideEffects、删除死代码)、chunk 粒度失衡(manualChunks 收敛);持续治理上建立"体积预算"(budget)与"回归拦截"(CI 中体积超过阈值失败)、定期产物快照对比(每次发版记录体积趋势)、以及"依赖准入"流程(新增依赖需评估体积成本)。治理的核心是让体积变化"可观测、有预算、有门禁",而非一次性优化。

本题考察体积治理的方法论:分析(工具+数据)、决策(分类治理)、机制(预算+门禁)三层闭环。回答应体现"数据驱动、持续治理"而非"一次性压缩"的工程理念。

#
★★

14. Service Worker 缓存策略(Cache-first/stale-while-revalidate)与代码分割 chunk 的预缓存配合

Service Worker 的 Cache-first、stale-while-revalidate 等缓存策略如何与代码分割 chunk 的预缓存配合?

  • 各缓存策略(Cache-first、Network-first、SWR)的语义与适用场景
  • 构建产物清单(manifest)与 chunk 预缓存的生成
  • 版本更新、缓存清理与回退的工程治理

Service Worker 缓存策略:Cache-first 命中缓存直接返回(适合不可变资源,如带 contenthash 的 JS/CSS),Network-first 优先网络失败回退缓存(适合易变资源),stale-while-revalidate 先返回缓存同时后台更新(适合数据类接口与页面壳),配合 navigation 的 network-first 保证页面更新。对代码分割 chunk,工程实践是"构建期生成资源清单 + 安装时预缓存":构建产物中的每个 chunk(含异步 chunk)与其 contenthash URL 写入 precache manifest,SW 安装阶段全部预缓存,运行时异步 import 的 chunk 命中 Cache-first 策略,秒开切换路由而无需网络请求。

配合要点:一是版本管理——新构建产生新 manifest,SW 检测到更新后下载新资源,采用"旧版服务到新版本 ready"的缓存替换策略(activate 时清理旧缓存);二是异步 chunk 与预缓存范围——全量预缓存可能过度(大应用几百个 chunk),可分层:首屏与高频路由预缓存、低频 chunk 用运行时缓存(Cache-first + 兜底网络)或按需预缓存;三是回退——缓存缺失时回退网络、网络失败回退离线页;四是更新时机——避免 SW 长期占住旧版本导致线上新代码不生效,用 skipWaiting/clientsClaim 与更新检查策略治理。

本题考察 SW 缓存与构建产物的联动:策略语义是基础,"清单化预缓存 + 分层缓存"是工程关键。回答应覆盖策略选择、manifest 机制、版本更新与回退治理,体现离线优先架构的完整设计。

#
★★

15. 子资源完整性(SRI)与内容哈希文件名在 CDN 缓存与供应链安全的协作

子资源完整性(SRI)与内容哈希文件名如何协作?在 CDN 缓存与供应链安全上各承担什么角色?

  • SRI 的 hash 校验机制(integrity 属性与 CORS 前提)
  • contenthash 文件名与 CDN 缓存、防篡改的关系
  • 协作边界:防篡改 vs 防缓存失效、第三方资源的 SRI 应用

SRI(Subresource Integrity)在

本题考察供应链安全的两道防线:contenthash 解决"版本一致性",SRI 解决"内容真实性"。回答应说明机制(校验前提、CORS)、协作关系与第三方资源应用,体现对安全与缓存协同的理解。

#
★★

16. Webpack 5 Persistent Caching 与 Rspack 在中大型工程的现代取舍

Webpack 5 的 Persistent Caching(持久化缓存)与 Rspack 在中大型工程中如何取舍?

  • Webpack 5 cache 持久化的机制与失效粒度
  • Rspack 内置缓存的差异与收益
  • 中大型工程的选型依据(生态、性能、迁移成本)

Webpack 5 的持久化缓存(cache: { type: 'filesystem' })把模块解析、转译结果与 chunk 计算写入磁盘缓存,二次构建只重算变更部分,显著缩短 CI 与本地增量构建时间;缓存键基于配置、源码与依赖(lockfile)内容,失效粒度为模块级,但 JS 实现的编译管线仍是性能上限,大型项目冷构建与深度变更后的重建时间依然可观。Rspack 用 Rust 内核实现同类缓存(内存 + 持久化),冷构建与热构建性能均大幅优于 webpack,且内置缓存无需复杂调参。

中大型工程取舍:若项目深度依赖 webpack 生态(大量自定义插件、特殊 loader 链、内部工具集成),留在 Webpack 5 + 持久化缓存是低风险方案,收益足够;若追求构建性能数量级提升且插件清单可控,迁移 Rspack 收益更大,但需承担兼容层回归(插件行为、MF、HMR 细节)与双构建金丝雀验证成本;混合路径是"先启用 webpack 持久化缓存拿到增量收益,再评估迁移 Rspack"。现代取舍还应考虑 Vite/Rolldown 路线:偏 Vite 生态的新项目可直接选择 Rolldown 底座,存量 webpack 项目在 Rspack 与 webpack+缓存 之间按迁移成本决策。

本题考察构建性能方案的选型框架:持久化缓存是"存量优化",Rspack 是"引擎替换"。回答应对比机制与收益边界,并给出按生态依赖度、迁移成本与路线图(Rolldown)分层的决策建议。

#
★★

17. Terser/SWC minify 在产物大小与构建时间的工程取舍

Terser 与 SWC minify(压缩)在产物大小与构建时间上如何取舍?

  • Terser 的压缩能力与产物大小优化(tree-shake 后压缩、mangle、dead code)
  • SWC minify 的 Rust 实现带来的构建时间优势
  • 压缩级别(compress、mangle 选项)与产物正确性风险

Terser 是 JS 生态标准的压缩器:提供细粒度选项(compress 的各类优化、mangle 变量名压缩、toplevel 处理、dead code 消除、属性名压缩),压缩比高(配合 gzip 后更明显),但纯 JS 实现,大型项目压缩阶段耗时显著,成为构建时间的大头之一。SWC minify 用 Rust 实现同等级别的压缩能力(swc 的 minify API、Rspack 内置),产物大小与 Terser 接近(部分场景略大或略小),但压缩速度快数倍到十倍,且支持并行,可显著缩短整体构建时间。

取舍要点:一是产物大小优先的场景(对首屏体积敏感、包体预算严格的 CDN 环境)用 Terser 的完整 compress 配置(如 pure_funcs、drop_console 配合,但需注意 drop_console 影响日志排查);二是构建时间优先(大型 monorepo、频繁发版 CI)用 SWC minify/Rspack 内置压缩;三是正确性风险——压缩选项的激进优化(如 unsafe 系列、属性压缩、esmodule 处理)可能破坏特定代码(依赖函数名反射、eval、动态属性),需在产物验证(冒烟测试、样本对比)中覆盖;四是压缩与摇树、代码分割的配合顺序(先摇树后压缩),以及 source map 在压缩后的映射正确性。实践中可用"双压缩对比"(同源产物分别压缩,对比体积与构建时长)做数据化决策。

本题考察压缩器的工程取舍:Terser 赢体积、SWC 赢时间,选择取决于项目约束。回答应覆盖选项语义、正确性风险与"双压缩对比"决策方法,体现数据驱动。

#
★★

18. 动态 polyfill 与按需加载(@babel/preset-env useBuiltIns: 'usage')的取舍

动态 polyfill 与 @babel/preset-env 的 useBuiltIns: 'usage' 按需加载如何取舍?

  • useBuiltIns: 'usage' 的按需注入机制与局限(静态分析)
  • 动态 polyfill(Polyfill.io 类、特性检测 + 按需加载)的差异
  • 现代方案(core-js 按需、原生特性检测)与性能权衡

@babel/preset-env 的 useBuiltIns: 'usage' 根据 browserslist 目标与源码中实际用到的 API,按需注入对应 core-js polyfill(如用到 Array.prototype.includes 且目标浏览器不支持才注入),相比 entry 全量引入能显著减小 polyfill 体积;局限是"静态分析"——无法感知运行时动态特性(如变量调用方法、字符串拼接的 API)、无法覆盖第三方依赖内的 API 使用(依赖自身未声明 core-js 时),且注入发生在构建产物里,目标浏览器变化需重新构建。

动态 polyfill 方案(如 Polyfill.io、自建特性检测服务):浏览器运行时按 UA/特性检测动态返回需要的 polyfill 脚本,精确到每个浏览器,避免构建期"按最弱目标打包"的冗余;局限是引入第三方服务(供应链风险、性能抖动)与运行时延迟。现代取舍:多数项目选择 useBuiltIns: 'usage' + 精确 browserslist(构建期静态注入,可控可审计),仅在用户画像复杂或需要极致瘦身时用动态方案;另有"按需 + 原生检测"组合——业务代码用特性检测(if (!window.fetch) 再加载对应 polyfill chunk)实现运行时按需,作为构建期注入的补充。核心权衡是"构建期确定性 vs 运行时精确性",同时考虑 polyfill 服务的安全与性能成本。

本题考察 polyfill 注入策略的两条路线:构建期静态按需(useBuiltIns usage)与运行时动态按需。回答应对比机制与局限(静态分析盲区、供应链风险),给出组合方案与决策依据。

#
★★

19. 预构建(Pre-bundling)的依赖优化(Vite esbuild 预构建)

Vite 的依赖预构建(Pre-bundling)如何优化依赖加载?配置上有哪些注意点?

  • 预构建的三重收益(ESM 化、扁平化、请求收敛)
  • 缓存失效机制与 force 重建
  • 与生产构建的一致性边界与 exclude 兜底

Vite 预构建在 dev 启动时用 esbuild 扫描并打包依赖:一是把 node_modules 中的 CJS 依赖转换为浏览器可加载的 ESM(自动生成 interop 包装),解决"裸模块无法解析"与"CJS 不能在浏览器直接运行"两个问题;二是依赖扁平化——多个包共享的同一依赖(react 等)只保留一份实例,避免双实例与版本分裂;三是请求收敛——把数百个依赖合并为少量预构建产物,避免浏览器发起大量请求;结果缓存于 node_modules/.vite,缓存键基于 lockfile 与依赖配置。

注意点:一是缓存失效——lockfile 或 optimizeDeps 配置变化时自动失效重建,必要时用 vite optimize --force 强制重建(如依赖被外部修改、缓存异常);二是 include/exclude 边界——扫描遗漏的动态依赖用 include 声明,无法转换的依赖用 exclude 排除并配合 external 处理(exclude 后浏览器需能原生解析或经其他途径加载);三是 dev/build 一致性——预构建(esbuild)与生产构建(Rollup/Rolldown)的转换语义可能不同,CJS interop、条件导出等在两端要验证一致;四是预构建产物体积——依赖过多时预构建产物变大,可结合 esbuildOptions 的 chunk 策略(Vite 5.4+ 支持 splitting)控制。

本题考察 Vite 预构建的机制与治理:三重收益是"为什么",缓存与 include/exclude 是"怎么配",dev/build 一致性是"边界在哪"。回答应覆盖这三点并给出 force 重建等运维手段。

#

20. SSR/ISR 与 Edge/CDN 缓存组合,缓存控制头与页面级失效策略在部署架构的取舍

SSR/ISR 与 Edge/CDN 缓存如何组合?缓存控制头与页面级失效策略在部署架构上如何取舍?

  • SSR/ISR/静态化的缓存层级(CDN、Edge、源站)与缓存头语义
  • 页面级失效(revalidate、tag/purge)与 CDN 缓存的协作
  • 数据新鲜度与性能的取舍及监控治理

SSR/ISR 与 Edge/CDN 的组合本质是"多级缓存分层":源站负责渲染(SSR 实时或 ISR 增量生成),Edge/CDN 缓存页面响应,用 Cache-Control(s-maxage、stale-while-revalidate)声明各层缓存策略;ISR(增量静态再生成)让页面"静态化 + 定时/按需重建",配合 CDN 长缓存获得静态性能,同时用 revalidate 机制(Next.js 的 revalidate 秒数与 on-demand revalidation)控制新鲜度;Edge 缓存可把渲染结果缓存到离用户最近的节点,显著降低首字节延迟。

页面级失效策略的取舍:一是"时间驱动"(revalidate 定时失效)——实现简单、适合新鲜度容忍度高的页面,但可能过期;二是"事件驱动"(on-demand purge/tag)——内容变更时按标签精准失效(如 Next.js unstable_cache tag、CDN purge API),新鲜度好但需要触发链路与监控;三是"混合"——高频变化区块(用户态、实时数据)走 SSR 不缓存或短缓存,低频区块(文章、商品详情)走 ISR 长缓存,实现"按区块分级"的缓存架构。工程治理:缓存命中率、stale 服务时长(SWR 回退)、purge 成功率纳入监控,缓存头通过统一中间件生成避免散落,发版时配合版本化路径或 purge 避免旧缓存残留。

本题考察服务端渲染与缓存的架构设计:多级缓存分层 + 失效策略权衡是核心。回答应覆盖缓存头语义、ISR/SSR 分层、时间与事件两种失效模式的取舍,以及监控治理。

#

21. 持久化缓存(localStorage/IndexedDB)与版本迁移、容量清理在离线应用的工程实践

离线应用中 localStorage/IndexedDB 持久化缓存的工程实践是什么?版本迁移与容量清理如何设计?

  • localStorage 与 IndexedDB 的容量、同步/异步与适用场景差异
  • 缓存数据结构版本化与迁移(migration)策略
  • 容量上限(Quota)与清理(LRU、TTL、逐出)机制

持久化缓存选型:localStorage 同步 API、容量约 5-10MB、适合小数据(用户偏好、轻量缓存),会阻塞主线程;IndexedDB 异步、容量可达 GB 级(受 Quota 限制)、支持索引与事务,适合大量结构化数据(接口缓存、离线文档、媒体资源)。工程上"接口响应缓存、大数据集"用 IndexedDB,"设置项、会话标记"用 localStorage,且两者都需 try/catch 包裹(隐私模式、Quota 满时写入会抛错)。

版本迁移设计:给缓存结构定义 schemaVersion,读取时校验版本,不匹配则执行迁移(migrate 函数逐版本升级:数据结构转换、字段更名、重建索引),迁移失败回退清空重建——保证"旧数据兼容升级、脏数据不阻塞启动";容量清理设计:监听 Quota 满的写入失败与 navigator.storage.estimate() 的用量,实现逐出策略——按访问时间 LRU 淘汰、按过期时间 TTL 淘汰、按类型优先级(核心缓存保底、可重建缓存先清)、提供手动清理入口与"缓存降级"(容量不足时退化为内存缓存或禁用缓存);关键数据与缓存数据分离(缓存可丢、核心数据必须可靠落库或同步)。

本题考察离线缓存的运维化设计:选型(两种存储差异)、版本化(schemaVersion 迁移)、容量治理(estimate/LRU/TTL)是三大支柱。回答应体现"缓存可丢失、可重建、可迁移"的工程认知。