perf(frontend): reduce initial rendering cost
This commit is contained in:
@@ -23,11 +23,34 @@
|
||||
<meta name="twitter:description" content="30+ free tools: merge, split, compress, convert PDFs, images, videos & text. No signup required." />
|
||||
<meta name="twitter:image" content="/social-preview.svg" />
|
||||
<meta name="twitter:image:alt" content="Dociva social preview" />
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var storedTheme = localStorage.getItem('theme');
|
||||
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (storedTheme === 'dark' || (!storedTheme && prefersDark)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
|
||||
var storedLanguage = localStorage.getItem('i18nextLng') || 'en';
|
||||
var normalizedLanguage = storedLanguage.split('-')[0];
|
||||
var resolvedLanguage = normalizedLanguage === 'ar' || normalizedLanguage === 'fr'
|
||||
? normalizedLanguage
|
||||
: 'en';
|
||||
document.documentElement.lang = resolvedLanguage;
|
||||
document.documentElement.dir = resolvedLanguage === 'ar' ? 'rtl' : 'ltr';
|
||||
} catch (error) {
|
||||
document.documentElement.lang = 'en';
|
||||
document.documentElement.dir = 'ltr';
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
|
||||
<link rel="dns-prefetch" href="https://fonts.gstatic.com" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Tajawal:wght@300;400;500;700&display=swap" rel="stylesheet" />
|
||||
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Tajawal:wght@400;500;700&display=swap" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Tajawal:wght@400;500;700&display=swap" rel="stylesheet" />
|
||||
<title>Dociva — Free Online File Tools</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -3,8 +3,6 @@ import { Routes, Route, useLocation } from 'react-router-dom';
|
||||
import { Toaster } from 'sonner';
|
||||
import Header from '@/components/layout/Header';
|
||||
import Footer from '@/components/layout/Footer';
|
||||
import CookieConsent from '@/components/layout/CookieConsent';
|
||||
import SiteAssistant from '@/components/layout/SiteAssistant';
|
||||
import ErrorBoundary from '@/components/shared/ErrorBoundary';
|
||||
import ToolLandingPage from '@/components/seo/ToolLandingPage';
|
||||
import { useDirection } from '@/hooks/useDirection';
|
||||
@@ -27,6 +25,8 @@ const BlogPostPage = lazy(() => import('@/pages/BlogPostPage'));
|
||||
const DevelopersPage = lazy(() => import('@/pages/DevelopersPage'));
|
||||
const InternalAdminPage = lazy(() => import('@/pages/InternalAdminPage'));
|
||||
const SeoRoutePage = lazy(() => import('@/pages/SeoRoutePage'));
|
||||
const CookieConsent = lazy(() => import('@/components/layout/CookieConsent'));
|
||||
const SiteAssistant = lazy(() => import('@/components/layout/SiteAssistant'));
|
||||
|
||||
// Tool Pages
|
||||
const PdfToWord = lazy(() => import('@/components/tools/PdfToWord'));
|
||||
@@ -72,6 +72,7 @@ const FlattenPdf = lazy(() => import('@/components/tools/FlattenPdf'));
|
||||
const RepairPdf = lazy(() => import('@/components/tools/RepairPdf'));
|
||||
const PdfMetadata = lazy(() => import('@/components/tools/PdfMetadata'));
|
||||
const ImageCrop = lazy(() => import('@/components/tools/ImageCrop'));
|
||||
const ImageToSvg = lazy(() => import('@/components/tools/ImageToSvg'));
|
||||
const ImageRotateFlip = lazy(() => import('@/components/tools/ImageRotateFlip'));
|
||||
const BarcodeGenerator = lazy(() => import('@/components/tools/BarcodeGenerator'));
|
||||
|
||||
@@ -145,6 +146,7 @@ export default function App() {
|
||||
<Route path="/tools/compress-image" element={<ToolLandingPage slug="compress-image"><CompressImage /></ToolLandingPage>} />
|
||||
<Route path="/tools/ocr" element={<ToolLandingPage slug="ocr"><OcrTool /></ToolLandingPage>} />
|
||||
<Route path="/tools/remove-background" element={<ToolLandingPage slug="remove-background"><RemoveBackground /></ToolLandingPage>} />
|
||||
<Route path="/tools/image-to-svg" element={<ToolLandingPage slug="image-to-svg"><ImageToSvg /></ToolLandingPage>} />
|
||||
|
||||
{/* Convert Tools */}
|
||||
<Route path="/tools/pdf-to-excel" element={<ToolLandingPage slug="pdf-to-excel"><PdfToExcel /></ToolLandingPage>} />
|
||||
@@ -196,8 +198,10 @@ export default function App() {
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<Suspense fallback={null}>
|
||||
<SiteAssistant />
|
||||
<CookieConsent />
|
||||
</Suspense>
|
||||
<Toaster
|
||||
position={isRTL ? 'top-left' : 'top-right'}
|
||||
dir={isRTL ? 'rtl' : 'ltr'}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
|
||||
interface AdSlotProps {
|
||||
@@ -23,8 +23,9 @@ export default function AdSlot({
|
||||
className = '',
|
||||
}: AdSlotProps) {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const adRef = useRef<HTMLModElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isLoaded = useRef(false);
|
||||
const [canLoad, setCanLoad] = useState(false);
|
||||
const clientId = (import.meta.env.VITE_ADSENSE_CLIENT_ID || '').trim();
|
||||
const slotMap: Record<string, string | undefined> = {
|
||||
'home-top': import.meta.env.VITE_ADSENSE_SLOT_HOME_TOP,
|
||||
@@ -35,7 +36,31 @@ export default function AdSlot({
|
||||
const resolvedSlot = /^\d+$/.test(slot) ? slot : slotMap[slot];
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoaded.current || !clientId || !resolvedSlot) return;
|
||||
if (canLoad || !containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof IntersectionObserver === 'undefined') {
|
||||
setCanLoad(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0]?.isIntersecting) {
|
||||
setCanLoad(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ rootMargin: '320px 0px' }
|
||||
);
|
||||
|
||||
observer.observe(containerRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, [canLoad]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoaded.current || !canLoad || !clientId || !resolvedSlot) return;
|
||||
|
||||
const existingScript = document.querySelector<HTMLScriptElement>(
|
||||
`script[data-adsense-client="${clientId}"]`
|
||||
@@ -60,7 +85,7 @@ export default function AdSlot({
|
||||
} catch {
|
||||
// AdSense not loaded (e.g., ad blocker)
|
||||
}
|
||||
}, [clientId, resolvedSlot]);
|
||||
}, [canLoad, clientId, resolvedSlot]);
|
||||
|
||||
if (!clientId || !resolvedSlot) return null;
|
||||
|
||||
@@ -68,9 +93,8 @@ export default function AdSlot({
|
||||
if (user?.plan === 'pro') return null;
|
||||
|
||||
return (
|
||||
<div className={`ad-slot ${className}`}>
|
||||
<div ref={containerRef} className={`ad-slot ${className}`}>
|
||||
<ins
|
||||
ref={adRef}
|
||||
className="adsbygoogle"
|
||||
style={{ display: 'block' }}
|
||||
data-ad-client={clientId}
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function ToolLandingPage({ slug, children }: ToolLandingPageProps
|
||||
</div>
|
||||
|
||||
{/* SEO Content Below Tool */}
|
||||
<div className="mx-auto mt-16 max-w-3xl">
|
||||
<div className="deferred-section mx-auto mt-16 max-w-3xl">
|
||||
<ToolWorkflowPanel />
|
||||
|
||||
{/* What this tool does */}
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function BarcodeGenerator() {
|
||||
{phase === 'done' && downloadUrl && (
|
||||
<div className="space-y-4 text-center">
|
||||
<div className="rounded-2xl bg-white p-6 ring-1 ring-slate-200 dark:bg-slate-800 dark:ring-slate-700">
|
||||
<img src={downloadUrl} alt="Barcode" className="mx-auto max-w-full" />
|
||||
<img src={downloadUrl} alt="Barcode" loading="lazy" decoding="async" className="mx-auto max-w-full" />
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<a href={downloadUrl} download className="btn-primary flex-1">{t('common.download')}</a>
|
||||
|
||||
@@ -113,7 +113,7 @@ export default function QrCodeGenerator() {
|
||||
{phase === 'done' && result && result.status === 'completed' && downloadUrl && (
|
||||
<div className="space-y-6 text-center">
|
||||
<div className="rounded-2xl bg-white p-8 ring-1 ring-slate-200 dark:bg-slate-800 dark:ring-slate-700">
|
||||
<img src={downloadUrl} alt="QR Code" className="mx-auto max-w-[300px] rounded-lg" />
|
||||
<img src={downloadUrl} alt="QR Code" loading="lazy" decoding="async" className="mx-auto max-w-[300px] rounded-lg" />
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<a href={downloadUrl} download={result.filename || 'qrcode.png'}
|
||||
|
||||
@@ -77,6 +77,7 @@ const otherTools: ToolInfo[] = [
|
||||
{ key: 'compressImage', path: '/tools/compress-image', icon: <Minimize2 className="h-6 w-6 text-orange-600" />, bgColor: 'bg-orange-50' },
|
||||
{ key: 'ocr', path: '/tools/ocr', icon: <ScanText className="h-6 w-6 text-amber-600" />, bgColor: 'bg-amber-50' },
|
||||
{ key: 'removeBg', path: '/tools/remove-background', icon: <Eraser className="h-6 w-6 text-fuchsia-600" />, bgColor: 'bg-fuchsia-50' },
|
||||
{ key: 'imageToSvg', path: '/tools/image-to-svg', icon: <ImageIcon className="h-6 w-6 text-indigo-600" />, bgColor: 'bg-indigo-50' },
|
||||
{ key: 'videoToGif', path: '/tools/video-to-gif', icon: <Film className="h-6 w-6 text-emerald-600" />, bgColor: 'bg-emerald-50' },
|
||||
{ key: 'qrCode', path: '/tools/qr-code', icon: <QrCode className="h-6 w-6 text-indigo-600" />, bgColor: 'bg-indigo-50' },
|
||||
{ key: 'htmlToPdf', path: '/tools/html-to-pdf', icon: <Code className="h-6 w-6 text-sky-600" />, bgColor: 'bg-sky-50' },
|
||||
@@ -156,7 +157,7 @@ export default function HomePage() {
|
||||
|
||||
<SocialProofStrip className="mb-10" />
|
||||
|
||||
<section className="mb-10 rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<section className="deferred-section mb-10 rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
|
||||
@@ -190,7 +191,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<section className="deferred-section mb-12 rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<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">
|
||||
@@ -221,7 +222,7 @@ export default function HomePage() {
|
||||
</section>
|
||||
|
||||
{/* Tools Grid */}
|
||||
<section>
|
||||
<section className="deferred-section">
|
||||
<h2 className="mb-6 text-center text-xl font-semibold text-slate-800 dark:text-slate-200">
|
||||
{t('home.pdfTools')}
|
||||
</h2>
|
||||
|
||||
@@ -90,7 +90,9 @@ export default function PricingPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SocialProofStrip className="mb-12" />
|
||||
<div className="deferred-section mb-12">
|
||||
<SocialProofStrip />
|
||||
</div>
|
||||
|
||||
{/* Plan Cards */}
|
||||
<div className="mb-16 grid gap-8 md:grid-cols-2">
|
||||
@@ -191,7 +193,7 @@ export default function PricingPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="mb-16 rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<section className="deferred-section mb-16 rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<div className="max-w-3xl">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">
|
||||
{t('pages.pricing.trustTitle')}
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function SeoCollectionPage({ slug }: SeoCollectionPageProps) {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<section className="deferred-section rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
{copy.toolsHeading}
|
||||
</h2>
|
||||
@@ -158,7 +158,7 @@ export default function SeoCollectionPage({ slug }: SeoCollectionPageProps) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<section className="deferred-section rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
{copy.selectionHeading}
|
||||
</h2>
|
||||
@@ -172,7 +172,7 @@ export default function SeoCollectionPage({ slug }: SeoCollectionPageProps) {
|
||||
</section>
|
||||
|
||||
{contentSections.length > 0 ? (
|
||||
<section className="grid gap-6 lg:grid-cols-2">
|
||||
<section className="deferred-section grid gap-6 lg:grid-cols-2">
|
||||
{contentSections.map((section) => (
|
||||
<article key={section.heading.en} className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
@@ -186,7 +186,7 @@ export default function SeoCollectionPage({ slug }: SeoCollectionPageProps) {
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<section className="deferred-section rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
{copy.relatedHeading}
|
||||
</h2>
|
||||
|
||||
@@ -197,7 +197,7 @@ export default function SeoPage({ slug }: SeoPageProps) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-8 lg:grid-cols-2">
|
||||
<section className="deferred-section grid gap-8 lg:grid-cols-2">
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
{copy.introHeading}
|
||||
@@ -222,7 +222,7 @@ export default function SeoPage({ slug }: SeoPageProps) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<section className="deferred-section rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
{copy.useCasesHeading}
|
||||
</h2>
|
||||
@@ -236,7 +236,7 @@ export default function SeoPage({ slug }: SeoPageProps) {
|
||||
</section>
|
||||
|
||||
{contentSections.length > 0 ? (
|
||||
<section className="grid gap-6 lg:grid-cols-2">
|
||||
<section className="deferred-section grid gap-6 lg:grid-cols-2">
|
||||
{contentSections.map((section) => (
|
||||
<article key={section.heading.en} className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
@@ -250,7 +250,7 @@ export default function SeoPage({ slug }: SeoPageProps) {
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<section className="deferred-section rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
{copy.relatedHeading}
|
||||
</h2>
|
||||
@@ -284,7 +284,7 @@ export default function SeoPage({ slug }: SeoPageProps) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<section className="deferred-section rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
{copy.internalLinksHeading}
|
||||
</h2>
|
||||
@@ -292,7 +292,7 @@ export default function SeoPage({ slug }: SeoPageProps) {
|
||||
<SuggestedTools currentSlug={page.toolSlug} limit={4} />
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<section className="deferred-section rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
{copy.supportHeading}
|
||||
</h2>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
body {
|
||||
@apply bg-white text-slate-900 antialiased dark:bg-slate-950 dark:text-slate-100;
|
||||
font-family: 'Inter', 'Tajawal', system-ui, sans-serif;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* RTL Support */
|
||||
@@ -154,3 +155,8 @@
|
||||
.modal-content {
|
||||
animation: modalSlideUp 0.25s ease-out;
|
||||
}
|
||||
|
||||
.deferred-section {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: 1px 720px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user