perf: 优化管理端生产加载体验

- 图标与重型组件按需加载,优化菜单预取和页面请求链路

- 开启 gzip 与静态缓存并修复子路由 KeepAlive 冲突
This commit is contained in:
2026-07-22 20:35:55 +08:00
parent 9f06c238d3
commit 19059fde96
27 changed files with 750 additions and 233 deletions

View File

@@ -103,6 +103,7 @@ const showHeaderNav = computed(() => {
});
const {
handleMenuEnter,
handleMenuSelect,
handleMenuOpen,
headerActive,
@@ -302,6 +303,7 @@ const headerSlots = computed(() => {
:theme="headerTheme"
class="w-full"
mode="horizontal"
@enter="handleMenuEnter"
@select="handleMenuSelect"
/>
</template>
@@ -330,6 +332,7 @@ const headerSlots = computed(() => {
:rounded="isMenuRounded"
:theme="sidebarTheme"
mode="vertical"
@enter="handleMenuEnter"
@open="handleMenuOpen"
@select="handleMenuSelect"
/>

View File

@@ -15,6 +15,7 @@ const props = withDefaults(defineProps<Props>(), {
});
const emit = defineEmits<{
enter: [string];
open: [string, string[]];
select: [string, string?];
}>();
@@ -26,6 +27,10 @@ function handleMenuSelect(key: string) {
function handleMenuOpen(key: string, path: string[]) {
emit('open', key, path);
}
function handleMenuEnter(key: string) {
emit('enter', key);
}
</script>
<template>
@@ -39,6 +44,7 @@ function handleMenuOpen(key: string, path: string[]) {
:rounded="rounded"
scroll-to-active
:theme="theme"
@enter="handleMenuEnter"
@open="handleMenuOpen"
@select="handleMenuSelect"
/>

View File

@@ -13,7 +13,7 @@ import { useNavigation } from './use-navigation';
function useExtraMenu(useRootMenus?: ComputedRef<MenuRecordRaw[]>) {
const accessStore = useAccessStore();
const { navigation, prefetch, willOpenedByWindow } = useNavigation();
const { navigation, schedulePrefetch, willOpenedByWindow } = useNavigation();
const menus = computed(() => useRootMenus?.value ?? accessStore.accessMenus);
@@ -87,7 +87,7 @@ function useExtraMenu(useRootMenus?: ComputedRef<MenuRecordRaw[]>) {
};
const handleMenuMouseEnter = (menu: MenuRecordRaw) => {
prefetch(menu.path);
schedulePrefetch(menu.children?.[0]?.path ?? menu.path);
if (!preferences.sidebar.expandOnHover) {
const { findMenu } = findRootMenuByPath(menus.value, menu.path);

View File

@@ -9,8 +9,12 @@ import { findRootMenuByPath } from '@easyflow/utils';
import { useNavigation } from './use-navigation';
const MAX_AUTOMATIC_PREFETCH_COUNT = 3;
const PREFETCH_INTERVAL = 120;
function useMixedMenu() {
const { navigation, prefetch, willOpenedByWindow } = useNavigation();
const { navigation, prefetch, schedulePrefetch, willOpenedByWindow } =
useNavigation();
const accessStore = useAccessStore();
const route = useRoute();
const splitSideMenus = ref<MenuRecordRaw[]>([]);
@@ -97,11 +101,13 @@ function useMixedMenu() {
if (!willOpenedByWindow(key)) {
rootMenuPath.value = rootMenu?.path ?? '';
splitSideMenus.value = _splitSideMenus;
_splitSideMenus.forEach((menu) => {
if (menu.path) {
prefetch(menu.path);
}
});
_splitSideMenus
.slice(0, MAX_AUTOMATIC_PREFETCH_COUNT)
.forEach((menu, index) => {
if (menu.path) {
schedulePrefetch(menu.path, PREFETCH_INTERVAL * (index + 1));
}
});
}
if (_splitSideMenus.length === 0) {
@@ -130,6 +136,10 @@ function useMixedMenu() {
}
};
const handleMenuEnter = (key: string) => {
schedulePrefetch(key);
};
/**
* 计算侧边菜单
* @param path 路由路径
@@ -166,6 +176,7 @@ function useMixedMenu() {
});
return {
handleMenuEnter,
handleMenuSelect,
handleMenuOpen,
headerActive,

View File

@@ -4,11 +4,37 @@ import { useRouter } from 'vue-router';
import { isHttpUrl, openRouteInNewWindow, openWindow } from '@easyflow/utils';
interface NetworkConnectionLike {
effectiveType?: string;
saveData?: boolean;
}
function canSpeculativelyPrefetch() {
if (typeof navigator === 'undefined') {
return false;
}
const nav = navigator as Navigator & {
connection?: NetworkConnectionLike;
mozConnection?: NetworkConnectionLike;
webkitConnection?: NetworkConnectionLike;
};
const connection =
nav.connection ?? nav.mozConnection ?? nav.webkitConnection;
if (!connection) {
return true;
}
return (
!connection.saveData &&
!['2g', '3g', 'slow-2g'].includes(connection.effectiveType ?? '')
);
}
function useNavigation() {
const router = useRouter();
const routeMetaMap = new Map<string, RouteRecordNormalized>();
const prefetchedPaths = new Set<string>();
const prefetchingPaths = new Set<string>();
const scheduledPrefetches = new Map<string, ReturnType<typeof setTimeout>>();
// 初始化路由映射
const initRouteMetaMap = () => {
@@ -40,6 +66,12 @@ function useNavigation() {
};
const prefetch = (path: string) => {
const scheduledPrefetch = scheduledPrefetches.get(path);
if (scheduledPrefetch) {
clearTimeout(scheduledPrefetch);
scheduledPrefetches.delete(path);
}
if (
isHttpUrl(path) ||
prefetchedPaths.has(path) ||
@@ -84,6 +116,24 @@ function useNavigation() {
});
};
const schedulePrefetch = (path: string, delay = 120) => {
if (
!canSpeculativelyPrefetch() ||
isHttpUrl(path) ||
prefetchedPaths.has(path) ||
prefetchingPaths.has(path) ||
scheduledPrefetches.has(path)
) {
return;
}
const timer = setTimeout(() => {
scheduledPrefetches.delete(path);
prefetch(path);
}, delay);
scheduledPrefetches.set(path, timer);
};
const navigation = async (path: string) => {
try {
const route = routeMetaMap.get(path);
@@ -116,7 +166,7 @@ function useNavigation() {
return shouldOpenInNewWindow(path);
};
return { navigation, prefetch, willOpenedByWindow };
return { navigation, prefetch, schedulePrefetch, willOpenedByWindow };
}
export { useNavigation };