xiaolongmr commited on
Commit
36b3bb0
·
1 Parent(s): 9340872

shan chu tupian yuchong xiazai, chidu gengxin an zhen he bing

Browse files
web/src/app/batch/components/image-results.tsx CHANGED
@@ -6,7 +6,7 @@ import { Clock3, Download, LoaderCircle, Pencil, RotateCcw, ScanSearch, Sparkles
6
  import { ImageShareEvidenceDialog } from "@/app/batch/components/image-share-evidence-dialog";
7
  import { ChatGptIcon } from "@/components/chatgpt-icon";
8
  import { Button } from "@/components/ui/button";
9
- import { downloadImageSrc, normalizeImageUrl, warmImageBlob } from "@/lib/image-url";
10
  import { cn } from "@/lib/utils";
11
  import type { ImageConversation, ImageTurnStatus, StoredImage, StoredReferenceImage } from "@/store/image-conversations";
12
 
@@ -395,9 +395,9 @@ export function ImageResults({
395
  event.currentTarget.naturalWidth,
396
  event.currentTarget.naturalHeight,
397
  );
398
- if (image.url && !image.b64_json) {
399
- warmImageBlob(image.url);
400
- }
401
  }}
402
  />
403
  {shareEvidenceEnabled && image.conversationId ? (
 
6
  import { ImageShareEvidenceDialog } from "@/app/batch/components/image-share-evidence-dialog";
7
  import { ChatGptIcon } from "@/components/chatgpt-icon";
8
  import { Button } from "@/components/ui/button";
9
+ import { downloadImageSrc, normalizeImageUrl } from "@/lib/image-url";
10
  import { cn } from "@/lib/utils";
11
  import type { ImageConversation, ImageTurnStatus, StoredImage, StoredReferenceImage } from "@/store/image-conversations";
12
 
 
395
  event.currentTarget.naturalWidth,
396
  event.currentTarget.naturalHeight,
397
  );
398
+ // ⚠️ 同 image-results.tsx:不要在这里 warmImageBlob。
399
+ // 跨域直链会被 fetch 的 CORS 拦下并回落到后端 /api/images/proxy,
400
+ // 让容器替每个访客把整张图重下一遍,图片反而更慢。
401
  }}
402
  />
403
  {shareEvidenceEnabled && image.conversationId ? (
web/src/app/batch/page.tsx CHANGED
@@ -48,7 +48,7 @@ import {
48
  type RedeemCode,
49
  type ThirdPartyImageModelOption,
50
  } from "@/lib/api";
51
- import { fetchImageBlob, releaseImageBlob, warmImageBlob } from "@/lib/image-url";
52
  import { formatChinaDateTime, formatChinaShortDateTime } from "@/lib/time";
53
  import { useAuthGuard } from "@/lib/use-auth-guard";
54
  import { Input } from "@/components/ui/input";
@@ -541,11 +541,8 @@ function taskDataToStoredImages(image: StoredImage, task: ImageTask): StoredImag
541
  durationMs: task.duration_ms,
542
  }];
543
  }
544
- resultItems.forEach((item) => {
545
- if (item.url && !item.b64_json) {
546
- warmImageBlob(item.url);
547
- }
548
- });
549
  return resultItems.map((item, index) => ({
550
  ...image,
551
  id: index === 0 ? image.id : `${image.id}-extra-${index}`,
 
48
  type RedeemCode,
49
  type ThirdPartyImageModelOption,
50
  } from "@/lib/api";
51
+ import { fetchImageBlob, releaseImageBlob } from "@/lib/image-url";
52
  import { formatChinaDateTime, formatChinaShortDateTime } from "@/lib/time";
53
  import { useAuthGuard } from "@/lib/use-auth-guard";
54
  import { Input } from "@/components/ui/input";
 
541
  durationMs: task.duration_ms,
542
  }];
543
  }
544
+ // ⚠️ 同 image/page.tsx:不要在这里预热 warmImageBlob(item.url)。
545
+ // 预热会与 <img> 抢并发连接,跨域失败还会让容器走 /api/images/proxy 重下一遍。
 
 
 
