perf: 子标签页按需加载(代码与元素懒加载)

原 v-show + 静态 import 导致三个子标签页的组件代码全部打进 MobileGame
chunk,且首屏即渲染全部(排行榜 onMounted 立即发 AJAX、视频元数据加载)。

改为:
- 三个标签页内容拆为独立组件 TeamLeaderboardTab/IterationsTab/ExperimentTab
- MobileGame 用 defineAsyncComponent + 动态 import,Vite 拆成独立 chunk
- v-if + Suspense:未激活的标签页组件根本不挂载(不发 AJAX、不渲染)
构建产物可见三个独立 chunk;headless 验证:默认团队页仅加载 team chunk,
点迭代记录才加载 IterationsTab chunk,点实验才加载 ExperimentTab chunk。
This commit is contained in:
Zikai
2026-07-12 15:44:39 +00:00
parent 88979a68c2
commit b3556b8639
4 changed files with 129 additions and 77 deletions

View File

@@ -1,18 +1,12 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref, defineAsyncComponent } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import HeroSection from './components/HeroSection.vue' import HeroSection from './components/HeroSection.vue'
import ScreenshotCarousel from './components/ScreenshotCarousel.vue' import ScreenshotCarousel from './components/ScreenshotCarousel.vue'
import SubTabs from './components/SubTabs.vue' import SubTabs from './components/SubTabs.vue'
import VersionTimeline from './components/VersionTimeline.vue'
import TeamCard from './components/TeamCard.vue'
import Leaderboard from './components/Leaderboard.vue'
const { locale } = useI18n({ useScope: 'global' }) const { locale } = useI18n({ useScope: 'global' })
const L = (zh, en) => (locale.value === 'zh-CN' ? zh : en) const L = (zh, en) => (locale.value === 'zh-CN' ? zh : en)
// 站点根绝对路径,用绑定避免被 Vite 当作构建期静态资源解析
const videoPoster = '/448/2.jpg'
const videoSrc = '/448/ml/449ml.mp4'
// 子标签页:默认显示团队+排行榜 // 子标签页:默认显示团队+排行榜
const subTabs = [ const subTabs = [
@@ -21,6 +15,13 @@ const subTabs = [
{ id: 'experiment', label: { zh: '实验', en: 'Experiment' } } { id: 'experiment', label: { zh: '实验', en: 'Experiment' } }
] ]
const activeTab = ref('team') const activeTab = ref('team')
// ★ 按需加载:三个子标签页用 defineAsyncComponent 动态 import
// 被 Vite/Rollup 拆成独立 chunk只有切到对应标签页时才下载与渲染。
// 配合 v-if未激活的标签页组件根本不挂载不执行 setup、不发 AJAX
const TeamLeaderboardTab = defineAsyncComponent(() => import('./components/TeamLeaderboardTab.vue'))
const IterationsTab = defineAsyncComponent(() => import('./components/IterationsTab.vue'))
const ExperimentTab = defineAsyncComponent(() => import('./components/ExperimentTab.vue'))
</script> </script>
<template> <template>
@@ -55,52 +56,18 @@ const activeTab = ref('team')
</div> </div>
</section> </section>
<!-- 子标签页区域 --> <!-- 子标签页区域按需加载v-if 确保未激活的不渲染 -->
<section class="block"> <section class="block">
<SubTabs v-model="activeTab" :tabs="subTabs" /> <SubTabs v-model="activeTab" :tabs="subTabs" />
<!-- 团队 + 排行榜默认 --> <Suspense>
<div v-show="activeTab === 'team'" class="block--two"> <TeamLeaderboardTab v-if="activeTab === 'team'" />
<div> <IterationsTab v-else-if="activeTab === 'iterations'" />
<h2 class="section-title"> <ExperimentTab v-else-if="activeTab === 'experiment'" />
<small>{{ L('团队', 'Team') }}</small> <template #fallback>
{{ L('成员', 'Members') }} <div class="tab-loading">{{ L('加载中…', 'Loading…') }}</div>
</h2> </template>
<TeamCard /> </Suspense>
</div>
<div>
<h2 class="section-title">
<small>{{ L('排行榜', 'Leaderboard') }}</small>
{{ L('玩家分数 Top 30', 'Top 30 Scores') }}
</h2>
<Leaderboard />
</div>
</div>
<!-- 迭代记录 -->
<div v-show="activeTab === 'iterations'">
<h2 class="section-title">
<small>{{ L('版本演进', 'Version History') }}</small>
{{ L('迭代记录', 'Iteration Record') }}
</h2>
<VersionTimeline />
</div>
<!-- 实验 -->
<div v-show="activeTab === 'experiment'">
<h2 class="section-title">
<small>{{ L('实验', 'Experiment') }}</small>
{{ L('ML-Agents 强化学习演示', 'ML-Agents Reinforcement Learning Demo') }}
</h2>
<div class="video-wrap">
<video controls preload="none" :poster="videoPoster">
<source :src="videoSrc" type="video/mp4" />
</video>
<p class="video-caption">
{{ L('胶囊体经 ML-Agents 训练,学习保球不掉与越障。', 'Capsules trained via ML-Agents to keep the ball from falling and to navigate obstacles.') }}
</p>
</div>
</div>
</section> </section>
</div> </div>
</div> </div>
@@ -129,36 +96,15 @@ const activeTab = ref('team')
.block__text p:last-child { .block__text p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.block--two { .tab-loading {
display: grid; padding: var(--space-xl);
grid-template-columns: 1fr 1fr; text-align: center;
gap: var(--space-xl); color: var(--text-tertiary);
align-items: start; font-size: 0.9rem;
}
.video-wrap {
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
background: var(--surface);
box-shadow: var(--shadow-sm);
}
.video-wrap video {
width: 100%;
display: block;
aspect-ratio: 16 / 9;
background: #000;
}
.video-caption {
padding: var(--space-md) var(--space-lg);
font-size: 0.88rem;
color: var(--text-secondary);
margin: 0;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.block__grid, .block__grid {
.block--two {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }

View File

@@ -0,0 +1,48 @@
<script setup>
import { useI18n } from 'vue-i18n'
const { locale } = useI18n({ useScope: 'global' })
const L = (zh, en) => (locale.value === 'zh-CN' ? zh : en)
// 站点根绝对路径,用绑定避免被 Vite 当作构建期静态资源解析
const videoPoster = '/448/2.jpg'
const videoSrc = '/448/ml/449ml.mp4'
</script>
<template>
<div class="tab-experiment">
<h2 class="section-title">
<small>{{ L('实验', 'Experiment') }}</small>
{{ L('ML-Agents 强化学习演示', 'ML-Agents Reinforcement Learning Demo') }}
</h2>
<div class="video-wrap">
<video controls preload="none" :poster="videoPoster">
<source :src="videoSrc" type="video/mp4" />
</video>
<p class="video-caption">
{{ L('胶囊体经 ML-Agents 训练,学习保球不掉与越障。', 'Capsules trained via ML-Agents to keep the ball from falling and to navigate obstacles.') }}
</p>
</div>
</div>
</template>
<style scoped>
.video-wrap {
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
background: var(--surface);
box-shadow: var(--shadow-sm);
}
.video-wrap video {
width: 100%;
display: block;
aspect-ratio: 16 / 9;
background: #000;
}
.video-caption {
padding: var(--space-md) var(--space-lg);
font-size: 0.88rem;
color: var(--text-secondary);
margin: 0;
}
</style>

View File

@@ -0,0 +1,17 @@
<script setup>
import { useI18n } from 'vue-i18n'
import VersionTimeline from './VersionTimeline.vue'
const { locale } = useI18n({ useScope: 'global' })
const L = (zh, en) => (locale.value === 'zh-CN' ? zh : en)
</script>
<template>
<div class="tab-iterations">
<h2 class="section-title">
<small>{{ L('版本演进', 'Version History') }}</small>
{{ L('迭代记录', 'Iteration Record') }}
</h2>
<VersionTimeline />
</div>
</template>

View File

@@ -0,0 +1,41 @@
<script setup>
import { useI18n } from 'vue-i18n'
import TeamCard from './TeamCard.vue'
import Leaderboard from './Leaderboard.vue'
const { locale } = useI18n({ useScope: 'global' })
const L = (zh, en) => (locale.value === 'zh-CN' ? zh : en)
</script>
<template>
<div class="tab-team block--two">
<div>
<h2 class="section-title">
<small>{{ L('团队', 'Team') }}</small>
{{ L('成员', 'Members') }}
</h2>
<TeamCard />
</div>
<div>
<h2 class="section-title">
<small>{{ L('排行榜', 'Leaderboard') }}</small>
{{ L('玩家分数 Top 30', 'Top 30 Scores') }}
</h2>
<Leaderboard />
</div>
</div>
</template>
<style scoped>
.block--two {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-xl);
align-items: start;
}
@media (max-width: 768px) {
.block--two {
grid-template-columns: 1fr;
}
}
</style>