Tối ưu hiệu năng tải menu trong dự án JeecgBoot

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

Thẻ: JeecgBoot SpringBoot Vue3 Redis PerformanceOptimization

Đăng vào ngày 2 tháng 6 lúc 02:40