546
  return resultItems.map((item, index) => ({
547
  ...image,
548
  id: index === 0 ? image.id : `${image.id}-extra-${index}`,
web/src/app/image/components/image-results.tsx CHANGED
@@ -6,7 +6,7 @@ import { Clock3, Download, LoaderCircle, Pencil, RotateCcw, ScanSearch, Sparkles
6
  import { ImageShareEvidenceDialog } from "@/app/image/components/image-share-evidence-dialog";
7
  import { ChatGptIcon } from "@/components/chatgpt-icon";
8
  import { Button } from "@/components/ui/button";
9
- import { downloadImageSrc, normalizeImageUrl, warmImageBlob } from "@/lib/image-url";
10
  import { cn } from "@/lib/utils";
11
  import type { ImageConversation, ImageTurnStatus, StoredImage, StoredReferenceImage } from "@/store/image-conversations";
12
 
@@ -166,6 +166,11 @@ export function ImageResults({
166
  }, []);
167
 
168
  const [imageDimensions, setImageDimensions] = useState<Record<string, string>>({});
 
 
 
 
 
169
  const [continueEditLoadingImageIds, setContinueEditLoadingImageIds] = useState<string[]>([]);
170
  const [annotationLoadingImageIds, setAnnotationLoadingImageIds] = useState<string[]>([]);
171
  const [now, setNow] = useState(() => Date.now());
@@ -186,9 +191,36 @@ export function ImageResults({
186
 
187
  const updateImageDimensions = (id: string, width: number, height: number) => {
188
  const dimensions = formatImageDimensions(width, height);
189
- setImageDimensions((current) => current[id] === dimensions ? current : { ...current, [id]: dimensions });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  };
191
 
 
 
 
 
 
 
 
 
 
192
 
193
  const continueEdit = async (
194
  conversationId: string,
@@ -395,9 +427,14 @@ export function ImageResults({
395
  event.currentTarget.naturalWidth,
396
  event.currentTarget.naturalHeight,
397
  );
398
- if (image.url && !image.b64_json) {
399
- warmImageBlob(image.url);
400
- }
 
 
 
 
 
401
  }}
402
  />
403
  {shareEvidenceEnabled && image.conversationId ? (
 
6
  import { ImageShareEvidenceDialog } from "@/app/image/components/image-share-evidence-dialog";
7
  import { ChatGptIcon } from "@/components/chatgpt-icon";
8
  import { Button } from "@/components/ui/button";
9
+ import { downloadImageSrc, normalizeImageUrl } from "@/lib/image-url";
10
  import { cn } from "@/lib/utils";
11
  import type { ImageConversation, ImageTurnStatus, StoredImage, StoredReferenceImage } from "@/store/image-conversations";
12
 
 
166
  }, []);
167
 
168
  const [imageDimensions, setImageDimensions] = useState<Record<string, string>>({});
169
+ // 把「每张图 onLoad 各触发一次 setState」合并成「每帧最多一次」。
170
+ // 一份会话常有几十张结果图,若逐张 setState,每加载完一张就要重渲染整份轮次列表
171
+ // (几十个子树 diff),主线程被占满,视觉上就是「图一直在转、点哪都迟钝」。
172
+ const pendingDimensionsRef = useRef<Record<string, string>>({});
173
+ const dimensionsRafRef = useRef<number | null>(null);
174
  const [continueEditLoadingImageIds, setContinueEditLoadingImageIds] = useState<string[]>([]);
175
  const [annotationLoadingImageIds, setAnnotationLoadingImageIds] = useState<string[]>([]);
176
  const [now, setNow] = useState(() => Date.now());
 
191
 
192
  const updateImageDimensions = (id: string, width: number, height: number) => {
193
  const dimensions = formatImageDimensions(width, height);
194
+ pendingDimensionsRef.current[id] = dimensions;
195
+ if (dimensionsRafRef.current !== null) {
196
+ return;
197
+ }
198
+ dimensionsRafRef.current = window.requestAnimationFrame(() => {
199
+ dimensionsRafRef.current = null;
200
+ const pending = pendingDimensionsRef.current;
201
+ pendingDimensionsRef.current = {};
202
+ setImageDimensions((current) => {
203
+ let next: Record<string, string> | null = null;
204
+ for (const [key, value] of Object.entries(pending)) {
205
+ if (current[key] !== value) {
206
+ next = next || { ...current };
207
+ next[key] = value;
208
+ }
209
+ }
210
+ return next || current;
211
+ });
212
+ });
213
  };
214
 
215
+ useEffect(() => {
216
+ return () => {
217
+ if (dimensionsRafRef.current !== null) {
218
+ window.cancelAnimationFrame(dimensionsRafRef.current);
219
+ dimensionsRafRef.current = null;
220
+ }
221
+ };
222
+ }, []);
223
+
224
 
225
  const continueEdit = async (
226
  conversationId: string,
 
427
  event.currentTarget.naturalWidth,
428
  event.currentTarget.naturalHeight,
429
  );
430
+ // ⚠️ 这里**不要**再加 `warmImageBlob(image.url)`。
431
+ // 图片是 ChatGPT 官网签名直链(跨域),<img> 已经把它完整下载过一遍;
432
+ // 再 fetch 一次会走 fetchImageBlobUncached,跨域被 CORS 拦下后回落到
433
+ // 后端 `/api/images/proxy`,让只有 2 核的容器替每个访客把整张图重下一遍。
434
+ // 一屏 6~9 张图 ⇒ 雪崩式排队,且浏览器对同域名的 6 条并发连接被这些
435
+ // 无用请求占满,<img> 反而要等——这就是「画图页图片加载慢、右键新标签页
436
+ // 打开却很快」的直接原因(新标签页不跑这段 JS,也不与轮询抢连接)。
437
+ // 下载 / 加入参考都是用户主动点击,届时 fetchImageBlob 自己会取,功能不变。
438
  }}
439
  />
440
  {shareEvidenceEnabled && image.conversationId ? (
web/src/app/image/page.tsx CHANGED
@@ -48,7 +48,7 @@ import {
48
  type RedeemCode,
49
  type ThirdPartyImageModelOption,
50
  } from "@/lib/api";
51
- import { fetchImageBlob, releaseImageBlob, warmImageBlob } from "@/lib/image-url";
52
  import { formatChinaDateTime, formatChinaShortDateTime } from "@/lib/time";
53
  import { useAuthGuard } from "@/lib/use-auth-guard";
54
  import { Input } from "@/components/ui/input";
@@ -534,11 +534,11 @@ function taskDataToStoredImages(image: StoredImage, task: ImageTask): StoredImag
534
  durationMs: task.duration_ms,
535
  }];
536
  }
537
- resultItems.forEach((item) => {
538
- if (item.url && !item.b64_json) {
539
- warmImageBlob(item.url);
540
- }
541
- });
542
  return resultItems.map((item, index) => ({
543
  ...image,
544
  id: index === 0 ? image.id : `${image.id}-extra-${index}`,
 
48
  type RedeemCode,
49
  type ThirdPartyImageModelOption,
50
  } from "@/lib/api";
51
+ import { fetchImageBlob, releaseImageBlob } from "@/lib/image-url";
52
  import { formatChinaDateTime, formatChinaShortDateTime } from "@/lib/time";
53
  import { useAuthGuard } from "@/lib/use-auth-guard";
54
  import { Input } from "@/components/ui/input";
 
534
  durationMs: task.duration_ms,
535
  }];
536
  }
537
+ // ⚠️ 这里**不要**再加「任务成功就 warmImageBlob(item.url)」的预热。
538
+ // 结果图是 ChatGPT 官网签名直链(跨域),预热会与紧接着的 <img> 请求**同时**发车:
539
+ // 跨域 fetch 被 CORS 拦下后回落到后端 `/api/images/proxy`,等于让容器替每个访客
540
+ // 把整张图重下一遍,还占掉浏览器对同域名的并发连接。渲染时 <img> 自己会拉,
541
+ // 用户点「下载 / 加入参考」时 fetchImageBlob 也会拉,预热纯属多余负担。
542
  return resultItems.map((item, index) => ({
543
  ...image,
544
  id: index === 0 ? image.id : `${image.id}-extra-${index}`,
web/src/lib/image-url.ts CHANGED
@@ -169,9 +169,30 @@ export async function fetchImageBlob(src: string) {
169
  return entry.promise;
170
  }
171
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  export function warmImageBlob(src: string) {
173
  if (!String(src || "").trim()) return;
174
- void fetchImageBlob(src).catch(() => undefined);
 
 
 
 
 
 
175
  }
176
 
177
  export function releaseImageBlob(src: string) {
 
169
  return entry.promise;
170
  }
171
 
172
+ /**
173
+ * 预取图片为 Blob,供「下载 / 加入参考」等需要字节流的操作用。
174
+ *
175
+ * ⚠️⚠️ **绝对不要在图片列表的渲染 / onLoad 路径里批量调用它**(2026-10-16 事故)。
176
+ * 结果图是 ChatGPT 官网签名直链(跨域),`<img>` 本来就要完整下载一遍;这里再 fetch
177
+ * 一次,跨域被 CORS 拦下后会回落到后端 `/api/images/proxy`,让只有 2 核的容器替**每个
178
+ * 访客**把整张图(1~3MB)重下一遍;同时这些无用请求会占满浏览器对同域名仅有的 6 条
179
+ * 并发连接,`<img>` 反而要排队——症状就是「画图页图片加载慢,右键新标签页打开却很快」。
180
+ * 下载/加入参考都是用户主动点击,届时 fetchImageBlob 自己会取并缓存,无需预热。
181
+ *
182
+ * 保留在飞上限作为兜底:万一将来又被挂到批量路径上,最多只并行 2 个,不会雪崩。
183
+ */
184
+ const MAX_INFLIGHT_WARMS = 2;
185
+ let inflightWarms = 0;
186
+
187
  export function warmImageBlob(src: string) {
188
  if (!String(src || "").trim()) return;
189
+ if (inflightWarms >= MAX_INFLIGHT_WARMS) return;
190
+ inflightWarms += 1;
191
+ void fetchImageBlob(src)
192
+ .catch(() => undefined)
193
+ .finally(() => {
194
+ inflightWarms = Math.max(0, inflightWarms - 1);
195
+ });
196
  }
197
 
198
  export function releaseImageBlob(src: string) {