From a1fd24a5f61f062f17828b88a7d2c58e6db52ebf Mon Sep 17 00:00:00 2001 From: Stringadmin Date: Fri, 5 Jun 2026 19:11:43 +0800 Subject: [PATCH] perf: reduce repeated collection traversal --- src/api/assetClient.ts | 8 ++++++- src/api/communityClient.ts | 10 ++++++--- src/api/keyServerClient.ts | 19 ++++++++++------ src/api/modelCapabilitiesClient.ts | 10 ++++++--- src/api/projectTaskClient.ts | 21 ++++++++++++++---- src/components/AppShell.tsx | 3 ++- src/features/canvas/CanvasPage.tsx | 11 +++++++--- src/features/community/communityCaseUtils.ts | 6 ++++- src/features/ecommerce/EcommercePage.tsx | 23 +++++++++++++++----- src/features/more/MorePage.tsx | 11 ++++++---- 10 files changed, 90 insertions(+), 32 deletions(-) diff --git a/src/api/assetClient.ts b/src/api/assetClient.ts index 049fd1f..954835e 100644 --- a/src/api/assetClient.ts +++ b/src/api/assetClient.ts @@ -67,7 +67,13 @@ function normalizeAssetStatus(value: unknown): WebAssetItem["status"] { } function normalizeTags(value: unknown): string[] { - return Array.isArray(value) ? value.map((item) => toStringValue(item)).filter(Boolean) : []; + if (!Array.isArray(value)) return []; + const tags: string[] = []; + for (const item of value) { + const tag = toStringValue(item); + if (tag) tags.push(tag); + } + return tags; } function normalizeAsset(raw: unknown): ServerAssetItem { diff --git a/src/api/communityClient.ts b/src/api/communityClient.ts index 0000bf2..706fb2a 100644 --- a/src/api/communityClient.ts +++ b/src/api/communityClient.ts @@ -62,9 +62,13 @@ function toStringValue(value: unknown, fallback = ""): string { } function toStringArray(value: unknown): string[] { - return Array.isArray(value) - ? value.map((item) => toStringValue(item)).filter(Boolean) - : []; + if (!Array.isArray(value)) return []; + const result: string[] = []; + for (const item of value) { + const text = toStringValue(item); + if (text) result.push(text); + } + return result; } function toMetadata(value: unknown): Record { diff --git a/src/api/keyServerClient.ts b/src/api/keyServerClient.ts index 151a551..777d46e 100644 --- a/src/api/keyServerClient.ts +++ b/src/api/keyServerClient.ts @@ -376,13 +376,18 @@ function createWorkflowFingerprint(workflow: WebCanvasWorkflow): string { function toActivePackages(value: unknown): WebUserSession["user"]["activePackages"] { if (!Array.isArray(value)) return undefined; - return value.filter(isRecord).map((entry) => ({ - name: toStringValue(entry.name, "Preview package"), - expiresAt: toStringValue(entry.expiresAt ?? entry.expires_at, ""), - remainingImage: toNumber(entry.remainingImage ?? entry.remaining_image), - remainingVideo: toNumber(entry.remainingVideo ?? entry.remaining_video), - remainingText: toNumber(entry.remainingText ?? entry.remaining_text), - })); + const packages: NonNullable = []; + for (const entry of value) { + if (!isRecord(entry)) continue; + packages.push({ + name: toStringValue(entry.name, "Preview package"), + expiresAt: toStringValue(entry.expiresAt ?? entry.expires_at, ""), + remainingImage: toNumber(entry.remainingImage ?? entry.remaining_image), + remainingVideo: toNumber(entry.remainingVideo ?? entry.remaining_video), + remainingText: toNumber(entry.remainingText ?? entry.remaining_text), + }); + } + return packages; } function normalizeUser(raw: unknown): WebUserSession["user"] | null { diff --git a/src/api/modelCapabilitiesClient.ts b/src/api/modelCapabilitiesClient.ts index 33ce1f4..7d284d0 100644 --- a/src/api/modelCapabilitiesClient.ts +++ b/src/api/modelCapabilitiesClient.ts @@ -49,9 +49,13 @@ function normalizeModelOption(raw: unknown): ModelCapabilityOption | null { } function normalizeModelList(value: unknown): ModelCapabilityOption[] { - return Array.isArray(value) - ? value.map(normalizeModelOption).filter((item): item is ModelCapabilityOption => Boolean(item)) - : []; + if (!Array.isArray(value)) return []; + const options: ModelCapabilityOption[] = []; + for (const item of value) { + const option = normalizeModelOption(item); + if (option) options.push(option); + } + return options; } function createFallbackCapabilities(): WebModelCapabilities { diff --git a/src/api/projectTaskClient.ts b/src/api/projectTaskClient.ts index efe56e3..65c96aa 100644 --- a/src/api/projectTaskClient.ts +++ b/src/api/projectTaskClient.ts @@ -71,10 +71,19 @@ function normalizeTask(raw: unknown): ServerProjectTask | null { } function extractTasks(payload: unknown): ServerProjectTask[] { - if (Array.isArray(payload)) return payload.map(normalizeTask).filter(Boolean) as ServerProjectTask[]; + const normalizeTasks = (rows: unknown[]): ServerProjectTask[] => { + const tasks: ServerProjectTask[] = []; + for (const row of rows) { + const task = normalizeTask(row); + if (task) tasks.push(task); + } + return tasks; + }; + + if (Array.isArray(payload)) return normalizeTasks(payload); if (!isRecord(payload)) return []; const rows = payload.tasks ?? payload.items; - return Array.isArray(rows) ? (rows.map(normalizeTask).filter(Boolean) as ServerProjectTask[]) : []; + return Array.isArray(rows) ? normalizeTasks(rows) : []; } function taskTitle(task: ServerProjectTask): string { @@ -110,8 +119,12 @@ export const projectTaskClient = { }, async listForProjects(projectIds: string[]): Promise { - const uniqueIds = Array.from(new Set(projectIds.map((id) => id.trim()).filter(Boolean))); - const results = await Promise.all(uniqueIds.map((id) => listProjectTasks(id))); + const uniqueIds = new Set(); + for (const projectId of projectIds) { + const id = projectId.trim(); + if (id) uniqueIds.add(id); + } + const results = await Promise.all(Array.from(uniqueIds, (id) => listProjectTasks(id))); return results.flat(); }, diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index a647f40..ec66e9b 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -106,8 +106,9 @@ function AppShell({ const visibleNavItems = useMemo( () => { + const navItemByKey = new Map(navItems.map((item) => [item.key, item])); return PRIMARY_NAV_ORDER - .map((key) => navItems.find((item) => item.key === key)) + .map((key) => navItemByKey.get(key)) .filter((item): item is WebNavItem => Boolean(item)); }, [navItems], diff --git a/src/features/canvas/CanvasPage.tsx b/src/features/canvas/CanvasPage.tsx index afb0b55..fb7bc8b 100644 --- a/src/features/canvas/CanvasPage.tsx +++ b/src/features/canvas/CanvasPage.tsx @@ -3529,9 +3529,14 @@ function CanvasPage({ return; } const toDelete = selectedNode ? [selectedNode] : selectedNodes; - const textIds = new Set(toDelete.filter((n) => n.kind === "text").map((n) => n.id)); - const imageIds = new Set(toDelete.filter((n) => n.kind === "image").map((n) => n.id)); - const videoIds = new Set(toDelete.filter((n) => n.kind === "video").map((n) => n.id)); + const textIds = new Set(); + const imageIds = new Set(); + const videoIds = new Set(); + for (const node of toDelete) { + if (node.kind === "text") textIds.add(node.id); + else if (node.kind === "image") imageIds.add(node.id); + else if (node.kind === "video") videoIds.add(node.id); + } if (textIds.size) setTextNodes((nodes) => nodes.filter((n) => !textIds.has(n.id))); if (imageIds.size) setImageNodes((nodes) => nodes.filter((n) => !imageIds.has(n.id))); if (videoIds.size) setVideoNodes((nodes) => nodes.filter((n) => !videoIds.has(n.id))); diff --git a/src/features/community/communityCaseUtils.ts b/src/features/community/communityCaseUtils.ts index 18c8cd0..5dd7026 100644 --- a/src/features/community/communityCaseUtils.ts +++ b/src/features/community/communityCaseUtils.ts @@ -91,7 +91,11 @@ export function getCommunityCaseSurface(item: Pick tag.trim()).filter(Boolean); + const tags: string[] = []; + for (const rawTag of item.tags) { + const tag = rawTag.trim(); + if (tag) tags.push(tag); + } if (tags.some((tag) => tag.includes("生成页面社区") || tag === "Web生成")) return "generation"; if (tags.some((tag) => tag.includes("画布页面社区") || tag.includes("工作流"))) return "canvas"; if (getWorkflowFromCase(item)) return "canvas"; diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index d47b853..a32b086 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -576,6 +576,7 @@ const cloneSetCountOptions: Array<{ { key: "white", title: "白底图", desc: "白底主图,多角度呈现商品细节" }, { key: "scene", title: "场景图", desc: "展示商品的生活使用场景和人物搭配" }, ]; +const cloneSetCountKeys = cloneSetCountOptions.map((option) => option.key); const defaultCloneSetCounts: Record = { selling: 3, white: 1, @@ -1634,7 +1635,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const generatedUrls: string[] = []; const stamp = Date.now(); - for (const countKey of cloneSetCountOptions.map((o) => o.key)) { + for (const countKey of cloneSetCountKeys) { const count = counts[countKey]; for (let i = 0; i < count; i++) { if (imageAbortRef.current.current) break; @@ -1910,7 +1911,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { platform, ratio, language, market, { gender: modelGender, age: modelAge, ethnicity: modelEthnicity, body: modelBody, appearance, scenes: selectedScenes, smartScene }, (s) => setTryOnStatus(s as TryOnStatus), - (res) => setTryOnResultImages(res.map((r) => r.src).filter(Boolean)), + (res) => { + const urls: string[] = []; + for (const item of res) { + if (item.src) urls.push(item.src); + } + setTryOnResultImages(urls); + }, ); lastFailedActionRef.current = () => handleTryOnGenerate(); }; @@ -2030,7 +2037,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { productImages.length === 0 ? "请先上传商品原图" : status === "generating" ? "生成中..." : `生成${selectedCloneOutput.label}`; const setPreviewCards: CloneResult[] = []; let setIndex = 0; - for (const countKey of cloneSetCountOptions.map((o) => o.key)) { + for (const countKey of cloneSetCountKeys) { const count = cloneSetCounts[countKey]; const info = setCountLabels[countKey]; for (let i = 0; i < count; i++) { @@ -2045,7 +2052,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const clonePreviewCards: CloneResult[] = []; let cloneIndex = 0; - for (const countKey of cloneSetCountOptions.map((o) => o.key)) { + for (const countKey of cloneSetCountKeys) { const count = cloneSetCounts[countKey]; const info = setCountLabels[countKey]; for (let i = 0; i < count; i++) { @@ -2057,6 +2064,12 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { cloneIndex++; } } + const detailSourcePreviewImages = detailProductImages.length + ? detailProductImages.reduce((urls, item) => { + urls.push(item.src); + return urls; + }, []) + : detailProductSamples; const cloneBasicSelects: Array<{ key: CloneBasicSelectKey; label: string; @@ -2555,7 +2568,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
- {(detailProductImages.length ? detailProductImages.map((item) => item.src) : detailProductSamples).map((src, index) => ( + {detailSourcePreviewImages.map((src, index) => (
{`商品原图
diff --git a/src/features/more/MorePage.tsx b/src/features/more/MorePage.tsx index 7f24631..f92b63e 100644 --- a/src/features/more/MorePage.tsx +++ b/src/features/more/MorePage.tsx @@ -13,7 +13,7 @@ import { VideoCameraOutlined, } from "@ant-design/icons"; import type { ReactNode } from "react"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import "../../styles/pages/more.css"; import type { WebImageWorkbenchTool, WebViewKey } from "../../types"; @@ -144,9 +144,12 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) { return t.category === filter; }); - const recentTools = recentIds - .map((id) => tools.find((t) => t.id === id)) - .filter((t): t is MoreTool => Boolean(t) && (t?.ready ?? false)); + const toolById = useMemo(() => new Map(tools.map((tool) => [tool.id, tool])), []); + const recentTools = recentIds.reduce((acc, id) => { + const tool = toolById.get(id); + if (tool?.ready) acc.push(tool); + return acc; + }, []); const groupedTools = filteredTools.reduce>((acc, t) => { if (!acc[t.category]) acc[t.category] = [];