From 95dc2f24e61b40ad210666e33a1f0746c234f821 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 3 Nov 2026 13:57:34 +0800 Subject: [PATCH] =?UTF-8?q?perf(=E6=8F=90=E4=BA=A4=E5=88=97=E8=A1=A8):=20?= =?UTF-8?q?=E6=8C=89=20=E2=86=91=20=E2=86=93=20=E6=8D=A2=E5=88=B0=E6=B2=A1?= =?UTF-8?q?=E7=9C=8B=E8=BF=87=E7=9A=84=E4=B8=80=E6=9D=A1=E4=B9=9F=E4=B8=8D?= =?UTF-8?q?=E5=8D=A1=E4=BA=86=20=E2=80=94=E2=80=94=20=E5=8F=B3=E6=A0=8F?= =?UTF-8?q?=E9=A2=84=E5=8F=96=E4=B8=8A=E4=B8=80=E6=9D=A1=E3=80=81=E4=B8=8B?= =?UTF-8?q?=E4=B8=80=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原来换到没缓存的一条要先卸掉代码区、转圈、再整个挂回来,CPU 降速 6 倍下每次卡 60–100ms;走缓存的只换内容不卡。现在这一条加载完顺手把上下两条也拉回来,同一条 只发一次请求(预取还在路上时按到它就接着等),按住 ↓ 划过去的那些行不预取。 Co-Authored-By: Claude Opus 5.5 --- .../submission/components/SubmissionPane.vue | 43 ++++++++++++++++++- .../oj/submission/composables/rowCursor.ts | 9 +++- apps/web/src/oj/submission/list.vue | 4 +- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/apps/web/src/oj/submission/components/SubmissionPane.vue b/apps/web/src/oj/submission/components/SubmissionPane.vue index abba10c..8ecea32 100644 --- a/apps/web/src/oj/submission/components/SubmissionPane.vue +++ b/apps/web/src/oj/submission/components/SubmissionPane.vue @@ -23,6 +23,8 @@ import UserName from 'shared/components/UserName.vue' */ const props = defineProps<{ row: SubmissionListItem + /** 按 ↑ ↓ 会落到的上一条、下一条:这条加载完顺手把它们也拉回来,换条时不用等网络 */ + neighbors?: SubmissionListItem[] /** 老师那套:重新判题、逐个测试点、复制两项收进「⋯」 */ teacher: boolean /** 在比赛的提交列表里(不能重判,题目链接走比赛) */ @@ -61,7 +63,42 @@ const detail = ref(null) const loading = ref(false) const failed = ref(false) -const judging = computed(() => props.row.result === SubmissionStatus.pending || props.row.result === SubmissionStatus.judging) +function isJudging(row: SubmissionListItem) { + return row.result === SubmissionStatus.pending || row.result === SubmissionStatus.judging +} +const judging = computed(() => isJudging(props.row)) + +/** + * 同一条只发一次请求:预取的还在路上时按到了它,接着等那一个,不再发第二个 + */ +const inflight = new Map>() +function fetchDetail(id: string) { + let request = inflight.get(id) + if (!request) { + request = getSubmission(id).finally(() => inflight.delete(id)) + inflight.set(id, request) + } + return request +} + +/** + * 预取上一条、下一条。只在这一条落定之后才做(按住 ↓ 一路划过去的那些行不预取), + * 进缓存的口径和 load() 一样:还在判的不要,列表和详情的结果对不上也不要。 + * + * 没预取的时候,换到没看过的一条要先把代码区卸掉、转圈、再整个重新挂上 —— + * 2026-11 实测(生产构建、CPU 降速 6 倍)每次卡 60–100ms;走缓存的只换内容,不卡。 + */ +function prefetch() { + for (const row of props.neighbors ?? []) { + if (!row.showLink || isJudging(row) || cache.get(row.id)?.result === row.result) continue + fetchDetail(row.id).then( + (res) => { + if (res.result === row.result) cache.set(row.id, res) + }, + () => {}, + ) + } +} // 每次加载一个序号:同一条先按「判题中」拉了一次、判完又拉一次时,晚回来的旧请求不能盖掉新的 let loadSeq = 0 @@ -78,15 +115,17 @@ async function load() { const hit = cache.get(row.id) if (hit && hit.result === row.result) { detail.value = hit + prefetch() return } loading.value = true try { - const res = await getSubmission(row.id) + const res = await fetchDetail(row.id) if (seq !== loadSeq) return // 列表那边还在判、详情这边已经判完了(或反过来)都不缓存,等两边对上 if (!judging.value && res.result === props.row.result) cache.set(row.id, res) detail.value = res + prefetch() } catch { if (seq === loadSeq) failed.value = true } finally { diff --git a/apps/web/src/oj/submission/composables/rowCursor.ts b/apps/web/src/oj/submission/composables/rowCursor.ts index c3a6595..2819bf3 100644 --- a/apps/web/src/oj/submission/composables/rowCursor.ts +++ b/apps/web/src/oj/submission/composables/rowCursor.ts @@ -35,6 +35,13 @@ export function useRowCursor(options: { const index = navigable.value.findIndex((row) => row.id === selectedId.value) return index === -1 ? null : { index: index + 1, count: navigable.value.length } }) + /** 按 ↑ ↓ 会落到的上一条、下一条(本页之内),右栏拿去预取 */ + const neighbors = computed(() => { + const list = navigable.value + const index = list.findIndex((row) => row.id === selectedId.value) + if (index === -1) return [] + return [list[index + 1], list[index - 1]].filter((row): row is Row => row !== undefined) + }) // 翻页是异步的,先记下「落地后停在哪一头」,新一页回来再选 let pendingJump: 'first' | 'last' | null = null @@ -125,5 +132,5 @@ export function useRowCursor(options: { move(e.key === 'ArrowUp' ? -1 : 1) }) - return { selectedId, selectedRow, position, maxPage, select, move, settleSelection, forgetJump } + return { selectedId, selectedRow, position, neighbors, maxPage, select, move, settleSelection, forgetJump } } diff --git a/apps/web/src/oj/submission/list.vue b/apps/web/src/oj/submission/list.vue index 8dc47bb..62ccd05 100644 --- a/apps/web/src/oj/submission/list.vue +++ b/apps/web/src/oj/submission/list.vue @@ -263,7 +263,7 @@ watch( // ==================== 选中与翻阅 ==================== -const { selectedId, selectedRow, position, maxPage, select, move, settleSelection, forgetJump } = useRowCursor({ +const { selectedId, selectedRow, position, neighbors, maxPage, select, move, settleSelection, forgetJump } = useRowCursor({ rows, total, query, @@ -858,6 +858,7 @@ function dayBreak(index: number) {