Files
yingqing/性能审计报告-AirShelf-2026-06-19.md
T
seaislee1209andClaude Opus 4.8 856a93b8a9 docs(audit): UI 还原 + 性能审计报告;修登录假动画延迟
- 新增 UI 还原对比报告 / 性能审计报告(2026-06-19,14 块页面 + 6 块性能,逐项 file:line)
- auth-screen: 删除登录成功后 ~1.5s 人为进度动画延迟,凭证验过即进

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 02:07:44 +08:00

161 lines
17 KiB
Markdown
Raw 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.
# AirShelf · 性能审计报告
> **范围:** 全量扫描 —— 前端(数据加载/水合、列表渲染、pipeline 重渲染、全站轮询)+ 后端(ORM N+1、端点耗时/payload、数据库索引)
> **背景:** 用户反馈刷新/进页面拉数据慢、不够丝滑;张业昌已优化过一波,仍有体感卡顿
> **日期:** 2026-06-19
> **方法:** 6 路并行审计,每块两端源码核查到 file:line
> **严重度:** P0 = 明显卡顿/秒级阻塞 · P1 = 可感知延迟 · P2 = 优化空间
---
## 〇、先说结论:"刷新慢/不丝滑"的真正元凶
按"改动成本 vs 体感收益"排序。**前两条是一行/小改就能立竿见影的,强烈建议先做。**
| # | 根因 | 类型 | 现状 | 一句话修复 | 收益 |
|---|---|---|---|---|---|
| 🔴 1 | **线上 DB 连接不复用** | 后端配置 | `production.py``CONN_MAX_AGE`,每请求重连远程 MySQL ~1.6s;并发 N 接口=N×1.6s。development 有这条优化,**线上没搬** | production.py 加 `CONN_MAX_AGE=300`+`CONN_HEALTH_CHECKS` | **刷新整页快几十秒→秒级** |
| 🔴 2 | **登录假动画延迟 1.5s** | 前端 | 凭证验过后还硬演 verified+entering 两段共 ~1.5s | **已修**(见本报告末) | 登录即时进 |
| 🔴 3 | **视频生成期 5s 全量回拉项目详情** | 前后端 | 有视频在跑时每 5s 调 `api.project()`,跑最重的 12-prefetch 序列化器、回 ~26KB,整页每 5 秒抖一下 | 轮询改用轻量 `poll-video-segment` 单段状态 | 视频阶段不再整页抖、payload 砍 99% |
| 🔴 4 | **消息中心首屏同步 N+1 爆炸** | 后端 | 进消息面板时 `list()` 里同步循环 aggregate+get_or_create,单次可达 **500~1000+ 条 SQL** | 批量聚合 + bulk_create + 改 Celery 异步 | 打开消息面板秒回 |
| 🔴 5 | **Asset 大表零索引** | 后端索引 | 全站最大增长表,资产库列表/计数/facets 全表扫 + filesort | 加 `(team,category,-created_at)` 等复合索引 | 资产多时数量级提速 |
| 🔴 6 | **pipeline 3100 行巨页不拆不 memo** | 前端 | 任一 state 变(打字/轮询/hover)reconcile 整棵树 | 5 个 stage 拆 memo 子组件 + 派生数据 useMemo | 输入/拖动恢复跟手 |
| 🟠 7 | **切走再回必重拉、无缓存** | 前端 | 换页即卸载,每次进页面从零拉、白骨架闪烁 | 加内存缓存 + stale-while-revalidate | 二次进页 0 等待 |
| 🟠 8 | **大页一次拉 200 条 + img 漏 lazy** | 前端 | 商品详情/图片工具 `pageSize:200`,任务中心缩图无 `loading=lazy` | 改服务端分页 + 补 lazy | 首屏字节/解码降一个量级 |
---
## 一、前端 · 数据加载与水合(7/10)
> 水合链路已认真优化过:骨架先出、`loadData` 并行 Promise.all、不阻塞首屏、liteRefresh、懒加载——核心卡顿点基本消除。剩下的是缓存与"小操作全量重拉"。
| 严重度 | 位置 | 问题 | 体感影响 | 修复建议 |
|---|---|---|---|---|
| P1 | 各路由 + `App.tsx` renderPage switch | **切走再回每次都重拉**:换页即卸载旧组件,`reloadFlag` 归零,资产库/消息/团队/消费/图片工具/商品详情/工作台进去都从零拉,无任何缓存 | 反复横跳每次白骨架闪烁,不丝滑 | 模块级内存缓存或 SWR:先渲染上次结果,再后台 revalidate |
| P1 | `App.tsx:387` `action()` | **几乎所有变更都触发全量 `loadData()`**(并发重拉 products+projects+billing+models+badge 五个);只有 pipeline 改/加/删分镜传了 liteRefresh | 改商品/传图/改资料后顶栏侧栏整体抖一下,且并发拉无关数据 | 给不动列表的操作普遍补 `liteRefresh`,或让调用方只刷相关切片 |
| P1 | `App.tsx:388` `action()` | **每次 action 无条件 `refreshProjectDetail()`**,即便不在 pipeline、操作与项目无关(改头像/充值/建商品都白拉 26KB 项目详情) | 非管线页做操作时白发一个大请求 | 仅 `page==="pipeline"` 或操作确属项目相关时才刷 |
| P2 | `ai-tools.tsx:94`/`products.tsx:551`/`:568` | `pageSize:200` 一次拉大页再前端筛/分页 | 资产多的团队首屏偏慢 | 改服务端分页 + 触底加载 |
| P2 | 站内点进管线 `App.tsx:759-786` | 详情未到时整页 loading,未与导航预取重叠 | 从项目列表点进管线有一次明显等待 | `openPipeline` 时即并行预取 `api.project(id)` |
✅ 已核实无问题:loadData 并行且不阻塞首屏、骨架态完整、搜索去抖到位、防连点到位、**无人为延迟卡流程**(除已修的登录)。
---
## 二、前端 · 列表与图片渲染(6.5/10)
> 分页/懒加载/去抖基础都对,但几处"大页一次拉 + 一次性渲染"和漏 lazy,会在素材多的真实账号上突然变卡。
| 严重度 | 位置 | 问题 | 体感影响 | 修复建议 |
|---|---|---|---|---|
| **P0** | 商品详情 `products.tsx:551` | 进详情一次拉该商品**最多 200 条**素材,前端再 filter+slice 只显示 12,200 条全进 state | 素材多的商品点进去明显"转一下" | 服务端按页拉(page+pageSize=12),"加载更多"走下一页 |
| **P0** | 图片工具任务中心 `ai-tools.tsx:314,349` | 结果缩图 `<img>` **漏 `loading="lazy"`**(同文件别处都加了)+ `:94` 一次拉 200 张,首屏并发几十张原图 | 进"图片生成"页一批图齐刷、带宽抖动 | 两处补 `loading="lazy"`,源数据改分页 |
| **P0** | 商品库/项目页 `App.tsx:141-142`+`api.ts:175,204` | `api.products()/projects()` 只取后端**第 1 页 20 条**,却被当"全量"做客户端搜索/筛选/分页 → 第 21 个起根本不在内存 | **搜不到/筛不到第 20 名以后的数据**,用户误判"卡了没加载" | products/projects 改服务端分页(像 library/messages 那样) |
| P1 | 图片工具模特网格 `ai-tools.tsx:568` | 一次拉 200 个模特进 state,点"全部模特"渲染全部缩图 | 模特库大时工作台首开+展开卡 | 默认拉前若干,"全部"走分页 |
| P1 | 商品详情视频 tab `products.tsx:643-646` | `videoProjects` 不分页不截断,一次渲染全部视频卡(assets tab 有 limit,videos 没有) | 项目多的商品切到视频 tab 一次铺满 | videos tab 也加 limit/分页 |
| P1 | 向导商品选择器 `projects.tsx:278-280` | 页码用 `Array.from({length:totalPages})` 全量展开,没用 pageWindow 折叠 | 商品多时页码一长条 | 复用 `pageWindow()` 折叠 |
| P2 | 商品库 `products.tsx:124-134` | 搜索纯客户端但每键触发整列表重算;`ProductCard` 未 memo,map 内联闭包 | 商品多时逐字搜索略顿 | ProductCard 包 memo,filtered 用 useMemo |
| P2 | 列表行未 memo / key 用 index | `projects.tsx:588`/`account.tsx:331,347` 内联闭包无 memo;`products.tsx:435,819` key=index | 当前每页 10 条无碍,数据增长后退化 | 列表项抽组件 memo,key 用稳定 id |
✅ 正向:六个页面 loading 都用骨架不白屏;messages/library/account 已是真服务端分页+去抖,messages 还做了滚动无限加载+防竞态。
---
## 三、前端 · pipeline 重渲染与轮询(5/10)
> 轮询清理/动画做得不错,但**单个 3100 行巨页 + 5s 全量回拉**让整页每 5 秒抖,后台 tab 也照拉不停。这是 pipeline"不丝滑"的核心。
| 严重度 | 位置 | 问题 | 体感影响 | 修复建议 |
|---|---|---|---|---|
| **P0** | `App.tsx:320-334`+`api.ts:207` | 视频 5s 轮询每轮 `api.project()` 拉**全量 ~26KB**再 `setProjectDetail`,无脏检查,即便没变也 set → 整页重渲染 | 有视频在跑时整页每 5 秒"刷一下",滚动/输入发顿 | 后端加轻量 `video-status` 端点;或 set 前对比版本号/JSON 跳过 |
| **P0** | `pipeline.tsx:445`(整组件)+`503-585` | 单个 3100 行函数持 60+ useState,5 阶段全在一棵树;`byId/scripts/shots/groups` 每次渲染重算(memo 远不够),组件无 memo 无拆分 | 任一 state 变(打字/轮询/hover)reconcile 整棵巨树,输入延迟 | 5 阶段拆 memo 子组件只渲当前 stage;派生数据包 useMemo |
| P1 | 全站轮询无 `visibilitychange` | 5s/8s/4s 轮询在**隐藏 tab/切后台时照跑**(全局 0 命中 document.hidden) | 切走后仍持续打接口,耗电耗流量,服务端空转 | poll tick 起始加 `if(document.hidden)return` |
| P1 | `pipeline.tsx:2076-2095` | stage2 `pendingAssets` 4s 轮询**空闲也永不停**,`list.length==0` 也照 setTimeout | 停在资产页啥也不干也每 4s 打接口 | 连续 N 轮空就停,有新提交再启 |
| P2 | `pipeline.tsx:84-89` `getAssetBlobUrl` | `createObjectURL` 进 module Map 缓存后**全程不 revoke**,跨项目累积 | 长会话内存缓慢上涨 | 切/卸载项目时 revoke + 清 Map |
✅ 已核实无问题:ProgressTimeline 的 1s interval 作用域小且 done 即停;两处 rAF 正确 gate+cleanup;5s/8s/120ms 轮询都在卸载时清理且按阶段 gate;CSS infinite 动画都是条件出现的 spinner/shimmer;无人为卡流程的 setTimeout。
---
## 四、后端 · ORM N+1 与序列化器(核心路径已治理,集中 2 个热点)
> 详情/列表核心路径已被认真优化(prefetch、defer 大 JSON 列、annotate 计数、轻量列表序列化器)。真正的放大点集中在消息中心首屏 + 写操作回吐全量。
| 严重度 | 位置 | 问题(估算多发 SQL) | 体感影响 | 修复建议 |
|---|---|---|---|---|
| **P0** | `apps/ops/views.py:69-235` `ensure_team_notifications`(在 `list()` 同步触发) | 首屏循环内:5 项目各 `credit_ledgers.aggregate()` + 拉最多 500 条 charge 流水逐条 `get_or_create`。**单次首屏 500~1000+ 条往返**,远程库几秒~几十秒 | 第一次开消息面板/换团队整页长转圈 | 项目花费 `values('project').annotate(Sum)` 批量;charge 通知批量查 dedupe_key + bulk_create;首屏走 Celery 异步 |
| P1 | `apps/ops/views.py:285-292` `type_counts` | **每次**列表请求额外 6 条 `.count()`(all/unread/task/team/billing/system) | 收件箱翻页每次多 6 个往返 | 一次 `values('notification_type').annotate(Count)`+单 unread count = 2 条 |
| P1 | `apps/projects/views.py` 写 action 一律回 `ProjectSerializer(project).data`(`:491/544/681/781` 等) | 每个采用版本/上传/存草稿按钮都回吐**整棵项目树**(几十~上百行),个别 action 未走 prefetch 还会 N+1 | 每次写操作后界面可感卡顿 + 回包巨大 | 只回受影响子对象(已有 VideoSegmentVersionSerializer 等);必须回全量则走带 prefetch 的重取 |
| P2 | `apps/projects/serializers.py:90-100` `BaseAssetGroupSerializer` | 线索核实:列表/详情**不 N+1**(queryset 已 prefetch adopted_asset/candidate/files)。仅 `adopt_base_asset` 单独序列化未预取的 group 时每组 +2~3 条 | 采用基础资产后单次轻微多查 | 单对象序列化前补 select_related/prefetch |
| P2 | `apps/assets/views.py:98-118` summary/facets | 资产库徽标 6 条 count、facets 多条 distinct(固定不放大) | 进资产库一组并发 count | 可合成一条 `aggregate(Count(Case(When)))` |
✅ 已核实无问题:项目列表用 ProjectListSerializer+annotate 无 N+1;详情/商品/AITask/资产列表均已 select_related/prefetch_related + defer 大 JSON;billing/team_members 已批量化。
---
## 五、后端 · 端点耗时与 payload(7/10)
| 严重度 | 位置 | 问题 | 体感影响 | 修复建议 |
|---|---|---|---|---|
| **P0** | `settings/production.py`(缺 `CONN_MAX_AGE`)vs `development.py:10-13` | **线上 MySQL 连接未复用**,每请求新建连接 ~1.6s 握手。开发环境设了 300s 复用并注明"登录并发 12 接口=卡几十秒",**这条优化只在 development 生效** | 线上每接口多 1.6s,一进页面并发 N 请求=N×握手 | production.py 加 `CONN_MAX_AGE=300`+`CONN_HEALTH_CHECKS=True`(提到 base.py 更稳) |
| **P0** | `App.tsx:324,331`+`pipeline.tsx:1876``projects/views.py:118` | 任一视频段 running 时**每 5s 调 `api.project`**,触发 12-prefetch 最重序列化器,把全部版本/分镜/资产/timeline 一次塞响应 | 视频阶段持续拉巨 payload + 整页重渲 | 轮询改用已有轻量 `poll-video-segment`(views.py:546)只回单段状态;或详情加 ETag→304 |
| P1 | `projects/views.py:278`+`assets/review.py:111`,前端 `pipeline.tsx:816`(8s) | `poll-reviews` 在**请求线程里同步循环调火山外部 HTTP**(每个审核中资产一次),前端每 8s 打一次 | N 个审核资产时单次请求阻塞=N×火山往返 | 火山审核轮询挪 Celery 后台,端点只读 DB review_status |
| P1 | `apps/ai/views.py:68` `AITaskViewSet` | 未设显式 `ordering`,分页可能重/漏行 | 任务多时前端反复拉 | 加 `ordering=["-created_at"]` |
| P2 | `apps/ops/views.py:276` list | 每次额外 6 条 count(同上 N+1 P1) | 无高频轮询,影响有限 | 合并 count |
| P2 | `apps/billing/views.py:182` trend | "按阶段分布"用 Python 循环累加本月全部流水,非 DB 聚合 | 流水多时账户页加载慢 | 改 `values('task__task_type').annotate(Sum)` |
✅ 已核实无问题:全局分页健全(PAGE_SIZE=20,无全量返回);assets/AITask/billing 已 defer 3MB+ JSON 大列(注释"40 条要 30s+"已修);poll-storyboard 后台线程不阻塞;通知生成已 Celery+60s 缓存。
---
## 六、后端 · 数据库索引(7/10)
> 基表 `team` FK 自带单列索引,纯 `filter(team=)` 不慢。真正缺的全是"team + 第二过滤列/时间排序"的**复合索引**。远程 MySQL 上数据量上来后,缺索引的过滤/排序会明显变慢。
| 严重度 | 表/字段 | 问题 | 体感影响 | 加什么索引 |
|---|---|---|---|---|
| **P0** | **Asset** `(team,category,-created_at)` 等 | `assets/models.py:6` 整表**零自定义索引**,资产库每 tab `filter(team,category).order_by(-created_at)` + 6 个 count + facets distinct。Asset 是全站最大增长表 | 资产库刷新/切 tab/徽标计数,大表全表扫+filesort 秒级卡 | `Index(["team","category","-created_at"])` + `(team,asset_type)` + 视需要 `(team,source)` |
| **P0** | **Asset** `(team,-created_at)` | 默认无筛选排序也要 team 内取全部再 filesort | 默认视图随资产数线性变慢 | `Index(["team","-created_at"])`(或被上条复合覆盖) |
| P1 | **CreditLedger** `(team,-created_at)` | 流水分页 `filter(team).order_by(-created_at)`,现有索引不覆盖。Ledger 高频写入 | 账单流水翻页随增长变慢 | `Index(["team","-created_at"])` |
| P1 | **CreditLedger** `created_at` 范围 | 消费趋势 `filter(team,ledger_type=CHARGE,created_at__date__gte)`+TruncDate 分组无法走索引 | 账户消费分析月度数据多时慢 | `(team,ledger_type)` 扩成 `(team,ledger_type,created_at)` |
| P1 | **AITask** `(team,-created_at)` | 无显式 ordering + 无时间序索引 | 任务列表分页不稳+filesort | `Index(["team","-created_at"])` + ViewSet `ordering` |
| P1 | **Project** `(team,-updated_at)` | 列表 `filter(team).order_by(-updated_at)`,索引不含 updated_at | 项目列表/侧栏随项目数变慢 | `Index(["team","-updated_at"])` |
| P1 | **Product** `(team,-created_at)` | 列表无显式 ordering + 时间序索引 | 商品列表分页随商品数变慢 | `Index(["team","-created_at"])` + 默认 ordering |
| P2 | **Asset** `metadata__product_id` | 按商品过滤走 JSON 提取 + 三路 Q 并集 distinct,无法走普通索引 | 商品详情"该商品资产"大表下慢 | MySQL 函数/生成列索引,或冗余真实 `product_id` 列 + `(team,product_id)` |
> ⚠️ 远程 MySQL 大表 `ALTER TABLE ADD INDEX` 会锁表,建议低峰执行或用 online DDL。Notification 表(`ops/models.py:52`)索引已做得很好,可作其它表范本。
---
## 七、已修复
- **登录假动画延迟(🔴2)** —— [auth-screen.tsx](core/frontend/src/routes/auth-screen.tsx) 已删除 `api.login()` 成功后的 verified/entering 两段共 ~1.5s 人为 `wait`,以及 checking 阶段的最小垫时。现在凭证验过即进,"进入"态由真实的 `onAuthed`(身份+数据水合)驱动,不再演动画。
---
## 八、建议执行顺序(按 ROI)
### 第一波 · 一行/小改,立竿见影(强烈建议先做)
1. **🔴1 production.py 加 `CONN_MAX_AGE`** —— 消掉线上每请求 1.6s 握手,"刷新慢"最大单点
2. **🔴3 视频 5s 轮询改轻量端点** —— 别再每 5s 拉全量项目详情
3. **🔴8 两处 `<img>``loading="lazy"`** + 商品详情/图片工具改服务端分页
4. **隐藏 tab 暂停轮询**(poll 加 document.hidden 守卫)
### 第二波 · 后端查询治理
5. **🔴4 消息中心首屏 N+1** —— 批量聚合 + bulk_create + Celery 异步
6. **🔴5 Asset/Ledger/Project/AITask 加复合索引**(低峰执行 DDL)
7. **poll-reviews 火山同步 HTTP 挪 Celery**
8. **写 action 不再回吐全量 ProjectSerializer**
### 第三波 · 前端体验
9. **🔴6 pipeline 巨页拆 memo 子组件**(工作量大,但根治 pipeline 不丝滑)
10. **🟠7 切走再回加内存缓存 + SWR**
11. **action() 普遍补 liteRefresh + 条件化 refreshProjectDetail**
---
## 九、客观说明
- **张业昌确实优化过一波且方向对**:骨架先出、并行水合、defer 大 JSON 列、列表轻量序列化器、prefetch、防竞态、去抖——这些都到位,不是没做。
- **剩下的卡顿高度集中**:① 一条线上配置漏搬(CONN_MAX_AGE)② 视频轮询拉全量 ③ 消息中心首屏 N+1 ④ Asset 缺索引 ⑤ pipeline 巨页。**修掉前 4 个(都不算大改),"刷新慢"基本能解决**;pipeline 巨页拆分是更大的工程,可单独排期。
- 本报告每条都带 file:line,可直接当修复 checklist 交给张业昌认领。