perf: defer 3rd-party analytics, optimize responsive image sizes, and prioritize LCP hero banner
This commit is contained in:
parent
379fc1bb77
commit
c247f21d83
@ -1,8 +1,8 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Script from 'next/script';
|
||||
import './globals.css';
|
||||
import NextTopLoader from 'nextjs-toploader';
|
||||
import ClientLayout from './ClientLayout';
|
||||
import AnalyticsDeferred from '../components/AnalyticsDeferred';
|
||||
import { getSeoConfig } from '../lib/seo';
|
||||
import { vazirmatn, lalezar } from '../lib/fonts';
|
||||
import { generateOrganizationSchema } from '../lib/schema';
|
||||
@ -200,63 +200,12 @@ export default async function RootLayout({
|
||||
{children}
|
||||
</ClientLayout>
|
||||
|
||||
{/* Google Tag Manager Script (Deferred to idle) */}
|
||||
{isGtmEnabled && (
|
||||
<Script
|
||||
id="google-tag-manager"
|
||||
strategy="lazyOnload"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','${gtmId}');
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Google Analytics 4 (GA4) Scripts (Deferred to idle) */}
|
||||
{isGaEnabled && (
|
||||
<>
|
||||
<Script
|
||||
strategy="lazyOnload"
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
|
||||
/>
|
||||
<Script
|
||||
id="google-analytics"
|
||||
strategy="lazyOnload"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${gaId}', {
|
||||
page_path: window.location.pathname,
|
||||
});
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Microsoft Clarity Script (Deferred to idle) */}
|
||||
{clarityId && (
|
||||
<Script
|
||||
id="microsoft-clarity"
|
||||
strategy="lazyOnload"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function(c,l,a,r,i,t,y){
|
||||
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
||||
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||
})(window, document, "clarity", "script", "${clarityId}");
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* Deferred Analytics (GTM, GA4, Clarity) to unblock Main Thread */}
|
||||
<AnalyticsDeferred
|
||||
gtmId={isGtmEnabled ? gtmId : undefined}
|
||||
gaId={isGaEnabled ? gaId : undefined}
|
||||
clarityId={clarityId}
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
99
frontend/application/components/AnalyticsDeferred.tsx
Normal file
99
frontend/application/components/AnalyticsDeferred.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
dataLayer?: any[];
|
||||
clarity?: any;
|
||||
}
|
||||
}
|
||||
|
||||
interface AnalyticsDeferredProps {
|
||||
gtmId?: string;
|
||||
gaId?: string;
|
||||
clarityId?: string;
|
||||
}
|
||||
|
||||
export default function AnalyticsDeferred({ gtmId, gaId, clarityId }: AnalyticsDeferredProps) {
|
||||
useEffect(() => {
|
||||
let loaded = false;
|
||||
|
||||
const loadAnalytics = () => {
|
||||
if (loaded) return;
|
||||
loaded = true;
|
||||
|
||||
// Clean up event listeners
|
||||
window.removeEventListener('scroll', loadAnalytics);
|
||||
window.removeEventListener('touchstart', loadAnalytics);
|
||||
window.removeEventListener('mousemove', loadAnalytics);
|
||||
window.removeEventListener('keydown', loadAnalytics);
|
||||
|
||||
// 1. Google Tag Manager
|
||||
if (gtmId) {
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.dataLayer.push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
|
||||
const gtmScript = document.createElement('script');
|
||||
gtmScript.async = true;
|
||||
gtmScript.src = `https://www.googletagmanager.com/gtm.js?id=${gtmId}`;
|
||||
document.head.appendChild(gtmScript);
|
||||
}
|
||||
|
||||
// 2. Google Analytics 4 (if standalone without GTM)
|
||||
if (gaId && !gtmId) {
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(...args: any[]) {
|
||||
window.dataLayer?.push(args);
|
||||
}
|
||||
gtag('js', new Date());
|
||||
gtag('config', gaId, { page_path: window.location.pathname });
|
||||
|
||||
const gaScript = document.createElement('script');
|
||||
gaScript.async = true;
|
||||
gaScript.src = `https://www.googletagmanager.com/gtag/js?id=${gaId}`;
|
||||
document.head.appendChild(gaScript);
|
||||
}
|
||||
|
||||
// 3. Microsoft Clarity
|
||||
if (clarityId) {
|
||||
(function (c: any, l: any, a: any, r: any, i: any) {
|
||||
c[a] =
|
||||
c[a] ||
|
||||
function () {
|
||||
(c[a].q = c[a].q || []).push(arguments);
|
||||
};
|
||||
const t = l.createElement(r);
|
||||
t.async = 1;
|
||||
t.src = 'https://www.clarity.ms/tag/' + i;
|
||||
const y = l.getElementsByTagName(r)[0];
|
||||
y.parentNode.insertBefore(t, y);
|
||||
})(window, document, 'clarity', 'script', clarityId);
|
||||
}
|
||||
};
|
||||
|
||||
// Listen to first user interaction
|
||||
window.addEventListener('scroll', loadAnalytics, { passive: true, once: true });
|
||||
window.addEventListener('touchstart', loadAnalytics, { passive: true, once: true });
|
||||
window.addEventListener('mousemove', loadAnalytics, { passive: true, once: true });
|
||||
window.addEventListener('keydown', loadAnalytics, { passive: true, once: true });
|
||||
|
||||
// Fallback: idle execution after 3.5s
|
||||
const timer = setTimeout(() => {
|
||||
if (typeof window !== 'undefined' && 'requestIdleCallback' in window) {
|
||||
(window as any).requestIdleCallback(loadAnalytics, { timeout: 2000 });
|
||||
} else {
|
||||
loadAnalytics();
|
||||
}
|
||||
}, 3500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
window.removeEventListener('scroll', loadAnalytics);
|
||||
window.removeEventListener('touchstart', loadAnalytics);
|
||||
window.removeEventListener('mousemove', loadAnalytics);
|
||||
window.removeEventListener('keydown', loadAnalytics);
|
||||
};
|
||||
}, [gtmId, gaId, clarityId]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -72,7 +72,7 @@ function ProductCard({ product, priority = false }: { product: Product; priority
|
||||
src={product.image}
|
||||
alt={nameFa}
|
||||
priority={priority}
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw)"
|
||||
sizes="(max-width: 640px) 180px, (max-width: 1024px) 240px, 280px)"
|
||||
className="w-full h-full group-hover:scale-105 transition-transform duration-500"
|
||||
imgClassName="object-contain w-full h-full max-h-[160px] sm:max-h-[200px] mx-auto"
|
||||
/>
|
||||
|
||||
@ -138,7 +138,7 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="whitespace-nowrap flex flex-nowrap justify-center lg:justify-start gap-2 sm:gap-4 sm:gap-6">
|
||||
<div className="whitespace-nowrap flex flex-nowrap justify-center lg:justify-start gap-2 sm:gap-4 sm:gap-6 min-h-[58px]">
|
||||
<button
|
||||
onClick={() => {
|
||||
const element = document.getElementById('canino-advisor');
|
||||
@ -173,9 +173,11 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne
|
||||
onMouseEnter={() => setIsPaused(true)}
|
||||
onMouseLeave={() => setIsPaused(false)}
|
||||
>
|
||||
<div className="relative z-10">
|
||||
<div
|
||||
className="aspect-square bg-gradient-to-br from-slate-200 to-slate-300 rounded-[2.5rem] overflow-hidden border-4 border-white shadow-2xl relative group touch-pan-y"
|
||||
<div className="relative aspect-square w-full max-w-md mx-auto">
|
||||
{/* Glow backdrop behind image */}
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-canina-blue/20 via-canina-gold/15 to-transparent rounded-3xl blur-2xl transform -rotate-3 scale-95 pointer-events-none" />
|
||||
|
||||
<div className="relative rounded-3xl overflow-hidden shadow-2xl border-4 border-white bg-medical-gray-100 aspect-square w-full"
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchMove={onTouchMove}
|
||||
onTouchEnd={onTouchEnd}
|
||||
@ -199,7 +201,9 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne
|
||||
alt={banner.title || "Canina Pharma Germany"}
|
||||
fill
|
||||
priority={idx === 0}
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 600px"
|
||||
fetchPriority={idx === 0 ? "high" : "auto"}
|
||||
loading={idx === 0 ? "eager" : "lazy"}
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 500px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</motion.div>
|
||||
@ -211,7 +215,9 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne
|
||||
alt={activeBanner?.title || "Canina Pharma Germany"}
|
||||
fill
|
||||
priority
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 600px"
|
||||
fetchPriority="high"
|
||||
loading="eager"
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 500px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -26,8 +26,8 @@ export default function SafeImage({
|
||||
imgClassName,
|
||||
fallbackText = "در حال بروزرسانی تصویر...",
|
||||
priority = false,
|
||||
sizes = "(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 400px",
|
||||
quality = 85,
|
||||
sizes = "(max-width: 640px) 200px, (max-width: 1024px) 300px, 400px",
|
||||
quality = 75,
|
||||
width = 500,
|
||||
height = 500,
|
||||
}: SafeImageProps) {
|
||||
|
||||
@ -276,28 +276,14 @@ export class ProductService {
|
||||
|
||||
public async getFeaturedProducts(): Promise<Product[]> {
|
||||
try {
|
||||
const response = await api.get('/products?limit=30');
|
||||
const all = response.data.data.map((item: Record<string, unknown>) => this.mapBackendToFrontend(item));
|
||||
const featured: Product[] = [];
|
||||
const categoriesSeen = new Set<string>();
|
||||
|
||||
for (const p of all) {
|
||||
if (p.category && !categoriesSeen.has(p.category)) {
|
||||
featured.push(p);
|
||||
categoriesSeen.add(p.category);
|
||||
}
|
||||
if (featured.length === 4) break;
|
||||
if (typeof window === 'undefined') {
|
||||
const data = await this.serverFetch('/products?limit=8', ['products', 'featured-products'], 3600);
|
||||
const list = Array.isArray(data?.data) ? data.data : Array.isArray(data) ? data : [];
|
||||
return list.slice(0, 4).map((item: Record<string, unknown>) => this.mapBackendToFrontend(item));
|
||||
}
|
||||
|
||||
if (featured.length < 4) {
|
||||
for (const p of all) {
|
||||
if (!featured.some(f => f.id === p.id)) {
|
||||
featured.push(p);
|
||||
}
|
||||
if (featured.length === 4) break;
|
||||
}
|
||||
}
|
||||
return featured;
|
||||
const response = await api.get('/products?limit=8');
|
||||
const list = Array.isArray(response.data?.data) ? response.data.data : [];
|
||||
return list.slice(0, 4).map((item: Record<string, unknown>) => this.mapBackendToFrontend(item));
|
||||
} catch (error) {
|
||||
console.error("[ProductService] Failed to fetch featured products:", error);
|
||||
return [];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user