项目:xiaoye-admin-react | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
本文梳理框架中与性能相关的所有设计决策:代码分割与 chunk 策略、页面缓存、Mock 启动的可用性权衡、发版自愈机制、构建产物结构与测量手段,并给出经过源码核实的优化建议。部署侧的缓存策略(Nginx immutable / no-cache)与 PWA 配置见《06-部署运行》4 与 5.3,本文专注前端侧并补全两者的联动链。
首屏的关键路径是:index.html(内联 #boot-loading 占位,跟随系统明暗,杜绝 JS 加载期白屏)→ 入口 JS → main.tsx 预置登录态(开发态)与 MSW 启动(有 5 秒竞速上限)→ Provider 链渲染 → 路由守卫 → 权限初始化 → 动态路由懒加载目标页 chunk。这条链路上有两个刻意的"性能换可用性"决策需要先理解:其一,MSW 启动被放在渲染前 await(防止首屏请求漏网直连真实网络),但加了 5 秒竞速上限——在隐私模式、禁用 Service Worker 的环境里,等待是有界的,宁可放弃 mock 也要把页面渲染出来;其二,权限初始化在生产态要等两个接口返回才重建路由,菜单未就绪期间 * 路由渲染 Loading 而非 404,用户感知为"骨架期"而非"报错期"。这两处都是"不追求极限首屏数字,追求确定性体验"的取舍,评估性能问题时不要把它们当缺陷。
src/routers/helper/ConvertRouter.tsx 顶部的 import.meta.glob("/src/views/**/*.tsx") 把全部约 60+ 个视图声明为独立懒加载 chunk,每个路由元素包 lazy() 与 Suspense(src/components/Lazy/index.tsx 的 PageLoader)。这意味着首屏只会下载"当前访问页面"的业务代码,其余页面按需加载——对一个含 77 个菜单节点的框架而言,这是最基本也最有效的一刀分割。glob 的两个硬约束(不支持别名、必须以 / 开头)与键匹配失败时的降级(渲染 Page Not Found 占位而非崩溃)见《07-模块详解》1.2。
一个已知的构建告警值得解释:INEFFECTIVE_DYNAMIC_IMPORT——login 页同时被 staticRouter.tsx 静态导入与 glob 动态导入,Rollup 无法将其单独分包(README.md "已知构建提示"一节)。它不影响功能,登录页代码会随入口加载,这反而合理:登录是首屏必经页。
vite.config.ts 的 manualChunks 把三类最重的第三方库独立成 chunk:vendor-react(react/react-dom/react-router)、vendor-antd(antd + @ant-design)、vendor-echarts(echarts + zrender)。源码注释写明动机是长期缓存命中率:业务代码高频迭代,vendor 基本不动,分包后每次发版用户只需重新下载变化的业务 chunk,vendor 命中浏览器缓存。产物命名(assets/js/[name]-[hash].js,其余资源按 assets/[ext]/ 分类)保证内容寻址,与 Nginx 的 immutable 一年缓存策略配套(见《06-部署运行》5.3)。chunkSizeWarningLimit: 1500 把告警阈值放宽到 1.5MB,因为 vendor-antd 全量引入必然超默认值——这是已知且被接受的(README.md 明确),优化路径见第 7 节。
另一条隐性的体积纪律来自 echarts:src/components/Echarts/config/index.ts 走 echarts/core 按需注册(只引入用到的图表与组件),而不是全量 import * as echarts。新增图表类型时记得去注册表补对应模块,否则图表不渲染——这是"按需引入"的代价面。
传输层还有三处容易被忽略的小设计。其一,VITE_DROP_CONSOLE 在四个 mode 均为 true,生产产物移除 console(与 ESLint 的 no-console 规则配合:源码只允许 warn/error,两者形成"开发留痕、生产瘦身"的闭环)。其二,静态资源按 assets/[ext]/ 分目录输出,配合 Nginx 的 gzip(level 5,JS/CSS/JSON/SVG 等类型,nas-deploy/nginx-default.conf),文本类资源传输体积再压一层;VITE_BUILD_COMPRESS 预压缩产物(.gz/.br)当前默认关闭,开启路径见第 7 节。其三,图标体系的三轨(iconfont 字体、assets/icons 的 SVG、@ant-design/icons)中,SVG 与 iconfont 走的是"引用即打包"的路径,新增图标时注意别引入整包字体文件——历史版本曾有 1.5MB 字体因 font.css 未被引用而全废的案例(见《13-技术债务与演进建议》D8),体积纪律要落到资源引用的每一环。
src/components/KeepAlive/index.tsx 采用"缓存节点常驻 DOM、display:none 隐藏"方案,性能收益是切页零重渲染成本:切回缓存页时组件状态、表单输入、滚动位置、effects 完整保留,不重新请求数据、不重建 DOM。相比卸载-恢复方案,它把"切换成本"转移成了"驻留内存成本",因此框架配了两条内存回收线:开启多标签时缓存集合与 tabsList 联动(关标签即释放缓存,keyAlive 判定,动态详情路由经 matchPath 按 meta.activeMenu 归属到父 tab);meta.isKeepAlive 未开启的页面根本不进缓存。源码注释还解释了为什么不使用 React 19 的 <Activity>:其 hidden 态会卸载 effects 并推迟提交,与懒加载 Suspense、rc-tabs 动画叠加时曾导致切页空白等故障——这是实测后的方案锁定,不建议"升级"回 Activity。缓存页的内存驻留意味着:给 isKeepAlive 打开的页面如果是超长列表或大型图表,驻留成本需要自行评估,不是所有页面都该开缓存。
框架对"常驻 DOM"方案的配套是严格的清理纪律,Echarts 封装(src/components/Echarts/index.tsx)是范本:实例挂载前 getInstanceByDom 防重复 init;卸载时 off("click") + dispose() + observer.disconnect() 成对清理;更新走单一 setOption 路径且传 notMerge: true(源码注释:防轮询场景 series 变短时旧 series 残留——这是历史审计 P2-5 的修复)。请求层的全屏 loading 用计数器管理并发(src/components/Loading/fullScreen.tsx,"第一个开、最后一个关",卸载顺序先 root.unmount() 再移除 DOM 节点),避免并发请求下遮罩闪烁与 DOM 泄漏。列表页统一的 latestRequestId ref 防竞态(慢请求返回不覆盖新请求结果)也是性能与正确性的交界设计。
Mock 启动的性能设计在 src/main.tsx 的 enableMocking(),核心是三重有界等待。第一重,5 秒竞速超时:worker.start() 与一个 5 秒 reject 定时器 Promise.race,Service Worker 注册在隐私模式/禁 SW/部分无头环境可能无限挂起,若不设界会白屏——超时后放弃 mock、控制台 console.warn 告警、页面照常渲染。第二重,渲染前 await:启动必须先于 createRoot,否则首屏的菜单/权限请求会赶在拦截生效前漏到真实网络;这是一笔明确的"首屏延迟换正确性"交易,正常环境下 SW 注册是毫秒级,交易几乎免费。第三重,接管缺口自愈:SW 首次安装存在 clients.claim() 时序缺口(worker 已激活但当前页面未被控制,navigator.serviceWorker.controller 为 null),表现为请求全部穿透并报"你所访问的资源不存在";启动逻辑等 300ms 复查,仍未接管就整页刷新一次,sessionStorage 键 xiaoye-msw-claim-reload 防异常环境无限刷新循环。三重的共同哲学是:任何一重失败都不阻塞渲染——宁可功能缺失(无 mock),不可整站不可用。
发版后旧 chunk 被服务器删除,停留在旧页面的在线用户切路由时会拿到加载失败,表现为白屏。框架对此的三层自愈在 src/components/ErrorBoundary/index.tsx:isChunkLoadError 用错误名与两类消息正则识别分片失败;"重试"按钮通过自增 retryCount 改变 children 的 key 强制卸载重挂载(源码注释:直接重置 hasError 无效,React.lazy 会缓存已 reject 的 promise);处于 chunk 错误态时监听 online 事件,恢复联网即 window.location.reload() 整页刷新拉取新版资源。
这套前端自愈与部署侧缓存策略是配套设计而非独立功能:index.html 设 no-cache(发版后存活标签页下次整页加载必得新版入口,引用到新 hash 资源),/assets/ 设 immutable 一年(哈希寻址永不变脏),PWA 的 navigateFallback: "index.html" 让离线深链也能落到缓存的入口(build/plugins.ts,navigateFallbackDenylist 排除 /api/ 接口请求)。完整链条是:发版 → 在线用户下次刷新拿到新 index.html → 引用新 chunk 正常加载;停留旧页的用户切页触发 ChunkLoadError → ErrorBoundary 提示重试或自动刷新。任何"用户反映页面是旧版"的问题沿"index.html 缓存头 → SW 缓存 → 容器是否重启"三处排查(bind-mount 指向 inode,见《06-部署运行》5.2 纪律)。
测量入口一:VITE_REPORT=true 构建(build/plugins.ts 的 rollup-plugin-visualizer)产出 stats.html,含各模块原始/gzip/brotli 体积与依赖归属,是分析"谁把 bundle 撑大"的第一工具,改动分割策略前后应各跑一次对比。测量入口二:产物目录直接观察——dist/assets/js/ 下按名字即可辨认三 vendor 与各页面 chunk 的相对体量。历史实测数据(docs/bug-audit.md 记录):vendor-antd 约 2.2MB、vendor-echarts 约 778KB(按需引入后),业务侧曾出现的超大项是大屏页的 570KB 中国地图 chunk(981KB geoJSON 静态打包)与 519KB 的 bg.png——注意 views/dataScreen 已在当前版本移除,这些数字仅作方法参考:先用 stats.html 定位大头,再判断是"按需引入缺失""静态资源未压缩"还是"数据未走接口"。
已知且接受的体积项:vendor-antd 超 1.5MB 告警(antd 全量引入 + Tree-shaking 受限部分);PWA 预缓存因此把 workbox 的 maximumFileSizeToCacheInBytes 放宽到 4MiB(build/plugins.ts 注释点名 vendor-antd)。构建期注入(vite.config.ts define)不产生运行时成本:__APP_INFO__ 与 __COMMIT_HASH__ 编译进产物供"关于"页与 Footer 展示。
以下建议均对应当前源码中真实存在的优化空间,按投入产出排序。一,压缩产物开启:.env.production 的 VITE_BUILD_COMPRESS 当前为 none,插件链已就绪(vite-plugin-compression),生产部署若能配 Nginx gzip_static/brotli_static 直接服务预压缩文件,改为 gzip,brotli 即可,零代码改动。二,antd 引入面治理:vendor-antd 2.2MB 的主因是全量引入,若追求极致可改 antd 官方按需方案(v6 的 ES module 已利于 tree-shaking,重点排查 @ant-design/icons 的桶导出——import { XOutlined } from "@ant-design/icons" 全量图标进包,改深路径或按需注册收益显著),配合 stats.html 验证。三,路由级预加载:当前 lazy 页面在点击时才加载,弱网下有感知延迟;低成本方案是在菜单 hover 时预热(import("../../views/xxx") 触发 chunk 请求),不建议改回静态导入。四,sourcemap 策略:生产 sourcemap: false 保持不变;若要排产线问题,改用构建后上传 Sentry 的 hidden source maps,不要直接放开。五,PWA 预缓存体量:4MiB 上限意味着每次发版全量预缓存更新,弱网设备首访偏重;可评估 navigateFallback 保留而 runtimeCaching 细分策略。六,不要做的优化:不要给 vendor-react/vendor-antd 再细分(chunk 碎片化会降低命中率);不要移除 MSW 的 5 秒超时"提速"(它换的是可用性);不要把 KeepAlive 改回 <Activity>(实测有回归,见第 3 节)。
四条收束。一,新视图必须落在 src/views/**——它同时是路由注册与代码分割的前提,放到别处等于退出懒加载体系。二,新增重组件(图表、编辑器)先查按需注册与清理配对:init/dispose、observe/disconnect、on/off 必须成对,参照 Echarts 封装。三,列表页复用 latestRequestId 防竞态范式(src/views/list/useProTable/index.tsx),禁止裸 await setState。四,改分割策略前跑 VITE_REPORT=true 留基线,改完对比——性能工作没有测量就没有发生。
最后补充一个评估性能问题时的心法:本框架的性能设计大多是"防御性"而非"压榨性"的——5 秒 MSW 超时、30 秒请求超时、ChunkLoadError 自愈、KeepAlive 内存回收线,这些机制的共同目标不是让快的时候更快,而是让慢的时候和坏的时候仍然可用。因此给它做性能优化时,正确顺序是先测量(stats.html 与产物目录)、再区分"真实瓶颈"与"告警噪音"(vendor-antd 的 1.5MB 告警属后者),最后才动结构;跳过测量直接优化的改动,在这个以自愈机制密集著称的代码库里,破坏面往往比收益面大。