Back to README
This project underwent extensive optimization to achieve a 98/100 Lighthouse score.
| Metric | Before | After | Improvement |
|---|---|---|---|
| Lighthouse Score | 54/100 | 98/100 | +44 points |
| First Contentful Paint | 32.2s | 1.8s | 18x faster |
| Largest Contentful Paint | 62.4s | 2.0s | 31x faster |
| Initial JS Bundle | ~2.5 MB | ~116 KB | 95% smaller |
| Total Blocking Time | 120ms | 0ms | Perfect |
- Syncfusion Grid Loading on Login Page — 2.1MB grid component loaded everywhere
- Static Imports of Heavy Dependencies —
registerLicensepulled entire Syncfusion tree - Barrel Export Tree-Shaking Issues — mixed native/Syncfusion barrels prevented dead-code elimination
- Eager CSS Loading — all Syncfusion CSS loaded upfront
- MainLayout Not Lazy-Loaded — dashboard layout loaded on every page
// Before: static import pulled ~2MB
import { registerLicense } from '@syncfusion/ej2-base';
// After: dynamic import defers loading
async function registerLicenseAsync(key: string): Promise<void> {
const { registerLicense } = await import('@syncfusion/ej2-base');
registerLicense(key);
}Impact: Removed ~2MB from initial bundle.
src/components/ui/
├── index.ts → Re-exports types only
├── native.ts → ButtonNative, InputNative (zero Syncfusion deps)
└── syncfusion.ts → DataGrid, Button, Input, Select, etc.
Login page imports from native.ts only — zero Syncfusion JavaScript.
Created lightweight native HTML components (ButtonNative, InputNative) — ~2KB vs ~200KB.
const MainLayout = lazy(async () => ({
default: (await import('@/components/layout/MainLayout')).MainLayout,
}));Dashboard layout code only loads when navigating to /dashboard.
src/styles/
├── login.css → Base styles + critical components only (~6KB gzipped)
├── app.css → Syncfusion styles + full components (~140KB gzipped)
Initial CSS reduced from ~150KB to ~6KB.
Post-build script strips heavy chunks from modulepreload hints. Also configured in vite.config.ts via build.modulePreload.resolveDependencies.
export const preloadSyncfusionModules = (): void => {
const preload = (): void => {
import('@syncfusion/ej2-react-grids').catch(() => undefined);
import('@syncfusion/ej2-react-calendars').catch(() => undefined);
import('@syncfusion/ej2-react-dropdowns').catch(() => undefined);
};
if ('requestIdleCallback' in window) window.requestIdleCallback(preload, { timeout: 2000 });
else setTimeout(preload, 100);
};Dashboard loads instantly because modules are already cached.
Pre-bundle all heavy dependencies via optimizeDeps.include and warm up critical files via server.warmup.clientFiles.
Final Production Bundle (Login Page):
| File | Size (gzipped) | Purpose |
|---|---|---|
index-*.js |
27.7 KB | Main app entry |
react-vendor-*.js |
66.1 KB | React + React DOM |
query-vendor-*.js |
13.5 KB | TanStack Query |
index-*.css |
6.4 KB | Login CSS |
| Total | ~116 KB | Initial load |
Deferred Chunks (loaded after login):
| File | Size (gzipped) | When Loaded |
|---|---|---|
syncfusion-grid-*.js |
498 KB | DataGrid pages |
syncfusion-inputs-*.js |
1.4 KB | Form pages |
app-*.css |
140 KB | Dashboard |
Always test with the production build:
npm run build
npm run preview
npx lighthouse http://localhost:4173 --viewThe dev server will always be slower — only use production builds for accurate metrics.
| Module | When to Preload | Location | Why |
|---|---|---|---|
| Syncfusion Components | On login submit | LoginPage | Dashboard needs them immediately |
| Form Libraries (react-hook-form, zod) | After login page loads | LoginPage useEffect | Forms used throughout dashboard |
| App CSS | On dashboard mount | MainLayout | Full styling for dashboard |
| Syncfusion Grid | On dashboard idle | MainLayout | Heavy, load when browser is idle |
- Use
requestIdleCallback— Only preload when browser is idle - Set timeout — Ensure preload happens even if browser never idles
- Catch errors — Silent failure for preloads (non-critical)
- Don't block — Preloading should never delay user interactions
- Order matters — Preload modules in order of likely use