Performance optimizations: Bundle size reduction
Optimizations implemented: 1. Font display: swap + preload for critical fonts 2. ReactQueryDevtools: Lazy load in dev only, exclude from production 3. Auth forms code splitting: LoginForm, PasswordResetRequestForm 4. Remove invalid swcMinify option (default in Next.js 15) Results: - Login page: 178 kB → 104 kB (74 kB saved, 42% reduction) - Password reset: 178 kB → 104 kB (74 kB saved, 42% reduction) - Homepage: 108 kB (baseline 102 kB shared + 6 kB page) Remaining issue: - 102 kB baseline shared by all pages (React Query + Auth loaded globally)
This commit is contained in:
2863
frontend/lighthouse-report.json
Normal file
2863
frontend/lighthouse-report.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,14 @@ const nextConfig: NextConfig = {
|
|||||||
ignoreDuringBuilds: false,
|
ignoreDuringBuilds: false,
|
||||||
dirs: ['src'],
|
dirs: ['src'],
|
||||||
},
|
},
|
||||||
|
// Production optimizations
|
||||||
|
reactStrictMode: true,
|
||||||
|
// Note: swcMinify is default in Next.js 15
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
// Enable bundle analyzer when ANALYZE=true
|
||||||
|
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||||
|
enabled: process.env.ANALYZE === 'true',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withBundleAnalyzer(nextConfig);
|
||||||
2237
frontend/package-lock.json
generated
2237
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -59,6 +59,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@hey-api/openapi-ts": "^0.86.11",
|
"@hey-api/openapi-ts": "^0.86.11",
|
||||||
|
"@next/bundle-analyzer": "^16.0.1",
|
||||||
"@peculiar/webcrypto": "^1.5.0",
|
"@peculiar/webcrypto": "^1.5.0",
|
||||||
"@playwright/test": "^1.56.1",
|
"@playwright/test": "^1.56.1",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
@@ -74,6 +75,7 @@
|
|||||||
"eslint-config-next": "15.2.0",
|
"eslint-config-next": "15.2.0",
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.2.0",
|
||||||
"jest-environment-jsdom": "^30.2.0",
|
"jest-environment-jsdom": "^30.2.0",
|
||||||
|
"lighthouse": "^12.8.2",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"whatwg-fetch": "^3.6.20"
|
"whatwg-fetch": "^3.6.20"
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { LoginForm } from '@/components/auth/LoginForm';
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
// Code-split LoginForm - heavy with react-hook-form + validation
|
||||||
|
const LoginForm = dynamic(
|
||||||
|
() => import('@/components/auth/LoginForm').then((mod) => ({ default: mod.LoginForm })),
|
||||||
|
{
|
||||||
|
loading: () => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="animate-pulse h-10 bg-muted rounded" />
|
||||||
|
<div className="animate-pulse h-10 bg-muted rounded" />
|
||||||
|
<div className="animate-pulse h-10 bg-primary/20 rounded" />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,7 +5,22 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { PasswordResetRequestForm } from '@/components/auth/PasswordResetRequestForm';
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
// Code-split PasswordResetRequestForm
|
||||||
|
const PasswordResetRequestForm = dynamic(
|
||||||
|
() => import('@/components/auth/PasswordResetRequestForm').then((mod) => ({
|
||||||
|
default: mod.PasswordResetRequestForm
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
loading: () => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="animate-pulse h-10 bg-muted rounded" />
|
||||||
|
<div className="animate-pulse h-10 bg-primary/20 rounded" />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default function PasswordResetPage() {
|
export default function PasswordResetPage() {
|
||||||
return (
|
return (
|
||||||
@@ -14,7 +29,7 @@ export default function PasswordResetPage() {
|
|||||||
<h2 className="text-3xl font-bold tracking-tight">
|
<h2 className="text-3xl font-bold tracking-tight">
|
||||||
Reset your password
|
Reset your password
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-2 text-muted-foreground">
|
||||||
We'll send you an email with instructions to reset your password
|
We'll send you an email with instructions to reset your password
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,11 +6,15 @@ import { Providers } from "./providers";
|
|||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
display: "swap", // Prevent font from blocking render
|
||||||
|
preload: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const geistMono = Geist_Mono({
|
||||||
variable: "--font-geist-mono",
|
variable: "--font-geist-mono",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
display: "swap", // Prevent font from blocking render
|
||||||
|
preload: false, // Only preload primary font
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { lazy, Suspense, useState } from 'react';
|
||||||
import { useState } from 'react';
|
|
||||||
import { ThemeProvider } from '@/components/theme';
|
import { ThemeProvider } from '@/components/theme';
|
||||||
import { AuthInitializer } from '@/components/auth';
|
import { AuthInitializer } from '@/components/auth';
|
||||||
|
|
||||||
|
// Lazy load devtools - only in development, never in production
|
||||||
|
const ReactQueryDevtools =
|
||||||
|
process.env.NODE_ENV === 'development'
|
||||||
|
? lazy(() =>
|
||||||
|
import('@tanstack/react-query-devtools').then((mod) => ({
|
||||||
|
default: mod.ReactQueryDevtools,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
export function Providers({ children }: { children: React.ReactNode }) {
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
const [queryClient] = useState(
|
const [queryClient] = useState(
|
||||||
() =>
|
() =>
|
||||||
@@ -29,7 +38,11 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthInitializer />
|
<AuthInitializer />
|
||||||
{children}
|
{children}
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
{ReactQueryDevtools && (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user