perf: defer 3rd-party analytics, optimize responsive image sizes, and prioritize LCP hero banner
Some checks failed
Deploy Canina / deploy (push) Successful in 2m2s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 7s

This commit is contained in:
parsa aghaei 2026-09-02 16:38:04 +03:30
parent 379fc1bb77
commit c247f21d83
6 changed files with 128 additions and 88 deletions

View File

@ -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>
);

View 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;
}

View File

@ -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"
/>

View File

@ -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>

View File

@ -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) {

View File

@ -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 [];