<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Richard</title>
    <description>The latest articles on Forem by Richard (@_1b0775853bf0b80636466b).</description>
    <link>https://forem.com/_1b0775853bf0b80636466b</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3699884%2Fb0b9b4b7-239d-4423-9d02-f1052e3994d2.jpg</url>
      <title>Forem: Richard</title>
      <link>https://forem.com/_1b0775853bf0b80636466b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/_1b0775853bf0b80636466b"/>
    <language>en</language>
    <item>
      <title>title: How I Optimized My AI Image App from 3s to 300ms with Next.js &amp; Supabase</title>
      <dc:creator>Richard</dc:creator>
      <pubDate>Thu, 08 Jan 2026 08:02:27 +0000</pubDate>
      <link>https://forem.com/_1b0775853bf0b80636466b/title-how-i-optimized-my-ai-image-app-from-3s-to-300ms-with-nextjs-supabase-5gj8</link>
      <guid>https://forem.com/_1b0775853bf0b80636466b/title-how-i-optimized-my-ai-image-app-from-3s-to-300ms-with-nextjs-supabase-5gj8</guid>
      <description>&lt;p&gt;Hello Developers! 👋&lt;/p&gt;

&lt;p&gt;I recently launched &lt;a href="https://www.nanobanan.tech" rel="noopener noreferrer"&gt;Nanobanan Editor&lt;/a&gt;, an AI-powered image editing tool focusing on natural language prompts.&lt;/p&gt;

&lt;p&gt;While building the MVP was fun, I hit a massive roadblock: &lt;strong&gt;Performance&lt;/strong&gt;. Specifically, my "Community Feed" page was taking &lt;strong&gt;3-5 seconds&lt;/strong&gt; to load. For a user-facing gallery, that's unacceptable.&lt;/p&gt;

&lt;p&gt;In this post, I want to share how I diagnosed the bottleneck and optimized it to &lt;strong&gt;under 300ms&lt;/strong&gt; (a 10x improvement) using client-side rendering strategies and database indexing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Stack 🛠️&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Framework&lt;/strong&gt;: Next.js 14 (App Router)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Database&lt;/strong&gt;: Supabase (PostgreSQL)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Styling&lt;/strong&gt;: TailwindCSS&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Deployment&lt;/strong&gt;: Vercel&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The Problem: Traditional SSR Bloat 🐢&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Initially, I implemented the community page using standard Server-Side Rendering (SSR) in Next.js.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ The slow way (Simplified)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;force-dynamic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CommunityPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Blocking current thread to fetch database&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;images&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;generations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)...&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Gallery&lt;/span&gt; &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Why it was slow:**&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Blocking&lt;/strong&gt;: The HTML wouldn't stream until the database query finished.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Complex Query&lt;/strong&gt;: I was querying a large dataset without proper indexes.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Network&lt;/strong&gt;: The server-to-database round trip added latency for every single request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Solution: CSR + SWR + Indexes 🚀&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I decided to pivot from SSR to Client-Side Rendering (CSR) with a "Stale-While-Revalidate" strategy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Step 1: Switch to CSR with Skeleton Loading&lt;/p&gt;
&lt;/blockquote&gt;


&lt;/blockquote&gt;

&lt;p&gt;Key change: Show the UI &lt;em&gt;immediately&lt;/em&gt; (skeletons), then fetch data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;useSWR&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;swr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CommunityPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Non-blocking fetch&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSWR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/community/feed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetcher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SkeletonGrid&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;// Instant feedback&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Gallery&lt;/span&gt; &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Step 2: Intelligent Caching with SWR&lt;/p&gt;
&lt;/blockquote&gt;


&lt;/blockquote&gt;

&lt;p&gt;I used &lt;code&gt;swr&lt;/code&gt; to handle caching. If a user visits the community page, leaves, and comes back 10 seconds later, &lt;strong&gt;it loads instantly&lt;/strong&gt; from the cache without hitting the API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSWR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/community/feed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;dedupingInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Reuse data for 60s&lt;/span&gt;
    &lt;span class="na"&gt;revalidateOnFocus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;// Don't re-fetch just because I clicked a tab&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Step 3: Database Indexing (The Real MVP)&lt;/p&gt;
&lt;/blockquote&gt;


&lt;/blockquote&gt;

&lt;p&gt;This was the biggest win. I analyzed my SQL query:&lt;br&gt;
&lt;code&gt;SELECT * FROM generations WHERE is_public = true ORDER BY created_at DESC LIMIT 12&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I realized I was doing a sequential scan. I added a composite index in Supabase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_generations_public_created&lt;/span&gt; 
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;generations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_public&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_public&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Result*&lt;em&gt;: The database query time dropped from *&lt;/em&gt;~500ms** to &lt;strong&gt;~15ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Results 📊&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;First Load&lt;/strong&gt;: ~300ms (Skeleton UI visible instantly)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Repeat Visit&lt;/strong&gt;: &amp;lt; 50ms (Cache hit)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Lighthouse Score&lt;/strong&gt;: Jumped from 65 to 95.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Try it out&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can experience the speed difference live here:&lt;br&gt;
👉 **[Nanobanan Editor Community]&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;(&lt;a href="https://www.nanobanan.tech/)**" rel="noopener noreferrer"&gt;https://www.nanobanan.tech/)**&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy0d8mww0e9oodwq9obiy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy0d8mww0e9oodwq9obiy.png" alt=" " width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This journey taught me that while SSR is powerful, sometimes good old Client-Side Rendering with a smart caching strategy provides a snappier UX for feed-based pages.&lt;/p&gt;

&lt;p&gt;Let me know what you think of the app! Also, happy to answer any questions about the Next.js + Supabase stack in the comments. 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;webdev #javascript #programming #showdev&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ai</category>
      <category>nextjs</category>
      <category>supabase</category>
    </item>
  </channel>
</rss>
