- 后端: JPQL构造器投影排除LONGTEXT大字段(uploadedImages/videoReferenceImages) - 后端: DTO层过滤非分镜图类型的base64内联resultUrl - 前端: 列表缩略图从video改为img loading=lazy,消除172并发请求 - 前端: download函数增加resultUrl懒加载(详情接口兜底) - 文档: 新增性能优化报告 docs/performance-optimization-report.md
6.1 KiB
6.1 KiB
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 构造器投影使用
- 排除
uploadedImages和videoReferenceImages字段 - 对
data:开头的 base64resultUrl智能过滤(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% |
| 总接口耗时(含统计) | — | 12–45 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 开启 GZIP,145KB JSON 可压缩至 ~20KB |
v-lazy 指令清理 |
低 | directives/lazyLoad.js 已不再使用,可考虑删除 |