So, I built my portfolio from scratch. I made some tech choices that I'm genuinely excited about, and I want to walk you through why!

The first ever prod deployment of what is now this website
Next.js vs Vite + React Router
Look, I get it. Vite is fast. Like, stupidly fast. The dev server spins up in milliseconds, HMR is instant, and the build times are chef's kiss. But here's the thing, I didn't just choose Next.js for the dev experience (which is amazing, especially with App Router). I also chose it because of what it gives me out of the box that would take forever to set up with Vite.
The Next.js Magic
File-based routing that just works. No more wrestling with React Router configurations. Just create a folder, drop in a page.tsx
, and you're done. Super convenient for simple projects, and let's me spin them up asap.
Server Components by default. This is huge. Especially in projects where your homepage is static content, NextJS saves you the trouble by avoiding to load your client with JS for no reason.
Built-in optimizations that would take me days to implement with Vite. Image optimization, font optimization, bundle splitting; it all just works. And trust me, I definitely appreciate not having to think about webpack configs.
// This just works in Next.js
import Image from "next/image";
// Auto-optimized, lazy-loaded, responsive images
<Image
src="/projects/cool-project.png"
alt="My cool project"
width={800}
height={400}
/>;
Tailwind: The CSS Framework That Actually Makes Sense
I used to be a "vanilla CSS purist." I'd write my own grid systems, my own organized stylesheets, and I'd always think that I didn't need a CSS framework because that's how I learned it in highschool. I was wrong. So incredibly wrong.
Why Tailwind Demolished My CSS Workflow (In a Good Way)
Bootstrap feels like 2015. All those pre-built components look the same, you end up fighting against the framework more than working with it, and don't even get me started on the bundle size.
Material-UI (MUI) is overkill. Sure, it's great for enterprise dashboards where you need every component under the sun, but for a portfolio? I just want to style things quickly without importing a library that's bigger than React itself.
Tailwind is different. It's not giving me pre-built components. It's giving me a design system at the CSS level. Need responsive breakpoints? They're built in. Want to ensure your colors actually work together? The palette is already designed. For the same reason I'd choose Drizzle over Prisma for my ORM, having extremely convenient building blocks at your disposal is something I will always prefer over pre-built components/styles.
// This is readable, maintainable, and doesn't require context switching
<div className="bg-white dark:bg-gray-900 p-8 rounded-lg shadow-lg hover:shadow-xl transition-all duration-300">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Project Title
</h2>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
Project description...
</p>
</div>
But here's what really sold me: I stopped writing CSS files. Like, at all. Everything is in the component, it's completely portable, and I never have to hunt through stylesheets to figure out why something looks weird.
The dark mode story alone is worth it. dark:bg-gray-900
and I'm done. No CSS variables, no theme switching logic, no separate stylesheets. It just works.
Framer Motion: Making Things Feel Alive
On another note, animation libraries are usually either too simple (CSS transitions) or too complex. Framer Motion hits the sweet spot perfectly.
Parallax Scrolling That Doesn't Suck
I also wanted that smooth parallax effect across my website as the main theme. You know, where the stars in the background move slower than the foreground? Usually this involves a bunch of scroll event listeners, performance optimization, and prayer.
With Framer Motion:
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start start", "end start"],
});
const backgroundY = useTransform(scrollYProgress, [0, 1], ["0%", "50%"]);
return (
<motion.div style={{ y: backgroundY }}>
{/* Stars background that moves at different speeds */}
</motion.div>
);
That's it. No performance issues (except on mobile, more on that later), no janky scrolling (also except on mobile, more on that later), no manually calculating viewport positions. The useTransform
hook handles all the math, and it's buttery smooth.
Lenis: Smooth Scrolling That Actually Works
Browser scroll is... fine. But it's not great. It's jumpy on some devices, inconsistent across browsers, and just doesn't feel premium.
Lenis is different. It feels like native scrolling, but better.
// One hook, global smooth scrolling
useEffect(() => {
const lenis = new Lenis({
duration: 1.2,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
});
function raf(time: number) {
lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
}, []);
The difference is immediately noticeable. Scrolling feels intentional, smooth, and responsive. It's one of those details that most people won't consciously notice, but it makes the whole site feel more polished.
The Mobile Performance Nightmare (And How I Fixed It)
Everything was working great on desktop. Then I tested it on my phone and... yikes. The projects section was jittery, scrolling felt laggy, and the parallax effects were super janky.
The Problem: Too Much JavaScript on the Main Thread
My parallax calculations were running on every scroll event, and mobile browsers were not happy about it. The solution (as of now)? Throttle everything.
// Before: Smooth on desktop, terrible on mobile
const handleScroll = () => {
// Expensive calculations on every scroll event
updateParallaxPositions();
};
// After: Smooth everywhere
const handleScroll = useCallback(
throttle(() => {
updateParallaxPositions();
}, 16), // ~60fps
[]
);
I also reduced the transform ranges on mobile. Desktop can handle complex animations, but mobile devices need gentler effects:
const isMobile = useMediaQuery("(max-width: 768px)");
const parallaxRange = isMobile ? ["0%", "10%"] : ["0%", "50%"];
Image Optimization Was The Real Hero
Next.js Image component is legitimately magic. It automatically serves WebP when supported, generates multiple sizes for different screens, and lazy loads everything. My projects section went from loading 20+ full-resolution images to loading optimized thumbnails that scale up as needed.
The performance improvement was dramatic, especially on mobile where bandwidth matters.
TypeScript: The Safety Net I Didn't Know I Needed
This is one of my favourite parts. I really didn't expect to love TypeScript as much as I do.
The autocomplete alone is worth it. But the real value is in refactoring. When I decided to restructure my project data, TypeScript caught every place I forgot to update. When I changed component APIs, it told me exactly what was broken. I legitimately cannot imagine going back to vanilla JS now.
interface ProjectData {
title: string;
description: string;
technologies: string[];
githubUrl?: string;
liveUrl?: string;
featured: boolean;
}
// This ensures I never forget required fields
const projects: ProjectData[] = [
{
title: "My Cool Project",
// TypeScript error if I forget description, technologies, etc.
},
];
Six months from now, I'll know exactly what data structure each component expects. That's the beauty of self-documenting code I guess.
The Final Stack
Here's what I ended up with and why:
- Next.js 14 with App Router for the framework
- TypeScript for sanity and developer experience
- Tailwind CSS for styling that doesn't make me cry
- Framer Motion for animations that feel intentional
- Lenis for smooth scrolling that actually works
- React Markdown for blog content rendering
This stack currently hits my sweet spot of developer experience, performance, and maintainability. Is it overkill for a portfolio? Maybe. Do I care? Not really, it's been super fun to learn everything! For a developer portfolio, that's really all you need.
If you build/built something similar, I'd love to see what tech choices you make!