verticallines

Loading...

Portfolio Website

Rahul Hathwar - 2025-01-01 - Modern Next.js Platform with Advanced Performance Optimization

Project Overview

This portfolio website represents a convergence of modern web development practices, thoughtful architecture, and creative design execution. Built with Next.js 15, TypeScript, and a carefully selected tech stack, it serves as both a comprehensive portfolio platform and a practical demonstration of production-grade web engineering.

Tech Stack:

  • Next.js 15 + React 18
  • TypeScript (strict mode)
  • TailwindCSS with custom design system
  • Framer Motion
  • MDX content management
  • Docker + Nginx deployment

Motivation & Design Philosophy

I wanted a portfolio website to serve as a unified "corner of the internet" to organize all my public details. The goal of this project was to create a website which functioned as a digital portfolio and to house additional content such as an "about me" section, a store, a blog, and more.

I've made previous attempts to create a personal website, however, I was never satisfied with my past attempts both functionally and stylistically. With this website, I wanted to include more creative appeal to the pages which showcased my skills such as the programming, 2D design, and 3D design pages. Additionally, I wanted this website to serve as an example of my web development capabilities.

Specialized Design Approach

As previously mentioned, I knew that I wanted to highlight my individual skills in their own specially designed "domains" rather than cramming everything related to my portfolio into one disorganized page. For each portfolio page, I added a twist to the design. For example, the programming portfolio page features a terminal-inspired design with monospace typography and command-line aesthetics. The use of the same navigation bar throughout the website was the glue to tie the designs together.


Technical Architecture

Strategic Technology Selection

In terms of functionality, my personal website was very simple as there was not a high rate at which data moved around and had to be reflected. However, I saw this website as more than a simple personal website. I wanted this website to be a playground for me to test various web technologies, and perhaps include a few overkill features just for fun which may later require a more robust technology stack.

For this reason, I chose to use React and Next.js. As these were popular tools in the industry, it was worth it for me to create this website with these tools as they would serve as a portfolio example. Since Next.js prioritized building static sites, I thought that it also was a good balance between my website's current technical needs and its future ones.

Additionally, with my desire to include both complex and unique styles in various parts of my website, I chose TailwindCSS as this tool provided a good balance between low-level flexibility and convenience. To bring some motion to the website, I used Framer Motion since it was a capable yet simple-to-use motion and animation library.

Static Export Architecture

The site uses Next.js with static export configuration (output: 'export'), compiling to pure HTML/CSS/JS. This architectural decision provides several key benefits:

  • Zero runtime costs: Hosted on static infrastructure (Nginx + Docker)
  • Maximum performance: No server-side processing bottlenecks
  • Simple deployment: Copy /out folder to any static host
  • CDN-ready: Easily distributed across edge networks
  • Extreme reliability: No servers to crash or databases to fail

Performance Optimization

The Re-render Problem: A Detective Story

During development, I noticed something odd: the programming portfolio page was making 3-4x more network requests than the 2D or 3D design pages, despite having fewer images. Using Chrome DevTools Network tab with HAR file exports, I captured concrete data: 101 requests on the programming page versus 59 on the 3D design page, with individual images being loaded multiple times.

This wasn't just a theoretical inefficiency. Scrolling felt slightly janky, clicking sidebar navigation links triggered sudden network spikes, and the browser cache setting made a massive difference (which it shouldn't for already-loaded content).

Finding the Culprit

The issue came down to how React tracks components. When you define a component inside another component's render function, React treats it as a completely new component on every render. Here's what was happening:

// BEFORE: Component defined inside render callback
buildSections("programming", (project, cover) => {
  const LazyImage = () => {
    // This creates a NEW component every single time
    // React sees a different function and remounts everything
  }
  return <LazyImage />
})

Every time the parent component updated (scroll events, state changes, navigation), the LazyImage function was redefined. React compared the old component to the new one, saw they were different function references, and completely remounted the component with fresh state. This meant:

  • New IntersectionObserver created
  • Image loading state reset
  • Network request triggered again
  • Previous observer never properly cleaned up

The Fix: Extract the component outside the render cycle and use React.memo to prevent unnecessary re-renders:

// AFTER: Stable component definition
const LazyImage: React.FC<Props> = React.memo(({ cover, projectName }) => {
  // Component identity is stable across renders
  // React preserves state and doesn't remount
})

Result: Network requests dropped from 101 to approximately 60, with each image loading exactly once.

The Scroll Event Disaster

Another culprit was a scroll event listener that updated component state on every pixel of scrolling:

// BEFORE: State update on every scroll event
useEffect(() => {
  function onScroll() {
    if (window.scrollY > 300) {
      setSideBarHeight("top-0")  // State update triggers full re-render
    }
  }
  window.addEventListener("scroll", onScroll)
}, [])

