You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

6.5 KiB

ADR 003: 分镜看板基于 orderIndex 的性能优化

状态

提议中 (Proposed)

背景

当前分镜看板基于时间轴(startTime/endTime)布局,存在以下问题:

  • 布局计算复杂(需要处理重叠、间隙)
  • 跳转定位困难(需要计算累积时间)
  • 不适合虚拟滚动(时间不连续)

决策

采用基于 orderIndex 序号排序 + 虚拟滚动分页的方案。

核心设计理念

抛开时间轴概念

  • 分镜按 orderIndex 顺序排列(紧密相连,无间隙)
  • 宽度由 duration 决定,位置由 orderIndex 决定
  • 使用虚拟滚动只渲染可见分镜

架构设计

1. 数据分层

Layer 1: 骨架数据(Skeleton)
├─ 用途:计算总宽度、跳转定位
├─ 数据:id, orderIndex, duration
└─ 大小:200 分镜 ≈ 5-10 KB

Layer 2: 分页数据(Paginated)
├─ 用途:虚拟滚动渲染
├─ 数据:按 orderIndex 范围加载(每页 50 个)
└─ 大小:50 分镜 ≈ 25-30 KB

2. API 设计

骨架数据接口(新增)
GET /api/v1/projects/{project_id}/storyboard-board/skeleton

响应

{
  "projectId": "xxx",
  "storyboardCount": 200,
  "tracks": [
    {
      "type": "storyboard",
      "items": [
        {
          "id": "xxx",
          "orderIndex": 0,
          "duration": 3.0
        },
        {
          "id": "yyy",
          "orderIndex": 1,
          "duration": 5.0
        }
      ]
    }
  ]
}

数据量:200 分镜 ≈ 5-10 KB

分页数据接口(新增)
GET /api/v1/projects/{project_id}/storyboard-board/paginated
  ?startIndex=0
  &endIndex=49
  &trackTypes=storyboard,resource

响应

{
  "items": [
    {
      "id": "xxx",
      "type": "storyboard",
      "orderIndex": 0,
      "duration": 3.0,
      "data": {
        "title": "分镜 1",
        "thumbnail_url": "...",
        "resources": {...}
      }
    }
  ]
}

数据量:50 分镜 ≈ 25-30 KB

3. 前端架构

虚拟滚动实现
import { VariableSizeList } from 'react-window';

// 1. 加载骨架数据
const { data: skeleton } = useStoryboardBoardSkeleton(projectId);

// 2. 计算每个分镜的宽度
const getItemSize = (index: number) => {
  const item = skeleton.items[index];
  return item.duration * pps; // pps = pixels per second
};

// 3. 虚拟滚动渲染
<VariableSizeList
  height={600}
  itemCount={skeleton.items.length}
  itemSize={getItemSize}
  width="100%"
  layout="horizontal"
>
  {({ index, style }) => (
    <StoryboardItem
      index={index}
      style={style}
      data={getItemData(index)}
    />
  )}
</VariableSizeList>
分页加载策略
// 计算可见范围的 orderIndex
const calculateVisibleRange = (scrollLeft: number, containerWidth: number) => {
  let accumulatedWidth = 0;
  let startIndex = 0;
  let endIndex = 0;

  for (let i = 0; i < skeleton.items.length; i++) {
    const itemWidth = skeleton.items[i].duration * pps;

    if (accumulatedWidth <= scrollLeft && startIndex === 0) {
      startIndex = i;
    }

    if (accumulatedWidth <= scrollLeft + containerWidth) {
      endIndex = i;
    } else {
      break;
    }

    accumulatedWidth += itemWidth;
  }

  return { startIndex, endIndex };
};

// 加载可见范围的数据
const { data: visibleItems } = useStoryboardBoardPaginated(
  projectId,
  visibleRange.startIndex,
  visibleRange.endIndex
);
跳转定位实现
// 点击分镜列表时跳转到对应分镜
const scrollToStoryboard = (storyboardId: string) => {
  // 1. 找到分镜的 orderIndex
  const targetItem = skeleton.items.find(item => item.id === storyboardId);
  if (!targetItem) return;

  // 2. 计算前面所有分镜的宽度总和
  let scrollLeft = 0;
  for (let i = 0; i < targetItem.orderIndex; i++) {
    scrollLeft += skeleton.items[i].duration * pps;
  }

  // 3. 滚动到目标位置
  listRef.current?.scrollTo(scrollLeft);
};

4. 性能优化

预加载策略
// 预加载相邻页(前后各 25 个分镜)
const preloadRange = {
  startIndex: Math.max(0, visibleRange.startIndex - 25),
  endIndex: Math.min(skeleton.items.length - 1, visibleRange.endIndex + 25)
};

useStoryboardBoardPaginated(projectId, preloadRange.startIndex, preloadRange.endIndex);
缓存策略
// TanStack Query 缓存配置
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 分钟
      cacheTime: 10 * 60 * 1000, // 10 分钟
      keepPreviousData: true, // 保留上一页数据,避免闪烁
    },
  },
});

实施计划

阶段 1:后端 API(2-3 天)

  • 新增骨架数据接口 /storyboard-board/skeleton
  • 新增分页数据接口 /storyboard-board/paginated
  • 添加响应压缩(gzip)

阶段 2:前端基础架构(2-3 天)

  • 实现骨架数据加载
  • 实现分页数据加载
  • 实现数据缓存管理

阶段 3:虚拟滚动(3-4 天)

  • 集成 react-window(水平虚拟滚动)
  • 实现可见范围计算
  • 实现跳转定位功能

阶段 4:优化和测试(2-3 天)

  • 性能测试(1000+ 分镜)
  • 跳转定位测试
  • 用户体验优化

总计:9-13 天

性能目标

指标 当前 目标
首次加载时间 2-5s <300ms
骨架数据量 300-500 KB <10 KB
分页数据量 - <30 KB
内存占用 随分镜数增长 恒定 ~50 MB
滚动 FPS 30-40 60
跳转定位时间 - <100ms
支持分镜数 <200 1000+

优势

  1. 架构简单:无需复杂的时间轴计算
  2. 性能卓越:虚拟滚动 + 分页加载
  3. 跳转快速:基于 orderIndex 直接计算位置
  4. 易于维护:逻辑清晰,代码简洁
  5. 可扩展性强:支持大规模数据

劣势

  1. 需要重构:现有时间轴逻辑需要调整
  2. 开发周期:预计 2 周
  3. 测试成本:需要大量测试

与 ADR 002 的对比

维度 ADR 002(时间轴) ADR 003(orderIndex)
架构复杂度
实施难度
性能 优秀 卓越
跳转定位 复杂 简单
开发周期 2-3 周 1.5-2 周

推荐:ADR 003(orderIndex 方案)

参考资料