1. 现象与触发条件 (Symptoms & Trigger)

  • 环境:SPA 前端 + Node.js 后端 + SQLite/JSON 存储 + Nginx 反向代理。
  • 触发条件:

    1. 内容生成流水线(如 LLM / 批量脚本)在生成富交互组件或文档块时,将生成出来的图片直接以 data:image/png;base64,... 的形式拼装在组件的 HTML/JS 属性内。
    2. 列表查询接口(如 GET /api/courses)将包含整套课程所有步骤的整份结构一并序列化下发。
  • 表面现象:

    • 线上特定学段(如「小学低年段」)打开关卡地图时,长时间停留在「本地图已开孔,课程尚未加入」空状态提示,长达数十秒甚至数分钟。
    • 弱网、移动设备或 Playwright 自动化测试出现 30 秒超时(Timeout 30000ms exceeded)。
    • 控制台出现前端状态回退:超时后捕获异常,将课程列表置空 (setCourses([]))。

2. 根因分析 (Root Cause)

  1. 载荷体积几何级膨胀:

    • 原始结构中仅有 9 门课的互动实验包含了 74 张未外置的 base64 图片,每张图片体积在 300KB ~ 1.2MB 之间。
    • 最终导致单一学段的 JSON 载荷体积达到 40.4 MB(Content-Length: 40438886)。
  2. 公网传输瓶颈:

    • 40MB 的文本载荷在公网/云服务器网络下,即使在宽带良好的环境下传输也耗时 1~2 分钟。
    • 在移动端或弱网下直接触发浏览器的 Fetch Timeout,导致前端拉取彻底失败。
  3. 空状态时机与状态机缺陷:

    • React 初始状态 courses = [],当 courses.length === 0 时 UI 直接渲染「课程尚未加入」的空提示。在 40MB 数据漫长的下载过程中,用户持续面对空提示并误以为「服务器上没有数据」。
  4. 移动端存储与跨桥崩溃风险:

    • 客户端带有「本地优先」缓存逻辑,若将 40MB 的 JSON 字符串写入 LocalStorage 会触发 QuotaExceededError;若传给原生混合应用(Android WebView JSBridge)会导致内存暴增甚至崩溃。

3. 解决方案 (Resolution Steps)

3.1 资产外置化 (Externalization of Assets)

  • 编写清洗脚本(如 scripts/externalize_artifact_images.mjs),扫描全部课程 JSON 与 SQLite 数据库。
  • 提取超过阈值(如 20KB)的 base64 图片数据并解码为二进制 Buffer。
  • 按内容哈希生成唯一文件名(art-${hash}.${ext}),统一落盘到静态资源目录 public/assets/artifacts/。
  • 将 JSON 中的 base64 替换为相对路径 /assets/artifacts/art-${hash}.${ext}。
  • 效果:

    • primary-lower 课程 JSON 从 40.4 MB 骤降至 2.1 MB(缩减 95%)。
    • primary-upper 课程 JSON 从 10.6 MB 降至 1.6 MB(缩减 85%)。

3.2 动静分离与长效缓存 (Dynamic-Static Separation & Caching)

  • 静态资源通过 Nginx / CDN 直发,并配置 Cache-Control: public, max-age=31536000。
  • 图片按需在学生具体学到该关卡、iframe 渲染时才被浏览器拉取,且具备强缓存能力。
  • API 接口仅下发轻量级的元数据与代码逻辑,网络传输由分钟级下降为百毫秒级。

3.3 路由别名与无鉴权一致性 (Public Route Alias)

  • 针对前端误调 /api/settings/public 报 401 的问题:

    • 后端统一将 /api/settings/public 映射为 /api/system/settings/public 别名,且同时支持 GET 与 HEAD 方法。
    • 确保未登录访客在首次加载、预检探测时不会误入拦截闸门。

4. 下次如何快速识别 (Takeaways)

  1. 抓包查看 Content-Length:

    • 当接口返回缓慢或超时时,第一时间 curl -sI 查看返回的 Content-Length。
    • 如果一个 API 列表接口的 Content-Length 超过 2MB,必然存在内联媒体、超长 Base64 或全表未分页的架构隐患。
  2. 规范内容生成流水线:

    • 自动化或 AI 生成的素材绝不可直接以 Base64 塞进数据库字段,必须有配套的「落盘 - 寻址 - 引用」管线。
  3. 空状态前置判定:

    • 前端组件的「暂无数据」状态展示,必须严格区分 isLoading 与真正无数据的状态,避免在数据拉取阶段闪烁或常驻错误提示。

本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Web