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%)
This commit is contained in:
@@ -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() {
|
||||
|
||||
<Footer />
|
||||
<Suspense fallback={null}>
|
||||
<SiteAssistant />
|
||||
<IdleLoad>
|
||||
<SiteAssistant />
|
||||
</IdleLoad>
|
||||
<CookieConsent />
|
||||
</Suspense>
|
||||
<Toaster
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, lazy, Suspense } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UploadCloud, PenLine, ChevronRight, FileCheck } from 'lucide-react';
|
||||
import ToolSelectorModal from '@/components/shared/ToolSelectorModal';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
import { getToolsForFile, detectFileCategory, getCategoryLabel } from '@/utils/fileRouting';
|
||||
import type { ToolOption } from '@/utils/fileRouting';
|
||||
import { useConfig } from '@/hooks/useConfig';
|
||||
|
||||
const ToolSelectorModal = lazy(() => 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<string | null>(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' : ''}`}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<input {...getInputProps()} aria-label={t('home.dragDropTitle', 'Drag & drop your file here')} />
|
||||
|
||||
{/* Cloud icon with animated ring */}
|
||||
<div className="relative mb-6">
|
||||
@@ -210,13 +211,15 @@ export default function HeroUploadZone() {
|
||||
</div>
|
||||
|
||||
{/* Tool Selector Modal */}
|
||||
<ToolSelectorModal
|
||||
isOpen={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
file={selectedFile}
|
||||
tools={matchedTools}
|
||||
fileTypeLabel={fileTypeLabel}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<ToolSelectorModal
|
||||
isOpen={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
file={selectedFile}
|
||||
tools={matchedTools}
|
||||
fileTypeLabel={fileTypeLabel}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<PublicStatsSummary | null>(null);
|
||||
const sectionRef = useRef<HTMLElement>(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 (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
aria-hidden="true"
|
||||
className={`min-h-[260px] rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-700 dark:bg-slate-900/70 ${className}`.trim()}
|
||||
>
|
||||
@@ -97,7 +117,7 @@ export default function SocialProofStrip({ className = '' }: SocialProofStripPro
|
||||
].filter((card): card is { label: string; value: string } => Boolean(card));
|
||||
|
||||
return (
|
||||
<section className={`min-h-[260px] rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-700 dark:bg-slate-900/70 ${className}`.trim()}>
|
||||
<section ref={sectionRef} className={`min-h-[260px] rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-700 dark:bg-slate-900/70 ${className}`.trim()}>
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="max-w-2xl">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-primary-600 dark:text-primary-400">
|
||||
|
||||
Reference in New Issue
Block a user