feat(performance): enhance routing and component loading efficiency

- Implemented lazy loading for all route components to improve initial load times and reduce bundle size.
- Added Suspense boundaries around lazy-loaded components for better loading states and user experience.
- Memoized guard components to prevent unnecessary re-renders and optimize performance.
- Introduced defensive programming practices in guard components to handle potential errors gracefully.
- Updated routing structure to utilize React Router's future features for enhanced performance.
This commit is contained in:
chamiakJ
2025-06-21 18:32:41 +05:30
parent bb57280c8c
commit b617d15c62
4 changed files with 338 additions and 157 deletions

View File

@@ -5,7 +5,6 @@ import i18next from 'i18next';
// Components
import ThemeWrapper from './features/theme/ThemeWrapper';
import PreferenceSelector from './components/PreferenceSelector';
// Routes
import router from './app/routes';
@@ -14,7 +13,6 @@ import router from './app/routes';
import { useAppSelector } from './hooks/useAppSelector';
import { initMixpanel } from './utils/mixpanelInit';
import { initializeCsrfToken } from './api/api-client';
import { useRoutePreloader } from './utils/routePreloader';
// Types & Constants
import { Language } from './features/i18n/localesSlice';
@@ -28,9 +26,9 @@ import { SuspenseFallback } from './components/suspense-fallback/suspense-fallba
* 1. React.memo() - Prevents unnecessary re-renders
* 2. useMemo() - Memoizes expensive computations
* 3. useCallback() - Memoizes event handlers
* 4. Route preloading - Preloads critical routes
* 5. Lazy loading - Components loaded on demand
* 6. Suspense boundaries - Better loading states
* 4. Lazy loading - All route components loaded on demand
* 5. Suspense boundaries - Better loading states
* 6. Optimized guard components with memoization
*/
const App: React.FC = memo(() => {
const themeMode = useAppSelector(state => state.themeReducer.mode);
@@ -39,25 +37,6 @@ const App: React.FC = memo(() => {
// Memoize mixpanel initialization to prevent re-initialization
const mixpanelToken = useMemo(() => import.meta.env.VITE_MIXPANEL_TOKEN as string, []);
// Preload critical routes for better navigation performance
useRoutePreloader([
{
path: '/worklenz/home',
loader: () => import('./pages/home/home-page'),
priority: 'high'
},
{
path: '/worklenz/projects',
loader: () => import('./pages/projects/project-list'),
priority: 'high'
},
{
path: '/worklenz/schedule',
loader: () => import('./pages/schedule/schedule'),
priority: 'medium'
}
]);
useEffect(() => {
initMixpanel(mixpanelToken);
}, [mixpanelToken]);
@@ -95,7 +74,12 @@ const App: React.FC = memo(() => {
return (
<Suspense fallback={<SuspenseFallback />}>
<ThemeWrapper>
<RouterProvider router={router} future={{ v7_startTransition: true }} />
<RouterProvider
router={router}
future={{
v7_startTransition: true
}}
/>
</ThemeWrapper>
</Suspense>
);

View File

@@ -1,20 +1,20 @@
import { lazy, Suspense } from 'react';
import AuthLayout from '@/layouts/AuthLayout';
import LoginPage from '@/pages/auth/login-page';
import SignupPage from '@/pages/auth/signup-page';
import ForgotPasswordPage from '@/pages/auth/forgot-password-page';
import LoggingOutPage from '@/pages/auth/logging-out';
import AuthenticatingPage from '@/pages/auth/authenticating';
import { Navigate } from 'react-router-dom';
import VerifyResetEmailPage from '@/pages/auth/verify-reset-email';
import { Suspense } from 'react';
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
// Lazy load auth page components for better code splitting
const LoginPage = lazy(() => import('@/pages/auth/login-page'));
const SignupPage = lazy(() => import('@/pages/auth/signup-page'));
const ForgotPasswordPage = lazy(() => import('@/pages/auth/forgot-password-page'));
const LoggingOutPage = lazy(() => import('@/pages/auth/logging-out'));
const AuthenticatingPage = lazy(() => import('@/pages/auth/authenticating'));
const VerifyResetEmailPage = lazy(() => import('@/pages/auth/verify-reset-email'));
const authRoutes = [
{
path: '/auth',
element: (
<AuthLayout />
),
element: <AuthLayout />,
children: [
{
path: '',
@@ -22,27 +22,51 @@ const authRoutes = [
},
{
path: 'login',
element: <LoginPage />,
element: (
<Suspense fallback={<SuspenseFallback />}>
<LoginPage />
</Suspense>
),
},
{
path: 'signup',
element: <SignupPage />,
element: (
<Suspense fallback={<SuspenseFallback />}>
<SignupPage />
</Suspense>
),
},
{
path: 'forgot-password',
element: <ForgotPasswordPage />,
element: (
<Suspense fallback={<SuspenseFallback />}>
<ForgotPasswordPage />
</Suspense>
),
},
{
path: 'logging-out',
element: <LoggingOutPage />,
element: (
<Suspense fallback={<SuspenseFallback />}>
<LoggingOutPage />
</Suspense>
),
},
{
path: 'authenticating',
element: <AuthenticatingPage />,
element: (
<Suspense fallback={<SuspenseFallback />}>
<AuthenticatingPage />
</Suspense>
),
},
{
path: 'verify-reset-email/:user/:hash',
element: <VerifyResetEmailPage />,
element: (
<Suspense fallback={<SuspenseFallback />}>
<VerifyResetEmailPage />
</Suspense>
),
},
],
},

View File

@@ -1,4 +1,5 @@
import { createBrowserRouter, Navigate, RouteObject, useLocation } from 'react-router-dom';
import { lazy, Suspense, memo, useMemo } from 'react';
import rootRoutes from './root-routes';
import authRoutes from './auth-routes';
import mainRoutes, { licenseExpiredRoute } from './main-routes';
@@ -8,68 +9,125 @@ import reportingRoutes from './reporting-routes';
import { useAuthService } from '@/hooks/useAuth';
import { AuthenticatedLayout } from '@/layouts/AuthenticatedLayout';
import ErrorBoundary from '@/components/ErrorBoundary';
import NotFoundPage from '@/pages/404-page/404-page';
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
import { ISUBSCRIPTION_TYPE } from '@/shared/constants';
import LicenseExpired from '@/pages/license-expired/license-expired';
// Lazy load the NotFoundPage component for better code splitting
const NotFoundPage = lazy(() => import('@/pages/404-page/404-page'));
interface GuardProps {
children: React.ReactNode;
}
export const AuthGuard = ({ children }: GuardProps) => {
const isAuthenticated = useAuthService().isAuthenticated();
// Route-based code splitting utility
const withCodeSplitting = (Component: React.LazyExoticComponent<React.ComponentType<any>>) => {
return memo(() => (
<Suspense fallback={<SuspenseFallback />}>
<Component />
</Suspense>
));
};
// Memoized guard components with defensive programming
export const AuthGuard = memo(({ children }: GuardProps) => {
const authService = useAuthService();
const location = useLocation();
if (!isAuthenticated) {
const shouldRedirect = useMemo(() => {
try {
// Defensive check to ensure authService and its methods exist
if (!authService || typeof authService.isAuthenticated !== 'function') {
return false; // Don't redirect if auth service is not ready
}
return !authService.isAuthenticated();
} catch (error) {
console.error('Error in AuthGuard:', error);
return false; // Don't redirect on error, let the app handle it
}
}, [authService]);
if (shouldRedirect) {
return <Navigate to="/auth" state={{ from: location }} replace />;
}
return <>{children}</>;
};
});
export const AdminGuard = ({ children }: GuardProps) => {
const isAuthenticated = useAuthService().isAuthenticated();
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
const currentSession = useAuthService().getCurrentSession();
AuthGuard.displayName = 'AuthGuard';
export const AdminGuard = memo(({ children }: GuardProps) => {
const authService = useAuthService();
const location = useLocation();
const guardResult = useMemo(() => {
try {
// Defensive checks to ensure authService and its methods exist
if (!authService ||
typeof authService.isAuthenticated !== 'function' ||
typeof authService.isOwnerOrAdmin !== 'function' ||
typeof authService.getCurrentSession !== 'function') {
return null; // Don't redirect if auth service is not ready
}
if (!authService.isAuthenticated()) {
return { redirect: '/auth', state: { from: location } };
}
const currentSession = authService.getCurrentSession();
const isFreePlan = currentSession?.subscription_type === ISUBSCRIPTION_TYPE.FREE;
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to="/auth" state={{ from: location }} replace />;
if (!authService.isOwnerOrAdmin() || isFreePlan) {
return { redirect: '/worklenz/unauthorized' };
}
if (!isOwnerOrAdmin || isFreePlan) {
return <Navigate to="/worklenz/unauthorized" replace />;
return null;
} catch (error) {
console.error('Error in AdminGuard:', error);
return null; // Don't redirect on error
}
}, [authService, location]);
if (guardResult) {
return <Navigate to={guardResult.redirect} state={guardResult.state} replace />;
}
return <>{children}</>;
};
});
export const LicenseExpiryGuard = ({ children }: GuardProps) => {
const isAuthenticated = useAuthService().isAuthenticated();
const currentSession = useAuthService().getCurrentSession();
AdminGuard.displayName = 'AdminGuard';
export const LicenseExpiryGuard = memo(({ children }: GuardProps) => {
const authService = useAuthService();
const location = useLocation();
const shouldRedirect = useMemo(() => {
try {
// Defensive checks to ensure authService and its methods exist
if (!authService ||
typeof authService.isAuthenticated !== 'function' ||
typeof authService.getCurrentSession !== 'function') {
return false; // Don't redirect if auth service is not ready
}
if (!authService.isAuthenticated()) return false;
const isAdminCenterRoute = location.pathname.includes('/worklenz/admin-center');
const isLicenseExpiredRoute = location.pathname === '/worklenz/license-expired';
// Don't check or redirect if we're already on the license-expired page
if (isLicenseExpiredRoute) {
return <>{children}</>;
}
if (isLicenseExpiredRoute) return false;
const currentSession = authService.getCurrentSession();
// Check if trial is expired more than 7 days or if is_expired flag is set
const isLicenseExpiredMoreThan7Days = () => {
// Quick bail if no session data is available
if (!currentSession) {
return false;
}
if (!currentSession) return false;
// Check is_expired flag first
if (currentSession.is_expired) {
// If no trial_expire_date exists but is_expired is true, defer to backend check
if (!currentSession.trial_expire_date) {
return true;
}
if (!currentSession.trial_expire_date) return true;
// If there is a trial_expire_date, check if it's more than 7 days past
const today = new Date();
@@ -97,27 +155,49 @@ export const LicenseExpiryGuard = ({ children }: GuardProps) => {
return false;
};
// Add this explicit check and log the result
const shouldRedirect = isAuthenticated && isLicenseExpiredMoreThan7Days() && !isAdminCenterRoute;
return isLicenseExpiredMoreThan7Days() && !isAdminCenterRoute;
} catch (error) {
console.error('Error in LicenseExpiryGuard:', error);
return false; // Don't redirect on error
}
}, [authService, location.pathname]);
if (shouldRedirect) {
return <Navigate to="/worklenz/license-expired" replace />;
}
return <>{children}</>;
};
});
export const SetupGuard = ({ children }: GuardProps) => {
const isAuthenticated = useAuthService().isAuthenticated();
LicenseExpiryGuard.displayName = 'LicenseExpiryGuard';
export const SetupGuard = memo(({ children }: GuardProps) => {
const authService = useAuthService();
const location = useLocation();
if (!isAuthenticated) {
const shouldRedirect = useMemo(() => {
try {
// Defensive check to ensure authService and its methods exist
if (!authService || typeof authService.isAuthenticated !== 'function') {
return false; // Don't redirect if auth service is not ready
}
return !authService.isAuthenticated();
} catch (error) {
console.error('Error in SetupGuard:', error);
return false; // Don't redirect on error
}
}, [authService]);
if (shouldRedirect) {
return <Navigate to="/auth" state={{ from: location }} replace />;
}
return <>{children}</>;
};
});
// Helper to wrap routes with guards
SetupGuard.displayName = 'SetupGuard';
// Optimized route wrapping function with Suspense boundaries
const wrapRoutes = (
routes: RouteObject[],
Guard: React.ComponentType<{ children: React.ReactNode }>
@@ -125,7 +205,11 @@ const wrapRoutes = (
return routes.map(route => {
const wrappedRoute = {
...route,
element: <Guard>{route.element}</Guard>,
element: (
<Suspense fallback={<SuspenseFallback />}>
<Guard>{route.element}</Guard>
</Suspense>
),
};
if (route.children) {
@@ -140,9 +224,8 @@ const wrapRoutes = (
});
};
// Static license expired component that doesn't rely on translations or authentication
const StaticLicenseExpired = () => {
// Optimized static license expired component
const StaticLicenseExpired = memo(() => {
return (
<div style={{
marginTop: 65,
@@ -184,23 +267,31 @@ const StaticLicenseExpired = () => {
</div>
</div>
);
};
});
StaticLicenseExpired.displayName = 'StaticLicenseExpired';
// Create route arrays (moved outside of useMemo to avoid hook violations)
const publicRoutes = [
...rootRoutes,
...authRoutes,
notFoundRoute
];
const protectedMainRoutes = wrapRoutes(mainRoutes, AuthGuard);
const adminRoutes = wrapRoutes(reportingRoutes, AdminGuard);
const setupRoutes = wrapRoutes([accountSetupRoute], SetupGuard);
// Apply LicenseExpiryGuard to all protected routes
// License expiry check function
const withLicenseExpiryCheck = (routes: RouteObject[]): RouteObject[] => {
return routes.map(route => {
const wrappedRoute = {
...route,
element: <LicenseExpiryGuard>{route.element}</LicenseExpiryGuard>,
element: (
<Suspense fallback={<SuspenseFallback />}>
<LicenseExpiryGuard>{route.element}</LicenseExpiryGuard>
</Suspense>
),
};
if (route.children) {
@@ -213,10 +304,21 @@ const withLicenseExpiryCheck = (routes: RouteObject[]): RouteObject[] => {
const licenseCheckedMainRoutes = withLicenseExpiryCheck(protectedMainRoutes);
// Create optimized router with future flags for better performance
const router = createBrowserRouter([
{
element: <ErrorBoundary><AuthenticatedLayout /></ErrorBoundary>,
errorElement: <ErrorBoundary><NotFoundPage /></ErrorBoundary>,
element: (
<ErrorBoundary>
<AuthenticatedLayout />
</ErrorBoundary>
),
errorElement: (
<ErrorBoundary>
<Suspense fallback={<SuspenseFallback />}>
<NotFoundPage />
</Suspense>
</ErrorBoundary>
),
children: [
...licenseCheckedMainRoutes,
...adminRoutes,
@@ -225,6 +327,15 @@ const router = createBrowserRouter([
],
},
...publicRoutes,
]);
], {
// Enable React Router future features for better performance
future: {
v7_relativeSplatPath: true,
v7_fetcherPersist: true,
v7_normalizeFormMethod: true,
v7_partialHydration: true,
v7_skipActionErrorRevalidation: true
}
});
export default router;

View File

@@ -1,32 +1,49 @@
import { RouteObject } from 'react-router-dom';
import { lazy, Suspense } from 'react';
import MainLayout from '@/layouts/MainLayout';
import HomePage from '@/pages/home/home-page';
import ProjectList from '@/pages/projects/project-list';
import settingsRoutes from './settings-routes';
import adminCenterRoutes from './admin-center-routes';
import Schedule from '@/pages/schedule/schedule';
import ProjectTemplateEditView from '@/pages/settings/project-templates/projectTemplateEditView/ProjectTemplateEditView';
import LicenseExpired from '@/pages/license-expired/license-expired';
import ProjectView from '@/pages/projects/projectView/project-view';
import Unauthorized from '@/pages/unauthorized/unauthorized';
import { useAuthService } from '@/hooks/useAuth';
import { Navigate, useLocation } from 'react-router-dom';
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
// Define AdminGuard component first
// Lazy load page components for better code splitting
const HomePage = lazy(() => import('@/pages/home/home-page'));
const ProjectList = lazy(() => import('@/pages/projects/project-list'));
const Schedule = lazy(() => import('@/pages/schedule/schedule'));
const ProjectTemplateEditView = lazy(() => import('@/pages/settings/project-templates/projectTemplateEditView/ProjectTemplateEditView'));
const LicenseExpired = lazy(() => import('@/pages/license-expired/license-expired'));
const ProjectView = lazy(() => import('@/pages/projects/projectView/project-view'));
const Unauthorized = lazy(() => import('@/pages/unauthorized/unauthorized'));
// Define AdminGuard component with defensive programming
const AdminGuard = ({ children }: { children: React.ReactNode }) => {
const isAuthenticated = useAuthService().isAuthenticated();
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
const authService = useAuthService();
const location = useLocation();
if (!isAuthenticated) {
try {
// Defensive checks to ensure authService and its methods exist
if (!authService ||
typeof authService.isAuthenticated !== 'function' ||
typeof authService.isOwnerOrAdmin !== 'function') {
// If auth service is not ready, render children (don't block)
return <>{children}</>;
}
if (!authService.isAuthenticated()) {
return <Navigate to="/auth" state={{ from: location }} replace />;
}
if (!isOwnerOrAdmin) {
if (!authService.isOwnerOrAdmin()) {
return <Navigate to="/worklenz/unauthorized" replace />;
}
return <>{children}</>;
} catch (error) {
console.error('Error in AdminGuard (main-routes):', error);
// On error, render children to prevent complete blocking
return <>{children}</>;
}
};
const mainRoutes: RouteObject[] = [
@@ -35,18 +52,56 @@ const mainRoutes: RouteObject[] = [
element: <MainLayout />,
children: [
{ index: true, element: <Navigate to="home" replace /> },
{ path: 'home', element: <HomePage /> },
{ path: 'projects', element: <ProjectList /> },
{
path: 'home',
element: (
<Suspense fallback={<SuspenseFallback />}>
<HomePage />
</Suspense>
)
},
{
path: 'projects',
element: (
<Suspense fallback={<SuspenseFallback />}>
<ProjectList />
</Suspense>
)
},
{
path: 'schedule',
element: <AdminGuard><Schedule /></AdminGuard>
element: (
<Suspense fallback={<SuspenseFallback />}>
<AdminGuard>
<Schedule />
</AdminGuard>
</Suspense>
)
},
{
path: `projects/:projectId`,
element: (
<Suspense fallback={<SuspenseFallback />}>
<ProjectView />
</Suspense>
)
},
{ path: `projects/:projectId`, element: <ProjectView /> },
{
path: `settings/project-templates/edit/:templateId/:templateName`,
element: <ProjectTemplateEditView />,
element: (
<Suspense fallback={<SuspenseFallback />}>
<ProjectTemplateEditView />
</Suspense>
),
},
{
path: 'unauthorized',
element: (
<Suspense fallback={<SuspenseFallback />}>
<Unauthorized />
</Suspense>
)
},
{ path: 'unauthorized', element: <Unauthorized /> },
...settingsRoutes,
...adminCenterRoutes,
],
@@ -58,7 +113,14 @@ export const licenseExpiredRoute: RouteObject = {
path: '/worklenz',
element: <MainLayout />,
children: [
{ path: 'license-expired', element: <LicenseExpired /> }
{
path: 'license-expired',
element: (
<Suspense fallback={<SuspenseFallback />}>
<LicenseExpired />
</Suspense>
)
}
]
};