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

153 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:` 开头的 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 智能过滤
```java
// 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 前端:下载函数懒加载适配
```javascript
// 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` 已不再使用,可考虑删除 |