Next.js와 Supabase로 AI 이미지 앱을 3초에서 300밀리초로 최적화한 방법
Source: Dev.to
Hello Developers! 👋
I recently launched Nanobanan Editor, an AI‑powered image editing tool that works with natural‑language prompts. While the MVP was fun to build, the Community Feed page was taking 3–5 seconds to load—a performance problem I needed to solve.
Below is how I diagnosed the bottleneck and brought the load time down to under 300 ms (a 10× improvement) using client‑side rendering strategies and database indexing.
The Stack 🛠️
- Framework: Next.js 14 (App Router)
- Database: Supabase (PostgreSQL)
- Styling: TailwindCSS
- Deployment: Vercel
The Problem: Traditional SSR Bloat 🐢
Initially the community page was rendered with standard Server‑Side Rendering (SSR):
// ❌ The slow way (Simplified)
export const dynamic = "force-dynamic";
export default async function CommunityPage() {
// Blocking current thread to fetch database
const images = await supabase.from('generations').select('*')...;
return ;
}
Why it was slow
- Blocking – HTML didn’t stream until the database query finished.
- Complex query – No proper indexes on the large dataset.
- Network latency – Each request incurred a server‑to‑database round‑trip.
The Solution: CSR + SWR + Indexes 🚀
I switched to Client‑Side Rendering (CSR) with a Stale‑While‑Revalidate (SWR) strategy and added the necessary database indexes.
Step 1: Switch to CSR with Skeleton Loading
Show a UI skeleton instantly, then fetch the data in the background.
"use client";
import useSWR from "swr";
export default function CommunityPage() {
// Non‑blocking fetch
const { data, isLoading } = useSWR("/api/community/feed", fetcher);
if (isLoading) return ; // Instant feedback
return ;
}
Step 2: Intelligent Caching with SWR
Cache the response so a quick revisit doesn’t hit the API again.
const { data } = useSWR("/api/community/feed", fetcher, {
dedupingInterval: 60_000, // Reuse data for 60 s
revalidateOnFocus: false, // No refetch just because the tab gains focus
});
Step 3: Database Indexing (The Real MVP)
The original query performed a sequential scan:
SELECT * FROM generations
WHERE is_public = true
ORDER BY created_at DESC
LIMIT 12;
Adding a composite index solved the problem:
CREATE INDEX idx_generations_public_created
ON generations (is_public, created_at DESC)
WHERE is_public = true;
The query time dropped from ≈ 500 ms to ≈ 15 ms.
Results 📊
- First load: ~300 ms (skeleton UI appears instantly)
- Repeat visit: < 50 ms (cache hit)
- Lighthouse score: jumped from 65 to 95
Try It Out
Experience the speed difference live:

This journey showed that while SSR is powerful, a well‑designed CSR approach with smart caching and proper indexing can deliver a much snappier UX for feed‑based pages. Feel free to ask any questions about the Next.js + Supabase stack in the comments!