Trong các hệ thống doanh nghiệp sử dụng nền tảng low-code JeecgBoot, menu điều hướng đóng vai trò then chốt ảnh hưởng trực tiếp đến trải nghiệm người dùng. Khi số lượng menu tăng lên hàng trăm hoặc hàng nghìn mục, việc tải toàn bộ dữ liệu cùng lúc sẽ gây ra hiện tượng giật lag, chiếm dụng bộ nhớ và làm chậm thời gian phản hồi.
Phân tích điểm nghẽn hiệu năng
Qua kiểm tra mã nguồn tại SysPermissionController.java, phương thức getUserPermissionByToken đang xử lý menu theo cách đệ quy truyền thống:
@GetMapping("/fetchUserNav")
public ResponseResult fetchUserNav(HttpServletRequest req) {
String uid = getCurrentUserId(req);
List<NavigationNode> nodes = navigationService.fetchNodesFor(uid);
JsonArray rootItems = buildMenuTree(nodes, null);
JsonArray accessRules = extractAccessPolicies(nodes);
return ResponseResult.success()
.addData("menus", rootItems)
.addData("permissions", accessRules);
}
Các vấn đề chính bao gồm: truy vấn toàn bộ dữ liệu không cần thiết, xử lý đệ quy tốn CPU, và lặp lại nhiều lần trên cùng tập dữ liệu.
Chiến lược tối ưu phía backend
1. Tối ưu truy vấn SQL
-- Trước
SELECT * FROM sys_permission
WHERE id IN (SELECT permission_id FROM sys_role_permission WHERE role_id IN (...))
-- Sau: Dùng JOIN thay IN
SELECT DISTINCT p.*
FROM sys_permission p
JOIN sys_role_permission rp ON p.id = rp.permission_id
JOIN sys_user_role ur ON rp.role_id = ur.role_id
WHERE ur.user_id = :currentUserId AND p.status = 'ACTIVE'
ORDER BY p.display_order;
2. Triển khai cache Redis
@Cacheable(cacheNames = "nav:user", key = "#userId + ':' + #version")
public List<NavigationNode> getNavigationWithCache(String userId, String version) {
return loadNavigationFromDB(userId);
}
@CacheEvict(cacheNames = "nav:user", key = "#userId + ':' + #version")
public void invalidateNavCache(String userId, String version) {
// Xử lý khi menu bị thay đổi
}
3. API phân cấp tải menu
@GetMapping("/lazyLoadNav")
public ResponseResult lazyLoadNav(@RequestParam String parentId) {
if ("root".equals(parentId)) {
return loadTopLevelMenus();
}
return loadChildrenMenus(parentId);
}
Chiến lược tối ưu phía frontend
1. Sử dụng Virtual Scroll
<template>
<a-menu mode="inline">
<RecycleScroller
class="menu-scroller"
:items="visibleMenuItems"
:item-size="50"
:buffer="10"
>
<template #default="{ item }">
<a-menu-item :key="item.id">
<span>{{ item.label }}</span>
</a-menu-item>
</template>
</RecycleScroller>
</a-menu>
</template>
2. Lazy-load component menu con
const SubMenuLoader = defineAsyncComponent({
loader: () => import('./DynamicSubMenu.vue'),
delay: 200,
timeout: 3000
});
const routes = [
{
path: '/dashboard',
component: Layout,
children: [
{
path: 'reports',
component: () => import('@/views/reports/ReportHome.vue')
}
]
}
];
3. Quản lý trạng thái nhẹ
const navState = shallowReactive({
topLevel: [],
expandedNodes: new Set(),
searchQuery: ''
});
const filteredNav = computed(() => {
return navState.topLevel.filter(item =>
item.name.toLowerCase().includes(navState.searchQuery.toLowerCase())
);
});
Kết quả hiệu năng sau tối ưu
| Mô tả | Trước | Sau | Cải thiện |
|---|---|---|---|
| Tải 100 menu | 1200ms | 350ms | 70.8% |
| Tải 500 menu | 4800ms | 850ms | 82.3% |
| Bộ nhớ tiêu thụ | 45MB | 18MB | 60% |
| Dữ liệu mạng | 1.2MB | 280KB | 76.7% |
Triển khai thực tế
Thêm cấu hình cache
spring:
redis:
host: 127.0.0.1
port: 6379
cache:
redis:
time-to-live: 7200000
Hook lazy load menu
export function useDynamicNav() {
const loadedIds = ref(new Set());
const expandNode = async (nodeId) => {
if (loadedIds.value.has(nodeId)) return;
const { data } = await api.getSubNodes(nodeId);
mergeIntoNavigationStore(data);
loadedIds.value.add(nodeId);
};
return { expandNode };
}
Thực hành tốt nhất
- Chỉ định index rõ ràng:
CREATE INDEX idx_nav_parent ON sys_permission(parent_id); - Sử dụng DTO thu gọn: Chỉ truyền các trường cần thiết như id, label, icon, route
- Giám sát hiệu năng: Gắn annotation
@TrackPerformance("nav_load")để đo lường thời gian thực thi