This caused the entire page to re-render constantly while scrolling, recreating all child components and triggering the image loading cascade. The solution: Remove the dynamic behavior entirely and use static CSS positioning.

Framer Motion vs. Traditional Event Handlers

I discovered a subtle but important performance difference between hover implementations:

// BAD: Creates state changes that propagate through entire tree
<div 
  onMouseEnter={() => setHoveredProject(projectKey)}
  onMouseLeave={() => setHoveredProject(null)}
>

// GOOD: Framer handles animation internally without parent re-renders  
<motion.div
  initial="hidden"
  whileHover="visible"
  variants={variants}
>

Traditional onMouseEnter/onMouseLeave events with state updates caused parent re-renders on every hover. Framer Motion's whileHover keeps the animation logic internal to the component, preventing state changes from bubbling up. This single change eliminated re-renders during user interaction.

Intelligent Image Loading System

The site implements a progressive image loading architecture that balances initial load performance with final image quality:

Dual-Folder Structure:

  • images_compressed/: JPG files at 85% quality (~500KB-2MB each)
  • images_uncompressed/: PNG originals (~10-50MB each)

Loading Strategy:

  1. Compressed images load immediately for instant visual feedback
  2. Intersection Observer watches for items entering viewport
  3. When visible, uncompressed versions load in background
  4. Seamless upgrade to high-quality imagery without blocking initial render
  5. Extension-agnostic fallback chains (tries .png, .jpg, .jpeg, .gif)

Critical Implementation Details:

The lazy loading needed careful memory management to prevent leaks:

const LazyImage = React.memo(({ cover, uncompressedCover }) => {
  const isMountedRef = useRef(true)
  
  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting && isMountedRef.current) {
        setImgSrc(uncompressedCover)
        observer.disconnect()  // Clean up immediately after loading
      }
    })
    
    return () => {
      isMountedRef.current = false  // Prevent setState after unmount
      observer.disconnect()
    }
  }, [uncompressedCover])
})

This approach reduces initial page weight by ~80% while maintaining visual quality for engaged users. The viewport detection uses a 50px root margin for predictive loading, so images start loading slightly before entering the viewport.

Advanced React Optimization Techniques

After identifying the re-render issues, I implemented a comprehensive optimization strategy using React's built-in performance tools.

Understanding React's Component Identity

React uses function references to track component identity. When you pass callbacks to child components, if those callbacks are redefined on every render, React thinks they're different and may trigger unnecessary updates. The solution is useCallback:

// BEFORE: New function created on every render
<buildSections("programming", (project, cover) => {
  return <ProjectCard project={project} />
}) />

// AFTER: Stable function reference
const renderProject = useCallback((project, cover) => {
  return <ProjectCard project={project} />
}, [/* dependencies */])

Memoization Strategy

The buildSections function loops through all projects, categorizes them, and generates JSX. This is expensive to recompute on every render. Using useMemo, I cached the entire result:

const sections = useMemo(() => {
  return buildSections(
    "programming",
    renderProject,      // Stable via useCallback
    renderCategory,     // Stable via useCallback
    renderWrapper,      // Stable via useCallback
    renderColumn,       // Stable via useCallback
    [2]
  )
}, [renderProject, renderCategory, renderWrapper, renderColumn])

Now sections only recomputes when the callbacks actually change (which they don't, since they're memoized).

Data-Driven Sidebar Navigation

The original sidebar had manually repeated code for each category link:

// BEFORE: Hardcoded repetition (40+ lines)
<Link href="#webdevelopment"><div>web development</div></Link>
<Link href="#gamedevelopment"><div>game development</div></Link>
<Link href="#serverdevelopment"><div>servers</div></Link>
// ... etc

This was refactored into a data-driven approach:

const SIDEBAR_CATEGORIES = [
  { id: 'webdevelopment', label: 'web development', filled: true },
  { id: 'gamedevelopment', label: 'game development', filled: false },
  // ...
]

const sidebarContent = useMemo(() => (
  SIDEBAR_CATEGORIES.map((category, index) => (
    <Link key={category.id} href={`#${category.id}header`}>
      {category.label}
    </Link>
  ))
), [])

Benefits: fewer lines of code, easier to maintain, automatically memoized, and the sidebar never re-renders unnecessarily.

Extracting Constants to Prevent Object Recreation

Every time you create an object literal in JSX, JavaScript allocates new memory:

// BEFORE: New object on every render
<motion.div variants={{ hidden: { opacity: 0 }, visible: { opacity: 1 } }} />

// AFTER: Single object created once
const DESCRIPTION_VARIANTS = {
  hidden: { opacity: 0, height: 0 },
  visible: { opacity: 1, height: "auto" }
}

<motion.div variants={DESCRIPTION_VARIANTS} />

