Skip to content

Commit c2ecbc0

Browse files
committed
Enhance performance and UX
1 parent 09bc828 commit c2ecbc0

11 files changed

Lines changed: 142 additions & 52 deletions

File tree

index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
3838
<meta name="theme-color" content="#000000" />
3939

40+
<!-- Preload hero image for faster LCP on home route -->
41+
<link rel="preload" as="image" href="/images/hero-moon.avif" type="image/avif" />
42+
4043
<!-- Font Preconnect for Performance -->
4144
<link rel="preconnect" href="https://fonts.googleapis.com">
4245
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

public/_headers

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
Cache-Control: public, max-age=31536000, immutable
3737

3838
# Cache static images in public folder for 30 days (extended from 1 week)
39+
/images/*
40+
Cache-Control: public, max-age=2592000, s-maxage=2592000, stale-while-revalidate=86400
3941
/*.png
4042
Cache-Control: public, max-age=2592000, s-maxage=2592000, stale-while-revalidate=86400
4143
/*.jpg

src/App.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Toaster } from '@/components/ui/toaster';
22
import { PageLoader } from '@/components/ui/LoadingStates';
3-
import NewsletterPopup from '@/components/NewsletterPopup';
43
import { TooltipProvider } from '@/components/ui/tooltip';
54
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
65
import { BrowserRouter, Routes, Route } from 'react-router-dom';
@@ -45,6 +44,7 @@ const About = lazyWithPrefetch('/about', () => import('./pages/About'));
4544
const Tag = lazyWithPrefetch('/tags/:tag', () => import('./pages/Tag'));
4645
const TagsIndex = lazyWithPrefetch('/tags', () => import('./pages/TagsIndex'));
4746
const Search = lazyWithPrefetch('/search', () => import('./pages/Search'));
47+
const NewsletterPopup = React.lazy(() => import('@/components/NewsletterPopup'));
4848

4949
// Optimize QueryClient for better performance
5050
const queryClient = new QueryClient({
@@ -65,11 +65,13 @@ const AnalyticsWrapper = ({ children }: { children: React.ReactNode }) => {
6565

6666
const App = () => {
6767
const [cookieConsentIsOpen, setCookieConsentIsOpen] = React.useState<boolean>(false);
68+
const [shouldLoadNewsletter, setShouldLoadNewsletter] = React.useState<boolean>(false);
6869

6970
// Load Cloudflare Analytics only when user has consented to analytics
7071
React.useEffect(() => {
7172
if (typeof window === 'undefined') return;
72-
if (window.location.hostname !== 'imadlab.me') return;
73+
const hostname = window.location.hostname;
74+
if (hostname !== 'imadlab.me' && hostname !== 'www.imadlab.me') return;
7375

7476
loadScriptIfConsented('analytics', 'https://static.cloudflareinsights.com/beacon.min.js', {
7577
'data-cf-beacon': '{"token": "e8df18bc2d9d4512835bc2f9798f4b24"}',
@@ -79,6 +81,14 @@ const App = () => {
7981
});
8082
}, []);
8183

84+
React.useEffect(() => {
85+
if (typeof window === 'undefined') return;
86+
if (window.location.pathname.startsWith('/admin')) return;
87+
88+
const timer = window.setTimeout(() => setShouldLoadNewsletter(true), 500);
89+
return () => window.clearTimeout(timer);
90+
}, []);
91+
8292
const baseContent = (
8393
<>
8494
{/* Skip to content for keyboard users */}
@@ -134,7 +144,11 @@ const App = () => {
134144
<Footer onOpenCookiePrefs={() => setCookieConsentIsOpen(true)} />
135145
</AnalyticsWrapper>
136146
</BrowserRouter>
137-
<NewsletterPopup />
147+
{shouldLoadNewsletter ? (
148+
<Suspense fallback={null}>
149+
<NewsletterPopup />
150+
</Suspense>
151+
) : null}
138152
<CookieConsent isOpen={cookieConsentIsOpen} onOpenChange={setCookieConsentIsOpen} />
139153
</>
140154
);

