From 7928e688d549ac4916de0742eb749ad3fb4d0cd0 Mon Sep 17 00:00:00 2001
From: Your Name <119736744+aborayan2022@users.noreply.github.com>
Date: Sat, 4 Apr 2026 22:36:45 +0200
Subject: [PATCH] perf: optimize frontend bundle - reduce main chunk 77%
- vite.config: separate lucide-react icons + analytics into own chunks
- App.tsx: defer SiteAssistant loading via requestIdleCallback
- HeroUploadZone: lazy-load ToolSelectorModal + dynamic import fileRouting
- HeroUploadZone: add aria-label on dropzone input (accessibility)
- SocialProofStrip: defer API call until component is in viewport
- index.html: remove dev-only modulepreload hint
Main bundle: 266KB -> 61KB (-77%)
---
frontend/index.html | 1 -
frontend/src/App.tsx | 19 +++++++++++--
.../src/components/shared/HeroUploadZone.tsx | 27 ++++++++++---------
.../components/shared/SocialProofStrip.tsx | 26 +++++++++++++++---
frontend/vite.config.ts | 8 ++++++
5 files changed, 63 insertions(+), 18 deletions(-)
diff --git a/frontend/index.html b/frontend/index.html
index 8578487..f5ffcb3 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -65,7 +65,6 @@
-
Dociva — Free Online File Tools
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 2e7384d..2b62e05 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,4 +1,4 @@
-import { lazy, Suspense, useEffect } from 'react';
+import { lazy, Suspense, useEffect, useState } from 'react';
import Clarity from '@microsoft/clarity';
import { Routes, Route, useLocation } from 'react-router-dom';
import { Toaster } from 'sonner';
@@ -48,6 +48,19 @@ function LoadingFallback() {
);
}
+function IdleLoad({ children }: { children: React.ReactNode }) {
+ const [ready, setReady] = useState(false);
+ useEffect(() => {
+ if ('requestIdleCallback' in window) {
+ const id = requestIdleCallback(() => setReady(true));
+ return () => cancelIdleCallback(id);
+ }
+ const id = setTimeout(() => setReady(true), 2000);
+ return () => clearTimeout(id);
+ }, []);
+ return ready ? <>{children}> : null;
+}
+
export default function App() {
useDirection();
const location = useLocation();
@@ -152,7 +165,9 @@ export default function App() {
-
+
+
+
import('@/components/shared/ToolSelectorModal'));
+
/**
* The MIME types we accept on the homepage smart upload zone.
* Covers PDF, images, video, and Word documents.
@@ -45,12 +45,13 @@ export default function HeroUploadZone() {
const [error, setError] = useState(null);
const onDrop = useCallback(
- (acceptedFiles: File[]) => {
+ async (acceptedFiles: File[]) => {
setError(null);
if (acceptedFiles.length === 0) return;
const file = acceptedFiles[0];
+ const { getToolsForFile, detectFileCategory, getCategoryLabel } = await import('@/utils/fileRouting');
const tools = getToolsForFile(file);
if (tools.length === 0) {
@@ -107,7 +108,7 @@ export default function HeroUploadZone() {
{...getRootProps()}
className={`hero-upload-zone group ${isDragActive ? 'drag-active' : ''}`}
>
-
+
{/* Cloud icon with animated ring */}
@@ -210,13 +211,15 @@ export default function HeroUploadZone() {
{/* Tool Selector Modal */}
-
+
+
+
>
);
}
diff --git a/frontend/src/components/shared/SocialProofStrip.tsx b/frontend/src/components/shared/SocialProofStrip.tsx
index 331fc99..cc47f55 100644
--- a/frontend/src/components/shared/SocialProofStrip.tsx
+++ b/frontend/src/components/shared/SocialProofStrip.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Star } from 'lucide-react';
@@ -12,8 +12,27 @@ interface SocialProofStripProps {
export default function SocialProofStrip({ className = '' }: SocialProofStripProps) {
const { t } = useTranslation();
const [stats, setStats] = useState(null);
+ const sectionRef = useRef(null);
+ const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
+ const el = sectionRef.current;
+ if (!el) return;
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ setIsVisible(true);
+ observer.disconnect();
+ }
+ },
+ { rootMargin: '200px' }
+ );
+ observer.observe(el);
+ return () => observer.disconnect();
+ }, []);
+
+ useEffect(() => {
+ if (!isVisible) return;
let cancelled = false;
getPublicStats()
@@ -31,11 +50,12 @@ export default function SocialProofStrip({ className = '' }: SocialProofStripPro
return () => {
cancelled = true;
};
- }, []);
+ }, [isVisible]);
if (!stats) {
return (
@@ -97,7 +117,7 @@ export default function SocialProofStrip({ className = '' }: SocialProofStripPro
].filter((card): card is { label: string; value: string } => Boolean(card));
return (
-
+
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 8ebab8b..1b07f16 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -88,6 +88,14 @@ export default defineConfig({
return 'editor';
}
+ if (id.includes('lucide-react')) {
+ return 'icons';
+ }
+
+ if (id.includes('@microsoft/clarity')) {
+ return 'analytics';
+ }
+
return undefined;
},
},