This might seem minor, but when you have 20+ animated components, it adds up. Fewer allocations mean less work for the garbage collector.

Grid Overlay Optimization

The grid overlay component had a resize event listener that updated state directly, causing parent re-renders:

// BEFORE: Direct state updates on resize
function handleResize() {
  setTheImages(newImages)  // Causes parent to re-render
}
window.addEventListener('resize', handleResize)

// AFTER: Debounced with requestAnimationFrame
function handleResize() {
  if (rafIdRef.current) {
    cancelAnimationFrame(rafIdRef.current)
  }
  rafIdRef.current = requestAnimationFrame(() => {
    setTheImages(newImages)
  })
}

Combined with wrapping the component in React.memo, the grid only updates when actually necessary, and the updates are batched with the browser's rendering cycle.

Code Organization & Reusability

Shared Animation Variants: The codebase employs a centralized animation system (util/animation-variants.ts) that prevents duplication across 20+ components. Instead of defining sidebar animations, description expands, and entrance effects repeatedly, they're defined once and imported everywhere.

Component Architecture:

  • Terminal UI components provide consistent aesthetics across programming section
  • Reusable gallery builder supports arbitrary categories and layouts
  • Separation of concerns: content (JSON), presentation (components), logic (utilities)

Content Management System

MDX Integration

Portfolio articles are written in MDX, providing:

  • Rich markdown formatting with component embedding
  • Custom styled components for headings, paragraphs, lists
  • Proper typography with carefully tuned spacing (mt-8, mb-4, etc.)
  • Syntax highlighting for code samples
  • Frontmatter for metadata management

Data Architecture

JSON-Based Portfolio Data: Three JSON files (programming_portfolio_data.json, 2ddesign_portfolio_data.json, 3ddesign_portfolio_data.json) contain structured project metadata. This approach offers:

  • Easy content updates without touching code
  • Type safety through TypeScript interfaces
  • Build-time validation
  • Simple integration with CMS if needed later

Dynamic Gallery Builder: The build_sections.tsx component is the heart of portfolio rendering:

  • Takes page identifier and item renderer as parameters
  • Reads corresponding JSON file
  • Groups projects by category
  • Distributes items across configurable column count
  • Returns fully rendered sections with headers

This abstraction means adding a new portfolio project requires only updating JSON—no code changes needed.


Design Features

Terminal-Style Programming Section

The programming portfolio page features custom components that evoke command-line interfaces:

  • ItemBox component: Sci-fi themed containers with custom gradients (bg-scifibox1)
  • Section headers: Monospace typography with terminal aesthetics
  • Grid overlays: Subtle cyberpunk visual elements
  • Color scheme: Inspired by syntax highlighting themes

These elements create a cohesive "developer aesthetic" that resonates with the technical nature of the content.

Animation System

Framer Motion powers smooth, purposeful animations throughout:

  • Sidebar reveals: Consistent slide-in from left with smooth easing
  • Staggered children: List items animate in sequence for polish
  • Multi-phase showcases: Complex orchestrated transitions (see AnimatedText.tsx)
  • Page entrances: Subtle fade-ins that don't distract
  • Hover effects: Responsive feedback on interactive elements

All animations respect user preferences—prefers-reduced-motion could be respected if needed.

Custom Cursor System

Desktop users experience a unique dot-ring cursor that follows the mouse:

  • Pure CSS + JavaScript implementation
  • Hidden on mobile (invisible sm:visible) to avoid conflicts with touch
  • Adds personality without compromising usability
  • Global override: * { cursor: none !important; } in base CSS

Development Workflow

Tooling & Quality Assurance

Storybook Integration: Component development happens in isolation with Storybook, providing:

  • Visual testing of component states
  • Documentation for design system
  • Shareable component library
  • Regression testing against unintended changes

Code Quality:

  • TypeScript strict mode catches errors before runtime
  • ESLint enforces consistent code style
  • Bundle analyzer identifies optimization opportunities
  • Hot reload with Fast Refresh for rapid iteration

Deployment Infrastructure

Dockerized Nginx:

# docker-compose.yml serves built /out directory
# SSL/TLS configuration documented in STATICDEPLOYMENT.md
# Simple updates: rebuild static export, copy to container

Technical Challenges & Solutions

Challenge 1: Image Lazy Loading Complexity

Problem: Initial implementation used a custom React hook (useSmartImageLoading.ts) with 102 lines of complex state management. This created infinite loops due to dependencies between effects.

Solution: Simplified to inline useState/useEffect pattern with Intersection Observer. Proved that simpler is often better—40 lines of straightforward code beats 100 lines of clever abstraction.

Challenge 2: Static Export Constraints

Problem: Next.js static export doesn't support:

  • Server-side rendering
  • API routes
  • Incremental static regeneration
  • Dynamic image optimization