src/components/ClickSpark.tsx

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const ClickSpark = ({
3232
}: ClickSparkProps) => {
3333
const canvasRef = useRef<HTMLCanvasElement>(null);
3434
const sparksRef = useRef<Spark[]>([]);
35-
const startTimeRef = useRef<number | null>(null);
35+
const animationFrameRef = useRef<number | null>(null);
3636
const prefersReducedMotion = usePrefersReducedMotion();
3737
const isCoarsePointer = useIsCoarsePointer();
3838
const disableEffects = prefersReducedMotion || isCoarsePointer;
@@ -83,21 +83,27 @@ const ClickSpark = ({
8383
[easing]
8484
);
8585

86-
useEffect(() => {
87-
if (disableEffects) {
88-
return;
89-
}
86+
const draw = useCallback(
87+
(timestamp: number) => {
88+
if (disableEffects) {
89+
animationFrameRef.current = null;
90+
return;
91+
}
9092

91-
const canvas = canvasRef.current;
92-
if (!canvas) return;
93-
const ctx = canvas.getContext('2d');
94-
if (!ctx) return;
95-
let animationId: number;
96-
const draw = (timestamp: number) => {
97-
if (!startTimeRef.current) {
98-
startTimeRef.current = timestamp;
93+
const canvas = canvasRef.current;
94+
if (!canvas) {
95+
animationFrameRef.current = null;
96+
return;
9997
}
100-
ctx?.clearRect(0, 0, canvas.width, canvas.height);
98+
99+
const ctx = canvas.getContext('2d');
100+
if (!ctx) {
101+
animationFrameRef.current = null;
102+
return;
103+
}
104+
105+
ctx.clearRect(0, 0, canvas.width, canvas.height);
106+
101107
sparksRef.current = sparksRef.current.filter((spark: Spark) => {
102108
const elapsed = timestamp - spark.startTime;
103109
if (elapsed >= duration) {
@@ -111,6 +117,7 @@ const ClickSpark = ({
111117
const y1 = spark.y + distance * Math.sin(spark.angle);
112118
const x2 = spark.x + (distance + lineLength) * Math.cos(spark.angle);
113119
const y2 = spark.y + (distance + lineLength) * Math.sin(spark.angle);
120+
114121
ctx.strokeStyle = sparkColor;
115122
ctx.lineWidth = 2;
116123
ctx.beginPath();
@@ -119,22 +126,43 @@ const ClickSpark = ({
119126
ctx.stroke();
120127
return true;
121128
});
122-
animationId = requestAnimationFrame(draw);
123-
};
124-
animationId = requestAnimationFrame(draw);
129+
130+
if (sparksRef.current.length > 0) {
131+
animationFrameRef.current = requestAnimationFrame(draw);
132+
} else {
133+
animationFrameRef.current = null;
134+
}
135+
},
136+
[disableEffects, duration, easeFunc, extraScale, sparkColor, sparkRadius, sparkSize]
137+
);
138+
139+
useEffect(() => {
140+
if (!disableEffects) {
141+
return () => {
142+
if (animationFrameRef.current !== null) {
143+
cancelAnimationFrame(animationFrameRef.current);
144+
animationFrameRef.current = null;
145+
}
146+
};
147+
}
148+
149+
sparksRef.current = [];
150+
const canvas = canvasRef.current;
151+
const ctx = canvas?.getContext('2d');
152+
ctx?.clearRect(0, 0, canvas?.width ?? 0, canvas?.height ?? 0);
153+
154+
if (animationFrameRef.current !== null) {
155+
cancelAnimationFrame(animationFrameRef.current);
156+
animationFrameRef.current = null;
157+
}
158+
125159
return () => {
126-
cancelAnimationFrame(animationId);
160+
if (animationFrameRef.current !== null) {
161+
cancelAnimationFrame(animationFrameRef.current);
162+
animationFrameRef.current = null;
163+
}
127164
};
128-
}, [
129-
sparkColor,
130-
sparkSize,
131-
sparkRadius,
132-
sparkCount,
133-
duration,
134-
easeFunc,
135-
extraScale,
136-
disableEffects,
137-
]);
165+
}, [disableEffects]);
138166

139167
const handleClick = (e: React.MouseEvent<HTMLDivElement>): void => {
140168
if (disableEffects) {
@@ -153,6 +181,10 @@ const ClickSpark = ({
153181
startTime: now,
154182
}));
155183
sparksRef.current.push(...newSparks);
184+
185+
if (animationFrameRef.current === null) {
186+
animationFrameRef.current = requestAnimationFrame(draw);
187+
}
156188
};
157189

158190
return (

src/components/Header.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ const Header = () => {
1212
const isCoarsePointer = useIsCoarsePointer();
1313

1414
useEffect(() => {
15-
const onScroll = () => setScrolled(window.scrollY > 8);
15+
const onScroll = () => {
16+
const nextScrolled = window.scrollY > 8;
17+
setScrolled((prev) => (prev === nextScrolled ? prev : nextScrolled));
18+
};
1619
onScroll();
1720
window.addEventListener('scroll', onScroll, { passive: true });
1821
return () => window.removeEventListener('scroll', onScroll);

src/components/Marquee.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,10 @@ const Marquee = ({ words, speed = 'normal' }: MarqueeProps) => {
3434

3535
return (
3636
<div className="relative w-full overflow-hidden py-4">
37-
<style>{`
38-
@keyframes marquee {
39-
0% { transform: translateX(0%); }
40-
100% { transform: translateX(-100%); }
41-
}
42-
.animate-marquee {
43-
animation: marquee ${duration} linear infinite !important;
44-
will-change: transform;
45-
}
46-
`}</style>
47-
<div className="flex whitespace-nowrap animate-marquee" style={{ animationDelay }}>
37+
<div
38+
className="flex whitespace-nowrap animate-marquee"
39+
style={{ animationDelay, animationDuration: duration }}
40+
>
4841
{/* Duplicate content to create seamless loop */}
4942
{[...shuffledWords, ...shuffledWords].map((word, index) => (
5043
<span

src/components/markdown/GfmMarkdown.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { ListItem } from './components/ListItem';
3939
import { ThematicBreak } from './components/ThematicBreak';
4040
import { MathRenderer } from './components/MathRenderer';
4141
import { MermaidBlock } from './components/MermaidBlock';
42+
import 'katex/dist/katex.min.css';
4243

4344
type RepositoryContext = {
4445
owner: string;

src/hooks/useAnalytics.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export const useAnalytics = () => {
2020
const sessionInitialized = useRef<boolean>(false);
2121
const pageViewIdRef = useRef<string | null>(null);
2222
const metadataRef = useRef<Awaited<ReturnType<typeof getAnalyticsMetadata>> | null>(null);
23+
const isProductionHost =
24+
typeof window !== 'undefined' &&
25+
(window.location.hostname === 'imadlab.me' || window.location.hostname === 'www.imadlab.me');
2326

2427
const getMetadata = useCallback(async () => {
2528
if (metadataRef.current) return metadataRef.current;
@@ -29,7 +32,7 @@ export const useAnalytics = () => {
2932
}, []);
3033

3134
useEffect(() => {
32-
if (!isAllowed('analytics')) return;
35+
if (!isProductionHost || !isAllowed('analytics')) return;
3336

3437
const sessionId = sessionIdRef.current;
3538

@@ -81,10 +84,10 @@ export const useAnalytics = () => {
8184
};
8285

8386
initSession();
84-
}, [getMetadata]);
87+
}, [getMetadata, isProductionHost]);
8588

8689
useEffect(() => {
87-
if (!isAllowed('analytics')) return;
90+
if (!isProductionHost || !isAllowed('analytics')) return;
8891

8992
const sessionId = sessionIdRef.current;
9093
const startTime = Date.now();
@@ -164,5 +167,5 @@ export const useAnalytics = () => {
164167
});
165168
}
166169
};
167-
}, [getMetadata, location.pathname]);
170+
}, [getMetadata, isProductionHost, location.pathname]);
168171
};

src/index.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,20 @@ All colors MUST be HSL.
382382
}
383383
}
384384

385+
@keyframes marquee {
386+
0% {
387+
transform: translateX(0%);
388+
}
389+
100% {
390+
transform: translateX(-100%);
391+
}
392+
}
393+
394+
.animate-marquee {
395+
animation: marquee 30s linear infinite;
396+
will-change: transform;
397+
}
398+
385399
/* Respect reduced motion preferences */
386400
@media (prefers-reduced-motion: reduce) {
387401
*,

src/main.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createRoot } from 'react-dom/client';
22
import App from './App.tsx';
3-
import 'katex/dist/katex.min.css';
43
import './index.css';
54
import { excludeFromAnalytics, includeInAnalytics, isOwnerExcluded } from './lib/consent';
65

0 commit comments

Comments
 (0)