Files
AIGC/docs/performance-optimization-report.md
blandarebiter 90b5118e45 perf(backend+frontend): 列表API响应体积优化 3.1MB→145KB (↓95.4%)
- 后端: JPQL构造器投影排除LONGTEXT大字段(uploadedImages/videoReferenceImages)
- 后端: DTO层过滤非分镜图类型的base64内联resultUrl
- 前端: 列表缩略图从video改为img loading=lazy,消除172并发请求
- 前端: download函数增加resultUrl懒加载(详情接口兜底)
- 文档: 新增性能优化报告 docs/performance-optimization-report.md
2026-04-10 18:46:37 +08:00

6.1 KiB
Raw Permalink Blame History

AIGC 后端性能优化报告

日期2026-04-10
范围:列表 API 响应体积优化 + 前端缩略图加载优化


一、问题背景

"我的作品"页面加载缓慢172 条记录的 /api/works/my-works 接口响应体积达 3.1MB,导致:

  • 网络传输耗时长
  • 前端 JSON 解析阻塞主线程
  • 浏览器同时加载 172 个 <video> 元素,并发请求导致页面卡死

二、根因分析

2.1 后端:大字段未做投影

原 JPA 查询 SELECT uw FROM UserWork uw ... 加载全部列,包含多个 LONGTEXT 字段:

字段 类型 172 条总体积 说明
resultUrl LONGTEXT 2512 KB 7 条含 data:image/jpeg;base64,... 内联图片,单条最长 473KB
uploadedImages LONGTEXT 514 KB 用户上传的参考图 base64
videoReferenceImages LONGTEXT ~0 KB 视频参考图(当前数据为空)
其他字段合计 ~30 KB 正常

关键发现82% 的体积来自 7 条分镜图作品的 resultUrl,它们存储的不是 CDN URL 而是 base64 内联图片数据。

2.2 前端:缩略图加载策略低效

问题 影响
自定义 v-lazy 指令使用 IntersectionObserver + new Image() 预加载 172 个元素同时触发,创建 172 个并发图片请求
<video preload="metadata"> 用于列表缩略图 浏览器为每个视频卡片发起元数据请求,并发量爆炸
<video>poster 属性 preload="none" 后视频区域透明,露出 CSS 渐变背景

三、优化方案

3.1 后端JPQL 构造器投影SQL 层排除大字段)

原理:使用 SELECT new UserWorkListDTO(...) JPQL 语法,让 Hibernate 生成的 SQL 只 SELECT 需要的列,从数据库到 JVM 就不传输大字段。

改动文件

UserWorkListDTO.java

  • 新增全参数构造函数,供 JPQL 构造器投影使用
  • 排除 uploadedImagesvideoReferenceImages 字段
  • data: 开头的 base64 resultUrl 智能过滤STORYBOARD_IMAGE 类型保留,其他类型置 null

UserWorkRepository.java

  • 新增 11 个 JPQL 投影查询方法(方法名后缀 DTO),返回 Page<UserWorkListDTO> / List<UserWorkListDTO>
  • 覆盖所有列表查询场景:按用户名、按类型、按状态、公共作品、搜索、标签

UserWorkService.java

  • 所有列表查询方法返回类型从 Page<UserWork> 改为 Page<UserWorkListDTO>
  • getProcessingWorks 返回类型从 List<UserWork> 改为 List<UserWorkListDTO>

UserWorkApiController.java

  • 所有列表接口直接返回 DTO移除 stream().map() 转换
  • 增加 [PERF] 计时日志(保留),输出查询/统计/组装各阶段耗时

3.2 后端base64 resultUrl 智能过滤

// UserWorkListDTO 构造函数中
boolean isStoryboardImage = workType == UserWork.WorkType.STORYBOARD_IMAGE;
this.resultUrl = (!isStoryboardImage && resultUrl != null && resultUrl.startsWith("data:"))
    ? null : resultUrl;
  • 非分镜图类型data: 开头的超长 resultUrl 置为 null列表不需要
  • 分镜图类型:保留 base64 resultUrl它就是图片本身且无 thumbnailUrl
  • 详情/下载场景:通过 GET /api/works/{id} 详情接口获取完整数据

3.3 前端:下载函数懒加载适配

// MyWorks.vue / Profile.vue 的 download 函数
if (!item.resultUrl) {
  const detailResp = await getWorkDetail(item.id)
  const detail = detailResp?.data?.data || detailResp?.data
  if (detail?.resultUrl) {
    item.resultUrl = detail.resultUrl
  }
}

当列表 DTO 中 resultUrl 为 null 时,下载前自动通过详情接口获取完整值。

3.4 前端:缩略图加载策略重构

改动 之前 之后
图片缩略图 v-lazy:loading="item.cover" (自定义指令) <img :src="item.cover" loading="lazy"> (原生懒加载)
视频缩略图 <video :src preload="metadata"> <img :src="item.cover" loading="lazy"> (统一用封面图)
视频播放 列表卡片中 <video> 加载元数据 仅在详情弹窗中 <video> 播放

核心思路:列表网格只需要展示封面图(静态图片),不需要 <video> 元素。视频播放只在用户点击进入详情弹窗时才加载。


四、性能对比数据

测试条件

  • 用户984523799记录数172 条
  • 测试方法JVM 内部 A/B 对比3 轮热身取稳定值

结果

指标 优化前 优化后 改善
JSON 响应体积 3164 KB (3.1 MB) 145 KB ↓ 95.4%
DB 查询耗时 ~20 ms ~10 ms ↓ 50%
总接口耗时(含统计) 1245 ms
前端并发请求 172 个 video + 172 个 Image 按需原生懒加载 大幅减少

五、改动文件清单

后端Java

文件 改动类型 说明
dto/UserWorkListDTO.java 修改 全参构造函数 + base64 过滤逻辑
repository/UserWorkRepository.java 修改 新增 11 个 JPQL 投影查询方法
service/UserWorkService.java 修改 返回类型改为 DTO
controller/UserWorkApiController.java 修改 类型同步 + PERF 计时日志
resources/logback-spring.xml 还原 临时 DEBUG 日志已恢复为 WARN

前端Vue

文件 改动类型 说明
views/MyWorks.vue 修改 缩略图改用 <img loading="lazy">download 加懒加载
views/Profile.vue 修改 缩略图改用 <img loading="lazy">download 加懒加载

六、遗留与建议

项目 优先级 说明
分镜图 base64 存储问题 建议生成分镜图时上传 CDN存 URL 而非 base64可再减少 ~2.5MB
列表分页优化 当前默认 size=1000 加载全部,可改为前端虚拟滚动 + 按需加载
GZIP 压缩 确认 Nginx/Spring Boot 开启 GZIP145KB JSON 可压缩至 ~20KB
v-lazy 指令清理 directives/lazyLoad.js 已不再使用,可考虑删除