Solution: Worked within constraints through architectural adjustments:

  • All dynamic routes have paths generated at build time via getStaticPaths
  • Data sourced from JSON files available during build
  • Custom image optimization pipeline with Sharp
  • Client-side lazy loading for progressive enhancement

Challenge 3: Consistency Across Disparate Designs

Problem: Each portfolio section has unique styling, but site needs cohesive navigation and branding.

Solution:

  • Shared NavbarMain, SideDrawer, SocialMediaSideBar components
  • Unified color system in tailwind.config.js despite theme variations
  • Responsive layouts that work across all breakpoints
  • Accessibility maintained (semantic HTML, alt attributes, keyboard navigation)

Results & Impact

Performance Optimization Results

The optimization work yielded measurable improvements across all key metrics:

Network Efficiency:

  • Programming page requests: 101 → ~60 (41% reduction)
  • Images per image: 3-4x loads → 1x load (75% reduction)
  • Cache behavior: Fixed (cache now works correctly)

Re-render Performance:

  • Scroll-triggered re-renders: ~50+ per scroll → ~5 per scroll
  • Hash navigation re-renders: Eliminated completely (was ~5-10x)
  • Sidebar re-renders on interaction: Eliminated (was every hover)

Memory Management:

  • Object allocations: ~80% reduction through constant extraction
  • Memory leaks: Eliminated via proper cleanup and mounted checks
  • IntersectionObserver instances: Properly cleaned up after use

User Experience:

  • Smooth scrolling: No janky behavior during navigation
  • Instant interactions: Sidebar clicks don't trigger network spikes
  • Responsive feel: Animations remain smooth under all conditions

Core Web Vitals

  • First Contentful Paint: < 1s
  • Largest Contentful Paint: < 2s
  • Total Blocking Time: Minimal (static assets only)
  • Cumulative Layout Shift: Near zero (dimensions specified)
  • Bundle Size: Optimized through code splitting

Technical Demonstration

This site proves several key capabilities:

  1. Modern framework proficiency: Next.js 15 with advanced features
  2. Performance engineering: Measurable optimization strategies
  3. Clean architecture: Maintainable, documented codebase
  4. Production deployment: Docker, Nginx, SSL/TLS configuration
  5. Design sensibility: Technical capability doesn't preclude aesthetic judgment

User Experience

  • Intuitive navigation: Despite varied designs, users report easy wayfinding
  • Smooth performance: Animations enhance rather than hinder usability
  • Responsive design: Works seamlessly across desktop, tablet, mobile
  • Fast load times: Progressive loading means no waiting for full page

Lessons Learned

Performance is About Observation, Not Guesswork

The most valuable lesson from this project was learning to measure before optimizing. Without HAR file analysis and network inspection, I would have never discovered that images were loading 3-4 times each. Tools like Chrome DevTools aren't just for debugging errors; they're essential for understanding what your application is actually doing.

React's Mental Model Matters

Understanding how React tracks component identity through function references transformed how I write components. The difference between defining a component inside versus outside a render function seems subtle but has massive performance implications. This mental model applies to all callback props: if you're creating new functions on every render, you're probably causing unnecessary work.

Memory Leaks Are Sneaky

A component that works perfectly can still leak memory if it doesn't clean up properly. Event listeners need removal, observers need disconnection, and setState should never be called after unmount. These bugs don't crash your app; they just make it slower over time. Defensive programming with refs and cleanup functions prevents these subtle issues.

Simple Solutions Beat Clever Ones

The scroll position state management was removed entirely in favor of static CSS. The custom image loading hook was replaced with inline effects. The hardcoded sidebar became a simple array map. Often, the best optimization is deleting code that seemed necessary but actually wasn't.

Conclusion

This portfolio website embodies the principle that personal projects should meet professional standards. Every aspect, from the progressive image loading system to the memoized component architecture, demonstrates technical competence and attention to detail.

The performance optimization journey illustrated how modern web development extends beyond writing functional code. It requires systematic investigation, understanding framework internals, and translating technical constraints into better user experiences. Discovering that the programming page made 70% more network requests than it should have led to a deep dive into React's rendering behavior, ultimately making the entire application faster and more efficient.

The site succeeds as both a portfolio platform and a portfolio piece itself. It proves that identifying performance issues, narrowing down root causes, and implementing targeted optimizations is as much a part of web development as writing the initial code. The codebase follows industry best practices with clean architecture, reusable components, comprehensive documentation, and long-term maintainability.

Most importantly, this project demonstrates that personal projects can rival professional work in quality and sophistication when approached with the right mindset: build something functional first, measure its behavior honestly, optimize what actually matters, and never stop learning from what the data tells you.

Copyright © Rahul Hathwar. All Rights Reserved.