perf(前端): 首页和题目详情页按需加载,首页 477 → 380KB、详情页 1031 → 682KB(传输量)
Deploy / deploy (push) Canceled after 0s

浏览器实测(preview 构建,生产规模数据):首页解压后 1428 → 1112KB,
未登录的题目详情页 3317 → 2021KB。后端接口 6–8ms,不是瓶颈,没动。

首页:
- 登录速报弹框挂在默认布局上,静态 import 了 md-editor-v3(~160KB JS + 76KB CSS),
  而 AI 分析只在登录后、提交满 3 次才有。MdPreview 改成异步组件,目标是本地包装
  LoginSummaryPreview.vue —— 直接 import("md-editor-v3") 会让 rolldown 把它和
  CodeMirror 语言包并成一个 467KB 的 chunk,排名页反而涨了 560KB(实测)
- 题目列表经 oj/api.ts 还带着 zod(~70KB):那个文件静态 import 了题目详情、提交详情
  两个 schema。改成和请求并行动态加载(单独的 oj/detailSchemas.ts,理由同
  profileSchema.ts),闸门仍是同步的 —— submissionDetailSchema 有 .catch(),
  不能改 contractDeferred

题目详情页:
- skulpt(~226KB gzip)原来一选 Python 就立刻预取。改成登录后才取(没登录交不了),
  且等 requestIdleCallback,不和题面、编辑器抢带宽
- StatisticsPanel 只有老师可见,却被 EditorToolbar 静态 import,每个学生都白拉一份
  chart.js(~68KB gzip)。改成异步组件,打开课堂统计时才加载
- SubmissionResult(带 DataTable)改成异步组件,第一次提交弹出结果面板时才加载

浏览器核过:首页正常;详情页未登录不再加载 skulpt / chart.js / DataTable;学生登录后
skulpt 空闲预取、提交后结果面板正常弹出;老师点课堂统计按需加载图表并正常渲染。
登录速报的 AI 分析与语法错误拦截路径本机没能触发,未在浏览器里核。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-27 16:34:32 +08:00
1 parent ca1df38440
commit a698ebf585
6 files changed
+63 -23

No files matched your search

@@ -15,7 +15,6 @@ import { useBreakpoints } from "shared/composables/breakpoints"
import { useUserStore } from "shared/store/user"
import storage from "utils/storage"
import type { LANGUAGE } from "utils/types"
import StatisticsPanel from "shared/components/StatisticsPanel.vue"
import { Icon } from "@iconify/vue"
import { NFlex } from "naive-ui"
import SubmitCode from "./SubmitCode.vue"
@@ -23,6 +22,11 @@ import SubmitCode from "./SubmitCode.vue"
const SubmitFlowchart = defineAsyncComponent(
() => import("./SubmitFlowchart.vue"),
)
// 只有老师看得见(下面的弹框挂了 isTeacherOrAbove),静态 import 的话每个学生
// 打开题目都要白拉一份 chart.js(~68KB gzip)
const StatisticsPanel = defineAsyncComponent(
() => import("shared/components/StatisticsPanel.vue"),
)
interface Props {
storageKey: string
@@ -9,7 +9,6 @@ import { useFireworks } from "oj/problem/composables/useFireworks"
import { useSubmissionMonitor } from "oj/problem/composables/useSubmissionMonitor"
import { LANGUAGE_FORMAT_VALUE, SubmissionStatus } from "utils/constants"
import type { SubmitCodePayload } from "utils/types"
import SubmissionResult from "./SubmissionResult.vue"
import { getSubmitButtonState } from "./submitButtonState"
import { useBreakpoints } from "shared/composables/breakpoints"
import { useUserStore } from "shared/store/user"
@@ -24,6 +23,11 @@ import {
const ProblemReaction = defineAsyncComponent(
() => import("./ProblemReaction.vue"),
)
// 结果面板第一次弹出(也就是第一次提交)时才加载:它带着 DataTable,而判题要等
// 好几秒,这点下载时间藏得住。进页面就加载的话,只看题不提交的人也要付这笔
const SubmissionResult = defineAsyncComponent(
() => import("./SubmissionResult.vue"),
)
// ==================== 基础状态 ====================
const userStore = useUserStore()
@@ -57,11 +61,16 @@ const isFormatting = ref(false)
const isSubmittingRequest = ref(false)
// ==================== Python 语法检测器预取 ====================
// 选中 Python 时就把 Skulpt 拉下来,避免点提交时才开始下载
// 选中 Python 时就把 Skulpt 拉下来,避免点提交时才开始下载。
// 但它有 ~226KB gzip,比题面和编辑器加起来还大:没登录的提交不了,不拉;
// 登录了也等浏览器空闲再拉,别和首屏的题面、编辑器抢带宽
watch(
() => codeStore.code.language,
(language) => {
if (language === "Python") prefetchPythonSyntaxChecker()
() => codeStore.code.language === "Python" && userStore.isAuthed,
(needed) => {
if (!needed) return
if ("requestIdleCallback" in window)
requestIdleCallback(prefetchPythonSyntaxChecker)
else setTimeout(prefetchPythonSyntaxChecker, 1000)
},
{ immediate: true },
)