Author: Claude (AI Assistant)
Date: December 23, 2025
Project: Nikunj Khitha's Next.js Portfolio
This document outlines a comprehensive plan for optimizing animations and code quality across the portfolio. The codebase is well-structured with a Next.js 15 + TypeScript + Tailwind CSS + Framer Motion stack. The main areas for improvement include:
- Animation Performance - Reduce CPU/GPU overhead and improve frame rates
- Bundle Size Optimization - Reduce JavaScript payload
- Code Architecture - Improve maintainability and reduce redundancy
- Accessibility Enhancements - Better reduced motion support
- Mobile Performance - Address heavy effects on low-power devices
Current Issues:
- 15 floating particles with continuous animations running indefinitely
Math.random()calls during render cause hydration mismatches- No cleanup of intervals when component unmounts
Proposed Changes:
// welcome-screen.tsx
- {[...Array(15)].map((_, i) => (
+ // Pre-compute positions once, reduce to 8 particles
+ const PARTICLES = useMemo(() =>
+ Array.from({ length: 8 }, (_, i) => ({
+ id: i,
+ left: `${(i * 12.5) + Math.random() * 10}%`,
+ top: `${(i * 12.5) + Math.random() * 10}%`,
+ delay: i * 0.15,
+ duration: 3 + (i % 3)
+ })), []);Benefits:
- Reduces particle count from 15 → 8 (47% less animation overhead)
- Eliminates hydration mismatches with SSR-stable positions
- Memoized calculation prevents re-computation
Current State: ✅ Good
- Already has mobile/low-perf detection
- Respects
prefers-reduced-motion - Pauses animations when tab is hidden
Suggested Improvements:
| Area | Current | Proposed |
|---|---|---|
| Radiating circles on mobile | 2 circles | 1 circle |
| Overlay layers | 3 layers | 2 layers on mobile |
| Animation duration on mobile | 0.5-0.65s | 0.4-0.5s |
// page-transition-animation.tsx
- const circleCount = isMobile || isLowPerf ? 2 : 3;
+ const circleCount = isMobile || isLowPerf ? 1 : 3;
// Consider removing tertiary overlay on mobile entirely
+ {!isMobile && (
<motion.div className="fixed inset-0 z-[50]..." />
+ )}Current Issues:
- 1,452 lines of WebGL code running continuously
- Uses
@ts-nocheck- no TypeScript safety - Runs on ALL devices including mobile where cursor isn't visible
- No cleanup mechanism visible
Proposed Changes:
// fluid-cursor.tsx - Add device detection
const FluidCursor = () => {
const [shouldRender, setShouldRender] = useState(false);
useEffect(() => {
// Only render on desktop with pointer devices
const hasPointer = window.matchMedia('(pointer: fine)').matches;
const isDesktop = window.innerWidth >= 1024;
setShouldRender(hasPointer && isDesktop);
}, []);
useEffect(() => {
if (!shouldRender) return;
const cleanup = fluidCursor();
return () => cleanup?.();
}, [shouldRender]);
if (!shouldRender) return null;
return (
<div className="pointer-events-none fixed inset-0 -z-10">
<canvas id="fluid" className="h-screen w-screen" />
</div>
);
};Benefits:
- Saves ~300KB of WebGL processing on mobile devices
- Prevents unnecessary canvas initialization
- Adds proper cleanup mechanism
Current Issues:
- Running simultaneously with fluid cursor (redundant)
- Creates new Line objects on every mouse move
- No object pooling for animations
Proposed Optimizations:
// Implement object pooling for Lines
const LINE_POOL: Line[] = [];
function getLineFromPool(config: LineConfig): Line {
if (LINE_POOL.length > 0) {
const line = LINE_POOL.pop()!;
line.reset(config);
return line;
}
return new Line(config);
}
function returnLineToPool(line: Line): void {
LINE_POOL.push(line);
}Current State: ✅ Good
- Respects reduced motion preferences
- Uses animation gate for coordinated timing
- Has
viewport: { once: true }
Minor Improvements:
// fade-up.tsx
- const initial = prefersReducedMotion ? { opacity: 0 } : { y: 80, opacity: 0 };
+ const initial = prefersReducedMotion ? { opacity: 0 } : { y: 40, opacity: 0 };
+ // Reduce y offset from 80 to 40 for subtler, faster animations// fade-right.tsx
- const initial = prefersReducedMotion ? { opacity: 0 } : { x: -100, opacity: 0 };
+ const initial = prefersReducedMotion ? { opacity: 0 } : { x: -60, opacity: 0 };
+ // Reduce x offset from -100 to -60 for subtler animationsCurrent Issues:
- Uses
setTimeoutwithout cleanup - Animation runs even when tab is hidden
Proposed Changes:
// flip-words.tsx
useEffect(() => {
if (!isAnimating) {
const timeoutId = setTimeout(() => {
startAnimation();
}, duration);
return () => clearTimeout(timeoutId); // Add cleanup
}
}, [isAnimating, duration, startAnimation]);
// Add visibility detection
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
const handler = () => setIsVisible(!document.hidden);
document.addEventListener('visibilitychange', handler);
return () => document.removeEventListener('visibilitychange', handler);
}, []);Run analysis with: npm run analyze
Expected Large Dependencies:
| Package | Estimated Size | Optimization Strategy |
|---|---|---|
framer-motion |
~150KB | Use lazy loading for AnimatePresence |
react-icons |
Variable | Import only needed icons |
openai |
~50KB | Server-only import |
@tsparticles/* |
~100KB | Consider removal (unused?) |
// pages/_app.tsx
import dynamic from 'next/dynamic';
const FluidCursor = dynamic(
() => import('@/components/fluid-cursor'),
{ ssr: false }
);
const WelcomeScreen = dynamic(
() => import('@/components/welcome-screen'),
{ ssr: false }
);
const PageTransitionAnimation = dynamic(
() => import('@/components/page-transition-animation'),
{ ssr: false }
);Check if @tsparticles/* packages are actually used:
grep -r "tsparticles" src/If unused, remove from package.json:
@tsparticles/engine@tsparticles/react@tsparticles/slim
Current Structure:
src/animation/
├── fade-right.tsx
├── fade-up.tsx
└── flip-words.tsx
Proposed Refactor:
// src/animation/fade.tsx - Unified component
export interface FadeProps {
children: ReactNode;
direction: 'up' | 'right' | 'left' | 'down';
duration?: number;
delay?: number;
offset?: number;
whileInView?: boolean;
className?: string;
}
export default function Fade({
direction = 'up',
offset = 40,
...props
}: FadeProps) {
// Unified implementation
}Extract animation state management:
// src/hooks/useAnimationConfig.ts
export function useAnimationConfig() {
const prefersReducedMotion = useReducedMotion();
const { animationsReady } = useAnimationGate();
const isMobile = useScreenBreakpoint(640);
return {
shouldAnimate: animationsReady && !prefersReducedMotion,
duration: isMobile ? 0.2 : 0.4,
staggerDelay: isMobile ? 0.04 : 0.06,
};
}// src/hooks/usePerformanceMode.ts
export function usePerformanceMode() {
const [mode, setMode] = useState<'high' | 'low' | 'minimal'>('high');
useEffect(() => {
const cores = navigator.hardwareConcurrency || 4;
const isMobile = window.innerWidth < 768;
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReduced) setMode('minimal');
else if (isMobile || cores <= 4) setMode('low');
else setMode('high');
}, []);
return mode;
}Current: ✅ CSS supports prefers-reduced-motion
Add JavaScript support:
// src/contexts/motion-preference.tsx
export function MotionPreferenceProvider({ children }) {
const prefersReduced = useReducedMotion();
return (
<MotionConfig reducedMotion={prefersReduced ? 'always' : 'never'}>
{children}
</MotionConfig>
);
}Add to WelcomeScreen:
<motion.button
onClick={handleClose}
className="absolute right-8 top-8 text-muted-foreground"
variants={itemVariants}
aria-label="Skip intro animation and enter portfolio"
>
Skip Intro
</motion.button>| Effect | Desktop | Mobile | Rationale |
|---|---|---|---|
| Fluid Cursor | ✅ Enabled | ❌ Disabled | No cursor on touch |
| Cursor Trail | ✅ Enabled | ❌ Disabled | No cursor on touch |
| Welcome Particles | 15 | 5 | Battery savings |
| Page Transition | 3 layers | 1 layer | Faster navigation |
| Backdrop blur | ✅ Full | 0.5 opacity | GPU performance |
/* globals.css */
@media (max-width: 768px) {
.backdrop-blur-lg {
backdrop-filter: blur(4px); /* Reduced from default */
}
.animate-bounce,
.animate-pulse {
animation: none;
}
}Existing Infrastructure: Jest + React Testing Library configured
# Run existing tests
npm test
# Run with coverage
npm run test:coverage-
Lighthouse Audit (run in Chrome DevTools)
- Target: Performance score > 90
- Target: First Contentful Paint < 1.5s
- Target: Largest Contentful Paint < 2.5s
-
Bundle Size Check
npm run analyze
-
Animation Frame Rate
- Open Chrome DevTools → Performance tab
- Record while navigating
- Target: Consistent 60fps on desktop, 30fps+ on mobile
- Page transitions work smoothly
- Welcome screen can be dismissed
- Skills pills animate on scroll
- Project cards hover effects work
- Theme switching doesn't break animations
- Mobile navigation is smooth
- Reduced motion preference is respected
| Priority | Task | Impact | Effort |
|---|---|---|---|
| 🔴 High | Disable fluid cursor on mobile | High | Low |
| 🔴 High | Add cleanup to FlipWords | Medium | Low |
| 🟡 Medium | Reduce welcome particles | Medium | Low |
| 🟡 Medium | Dynamic import heavy components | High | Medium |
| 🟡 Medium | Consolidate Fade components | Low | Medium |
| 🟢 Low | Object pooling for cursor trail | Low | High |
| 🟢 Low | TypeScript for useFluidCursor | Low | High |
- Add
next/dynamicimports for FluidCursor, WelcomeScreen - Reduce particle count in WelcomeScreen from 15 → 8
- Add device detection to disable cursor effects on mobile
- Add
clearTimeoutcleanup to FlipWords - Reduce animation offsets (y: 80→40, x: -100→-60)
The portfolio has a strong foundation with thoughtful attention to accessibility (reduced motion support) and performance (mobile detection in page transitions). The main optimization opportunities are:
- Cursor effects - Currently running on all devices including mobile
- Welcome screen - Too many animated particles
- Bundle size - Dynamic imports can improve initial load
- Animation cleanup - Missing timeouts/intervals cleanup
Implementing the high-priority items should noticeably improve mobile performance and reduce battery drain without sacrificing the premium visual experience on desktop.