<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ankita Patil — Engineering Notes]]></title><description><![CDATA[Deep dives into React, Next.js, performance optimization, scalable frontend architecture, and practical engineering lessons from real-world projects.]]></description><link>https://blog.ankitapatil.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 10 Sep 2026 18:44:00 GMT</lastBuildDate><atom:link href="https://blog.ankitapatil.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a Production-Style AI System: Designing VibeFit from Image Upload to Intelligent Recommendations]]></title><description><![CDATA[Live Demo → Experience the application.

Architecture Walkthrough → Understand how the system works.

Source Code → Explore the implementation.

Project Deep Dive → Read the complete architecture and ]]></description><link>https://blog.ankitapatil.dev/building-production-ai-recommendation-system-langgraph-rag</link><guid isPermaLink="true">https://blog.ankitapatil.dev/building-production-ai-recommendation-system-langgraph-rag</guid><category><![CDATA[AI Engineering]]></category><category><![CDATA[langgraph]]></category><category><![CDATA[AI Agents Explained]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[pgvector]]></category><category><![CDATA[gemini]]></category><category><![CDATA[openai]]></category><category><![CDATA[vector embeddings]]></category><category><![CDATA[semantic search]]></category><category><![CDATA[System Design]]></category><category><![CDATA[System Architecture]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[agentic ai development]]></category><category><![CDATA[Orchestration]]></category><category><![CDATA[llm]]></category><category><![CDATA[AI Workflow]]></category><dc:creator><![CDATA[Ankita Patil]]></dc:creator><pubDate>Thu, 16 Jul 2026 07:51:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69916e90a0e1de594226e1d0/95fc9a17-46e1-4fcf-b234-f4438cb46566.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<ul>
<li><p><a href="https://vibe-fit-blond.vercel.app/">Live Demo</a> → Experience the application.</p>
</li>
<li><p><a href="https://www.loom.com/share/552e8099b6594e9685ce440ce8194865">Architecture Walkthrough</a> → Understand how the system works.</p>
</li>
<li><p><a href="https://github.com/Ankita-Nandkumar-Patil/vibefit">Source Code</a> → Explore the implementation.</p>
</li>
<li><p><a href="https://ankitapatil.dev/project-listing/vibe-fit">Project Deep Dive</a> → Read the complete architecture and engineering decisions.</p>
</li>
</ul>
<hr />
<p>I thought building an AI stylist would be about prompts.</p>
<p>It wasn't.</p>
<p>When I started VibeFit, I assumed most of my time would go into prompt engineering.</p>
<p>Instead, I spent far more time designing workflows, validating AI responses, building retrieval pipelines and thinking about system reliability.</p>
<p>That completely changed the way I think about AI engineering.</p>
<p>Today, VibeFit is much more than an application that recommends outfits. It's an end-to-end AI system that understands a user's wardrobe, stores it as semantic knowledge and retrieves only the relevant clothing before asking an LLM to generate recommendations.</p>
<p>This article is a walkthrough of how I designed that system and the engineering decisions that shaped it.</p>
<hr />
<h3><strong>What is VibeFit?</strong></h3>
<p>VibeFit is an AI-powered wardrobe assistant.</p>
<p>Users upload images of their clothing, and the system automatically understands each item using Gemini Vision. It extracts structured metadata such as category, color, fabric, season and occasion, generates semantic embeddings and stores everything in PostgreSQL using pgvector.</p>
<p>Later, when a user asks something like:</p>
<p>"What should I wear for a client meeting?"</p>
<p>the application doesn't send the entire wardrobe to Gemini.</p>
<p>Instead, it retrieves only the most relevant garments using semantic search and lets Gemini reason over that much smaller context.</p>
<p>That one architectural decision significantly improved recommendation quality while reducing hallucinations and token usage.</p>
<h3><strong>System Architecture</strong></h3>
<p>The application consists of two independent AI pipelines.</p>
<h3>1. Ingestion Pipeline</h3>
<p>Runs once for every uploaded clothing item.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69916e90a0e1de594226e1d0/0757ece8-38c0-4ad0-8e6a-c258d6cf8cfa.png" alt="AI ingestion pipeline architecture showing how a clothing image is transformed into semantic knowledge. The workflow includes image upload, Gemini Vision analysis, structured metadata extraction, business validation, OpenAI embedding generation, and storage in PostgreSQL with pgvector for semantic retrieval." style="display:block;margin:0 auto" />

<p>The goal of this pipeline is simple.</p>
<p>Convert an uploaded image into structured, searchable knowledge.</p>
<hr />
<h3>2. Retrieval Pipeline</h3>
<p>Runs whenever a recommendation is requested.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69916e90a0e1de594226e1d0/560a79a5-5427-448d-b726-286de0952f46.png" alt="AI retrieval pipeline architecture showing how a natural language styling request is converted into embeddings, matched against a PostgreSQL pgvector database using semantic search, and passed to Gemini to generate personalized outfit recommendations from the user's wardrobe." style="display:block;margin:0 auto" />

<p>Instead of asking the LLM to search hundreds of clothing items, the retrieval system performs that work first.</p>
<p>Gemini only focuses on reasoning.</p>
<hr />
<h3><strong>Engineering Decisions That Made the Biggest Difference Migrating from n8n to LangGraph</strong></h3>
<p>The first version of VibeFit was built in n8n.</p>
<p>It helped me validate the product quickly, but as the workflow became more complex, I started needing shared state, retries and conditional routing.</p>
<p>That's when I migrated the orchestration to LangGraph.</p>
<p>Instead of designing connected nodes, I started designing state transitions. Every node became responsible for a single task, while the graph handled execution.</p>
<p>It made the workflow much easier to maintain and extend.</p>
<hr />
<h3><strong>Structured Outputs Are Only Half the Story</strong></h3>
<p>One lesson I learned early is that valid JSON doesn't always mean valid data.</p>
<p>Gemini generates structured metadata using a predefined schema, but that alone isn't enough.</p>
<p>After parsing the response, I validate it against my own business rules before storing anything in the database.</p>
<p>The AI generates data.</p>
<p>The application decides whether that data can be trusted.</p>
<hr />
<h3><strong>Why I Didn't Generate Embeddings from Images</strong></h3>
<p>Most people assume image search starts with image embeddings.</p>
<p>In VibeFit, I take a different approach.</p>
<p>Gemini first converts the image into structured metadata. That metadata is transformed into a descriptive sentence, and only then do I generate embeddings.</p>
<p>Users search with intent, not pixels.</p>
<p>Representing the clothing semantically produced much better retrieval quality than relying only on visual similarity.</p>
<hr />
<h3><strong>PostgreSQL + pgvector Instead of Pinecone</strong></h3>
<p>Introducing another database would have added complexity without solving a real problem.</p>
<p>Every clothing item already had relational data like category, image URL and ownership. Keeping metadata and embeddings together inside PostgreSQL made the architecture simpler while still providing fast semantic search.</p>
<p>For the current scale, pgvector was the right tradeoff.</p>
<hr />
<h3><strong>What This Project Taught Me</strong></h3>
<p>Before building VibeFit, I thought AI engineering was mostly about choosing models and writing prompts.</p>
<p>Today, I think it's mostly software engineering.</p>
<p>The quality of the system depends on validation, retrieval, orchestration and reliability far more than it depends on clever prompts.</p>
<p>A good AI application isn't the one with the smartest model.</p>
<p>It's the one that gives the model exactly the information it needs and nothing more.</p>
<p>That shift in thinking changed the way I approach every AI project.</p>
<hr />
<h3><strong>What's Next?</strong></h3>
<p>There are several improvements I'd like to make as the project evolves.</p>
<p>Introduce a message queue for asynchronous processing. Add hybrid search and reranking. Version prompts and evaluate retrieval quality. Improve observability with tracing and workflow metrics. Introduce confidence scoring and recommendation validation.</p>
<p>The current architecture solves today's problems, but it's designed in a way that can evolve as the application grows.</p>
<hr />
<h3><strong>Resources</strong></h3>
<p>🚀 Live Demo: <a href="https://vibe-fit-blond.vercel.app/">https://vibe-fit-blond.vercel.app/</a></p>
<p>💻 GitHub: <a href="https://github.com/Ankita-Nandkumar-Patil/vibefit">https://github.com/Ankita-Nandkumar-Patil/vibefit</a></p>
<p>🎥 Loom Walkthrough: <a href="https://www.loom.com/share/552e8099b6594e9685ce440ce8194865">https://www.loom.com/share/552e8099b6594e9685ce440ce8194865</a></p>
<p>🌐 Detailed Architecture: <a href="https://ankitapatil.dev/project-listing/vibe-fit">https://ankitapatil.dev/project-listing/vibe-fit</a></p>
<hr />
<h3><strong>Final Thoughts</strong></h3>
<p>Building VibeFit taught me something I didn't expect.</p>
<p>The LLM is probably the least interesting part of the application.</p>
<p>The interesting engineering happens everywhere around it.</p>
<p>Designing reliable workflows, validating probabilistic outputs, building semantic retrieval and making different components work together is what turns an AI demo into an AI system.</p>
<p>That's the part of AI engineering I enjoy the most.</p>
]]></content:encoded></item><item><title><![CDATA[Moving the Brains to the Metal: My Local AI Setup with Gemma 4]]></title><description><![CDATA[Building for the web in 2026 means being an AI orchestrator. Most devs are just API consumers, but if you want to understand the stack, you move the intelligence to the metal. I recently migrated my w]]></description><link>https://blog.ankitapatil.dev/moving-the-brains-to-the-metal-my-local-ai-setup-with-gemma-4</link><guid isPermaLink="true">https://blog.ankitapatil.dev/moving-the-brains-to-the-metal-my-local-ai-setup-with-gemma-4</guid><category><![CDATA[gemma-4]]></category><category><![CDATA[Local LLM]]></category><category><![CDATA[ollama]]></category><category><![CDATA[gen ai]]></category><category><![CDATA[AI Engineering]]></category><category><![CDATA[AI Development Services]]></category><dc:creator><![CDATA[Ankita Patil]]></dc:creator><pubDate>Mon, 04 May 2026 13:15:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69916e90a0e1de594226e1d0/9b0da8a2-75f0-465b-a958-f7354bd39231.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Building for the web in 2026 means being an AI orchestrator. Most devs are just API consumers, but if you want to understand the stack, you move the intelligence to the metal. I recently migrated my workflow from cloud dependencies to a local-first setup.</p>
<p>Here is the technical breakdown of how I’m running <strong>Gemma 4</strong> on a constrained system to own my entire development lifecycle.</p>
<h3>The Stack: Why Gemma 4?</h3>
<p>I chose <strong>Gemma 4</strong> as the primary driver for its logic-to-weight ratio. While <strong>Qwen 3</strong> is a solid backup for multilingual reasoning, Gemma 4 hits the sweet spot for React/Next.js code generation and complex system architecture.</p>
<ul>
<li><p><strong>Privacy:</strong> Zero data leakage.</p>
</li>
<li><p><strong>Latency:</strong> Sub-millisecond response times.</p>
</li>
<li><p><strong>Context:</strong> No more worrying about token costs while debugging heavy repos.</p>
</li>
</ul>
<h3>Hardware vs. Tooling: Ollama or LM Studio?</h3>
<p>The choice of engine depends entirely on your system's "personality."</p>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><td><p><strong>Tool</strong></p></td><td><p><strong>Best For</strong></p></td><td><p><strong>Technical Edge</strong></p></td></tr><tr><td><p><strong>Ollama</strong></p></td><td><p>Terminal-centric devs</p></td><td><p>Lightweight, headless service. Best for 8GB RAM setups.</p></td></tr><tr><td><p><strong>LM Studio</strong></p></td><td><p>GPU-heavy machines</p></td><td><p>High-performance NVIDIA/CUDA offloading. Visual VRAM monitoring.</p></td></tr></tbody></table>

<p>Since I prioritize a minimalist, CLI-driven workflow, I went with <strong>Ollama</strong>. It keeps my system resources lean while serving the model as a local endpoint.</p>
<h3>The Workflow: Aider + Local LLM</h3>
<p>Aider is the bridge that makes local AI feel like a superpower. It’s an autonomous terminal tool that treats your local model like a senior pair programmer.</p>
<ol>
<li><p><strong>Orchestration:</strong> Point Aider to your local Ollama port.</p>
</li>
<li><p><strong>Execution:</strong> Ask for feature updates directly in the terminal.</p>
</li>
<li><p><strong>Result:</strong> Gemma 4 processes the intent, and Aider applies the git-aware edits to your code.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69916e90a0e1de594226e1d0/802eefc8-6295-4c6c-b7e6-e910713fe604.png" alt="Technical diagram of a local AI coding workflow using Ollama and Aider, showing hardware orchestration of the Gemma 4 model interacting with VS Code files." style="display:block;margin:0 auto" />

<h3>The 8GB RAM Constraint: Quantization is Key</h3>
<p>Running a 2026-tier model on 8GB of RAM is an exercise in optimization. To keep the machine from swapping to disk, I used <strong>4-bit quantization</strong>. This reduces the memory footprint by over 60% with negligible loss in coding accuracy.</p>
<p>The Technical Implementation</p>
<p><strong>Fire up the Engine (Ollama)</strong></p>
<p>Once the app is installed, pull the specific model version. I suggest starting with the 4-bit quantized version to save your RAM.</p>
<p>PowerShell</p>
<p>#<code>Pull the model to your local library</code></p>
<p><code>ollama pull gemma4:4b</code></p>
<p>#<code>Run and verify the model is active</code></p>
<p><code>ollama run gemma4:4b</code></p>
<p><strong>The Aider Integration</strong></p>
<p>Aider needs to know where your local "brain" is living. Since Ollama serves models on port <code>11434</code> by default, we point Aider there.</p>
<p>PowerShell</p>
<p>#<code>Install Aider in your project environment</code></p>
<p><code>pip install aider-chat</code></p>
<p>#<code>Launch Aider pointing to your local Gemma instance</code></p>
<p><code>aider --model ollama/gemma4</code></p>
<p><strong>Memory Management</strong> <strong>(The 8GB Survival Script)</strong></p>
<p>If your system is struggling, you can check exactly what is eating your memory before you start. I use a quick one-liner to find and kill heavy processes that aren't needed for the current build.</p>
<p>PowerShell</p>
<p>#<code>Check for the top 5 memory hogs</code></p>
<p><code>ps | sort –p ws | select –last 5</code></p>
<p><strong>Pro-tip:</strong> Kill your Docker containers and heavy browser caches before starting a long inference session. Every megabyte counts when you're running the brain and the dev server on the same chip.</p>
]]></content:encoded></item><item><title><![CDATA[Stop Building Digital Graveyards: Solving the AI Latency Tax with Shadow Ingestion. ]]></title><description><![CDATA[Most fashion apps fail, because the friction of data entry is higher than the quality of output. You download the app, spend 10 minutes manually tagging the shirt, then remember you have 50 more items]]></description><link>https://blog.ankitapatil.dev/ai-orchestration-shadow-ingestion</link><guid isPermaLink="true">https://blog.ankitapatil.dev/ai-orchestration-shadow-ingestion</guid><category><![CDATA[Product Engineering]]></category><category><![CDATA[AI Orchestration]]></category><category><![CDATA[n8n]]></category><category><![CDATA[gemini]]></category><category><![CDATA[llm]]></category><category><![CDATA[openai]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[fashion tech]]></category><dc:creator><![CDATA[Ankita Patil]]></dc:creator><pubDate>Thu, 23 Apr 2026 14:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69916e90a0e1de594226e1d0/316567b8-d9ed-4c62-9958-93915a9f01ea.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most fashion apps fail, because the friction of data entry is higher than the quality of output. You download the app, spend 10 minutes manually tagging the shirt, then remember you have 50 more items to go, and then eventually delete the app.</p>
<p>I have been working on a project that will match the vibe and not the stress. As a product engineer I set one rule for <strong>VibeFit</strong>: if user have to type "Yellow", "Cotton", or "Summer" , I've failed. In 2026 we don't build CRUD apps for users to deal with same stress on screen that they already bear off screen, instead we build <strong>intent-based engines.</strong></p>
<hr />
<p><strong>Orchestrating the invisible:</strong></p>
<p>Most devs go for monolith Express server to handle AI logic. But in AI driven product, your backend is the logic glue, it's not just a gatekeeper for a DB.</p>
<p>I chose <strong>n8n orchestration</strong>, and here's the engineering reality:</p>
<ul>
<li><p><strong>Observability over black box code</strong>: We've been developing the apps by checking logs and debuggers, and it do take time. When Gemini return a JSON object wrapped in markdown, I don't want to go through the logs, I want to see exactly where the pipe leaked on a visual canvas.</p>
</li>
<li><p><strong>Decoupling the Stylist</strong>: My Next.Js frontend should not care about how Gemini-2.5 Flash tags a photo. So I offloaded this to keep the <strong>Main Thread Lean</strong>, focusing on premium UX.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/69916e90a0e1de594226e1d0/0e46ac00-f64c-41c3-b001-2393a51b0b40.png" alt="A technical architecture diagram of the VibeFit ingestion pipeline. It shows an image upload from a Next.js frontend triggering an n8n orchestration workflow. The flow : visual tagging via Gemini-2.5 Flash and 1536-dimension vector embedding generation via OpenAI, finally syncing to a Supabase PostgreSQL database via a real-time subscription." style="display:block;margin:0 auto" />

<p>fig: The VibeFit Ingestion Pipeline: Turning raw pixels into structured semantic data.</p>
<hr />
<p><strong>The classic Latency Tax:</strong><br />Most AI products break (right at the UX), when you upload a photo, and the app shows a spinner for 8 seconds while the AI is thinking. For VibeFit I decided to treat this AI Thinking as a background task, and named it <strong>Shadow Ingestion</strong>.</p>
<p>How exactly Shadow Ingestion works:</p>
<ol>
<li><p><strong>The handshake</strong>: User uploads the image and UI returns a "Success" state in &lt;200ms.</p>
</li>
<li><p><strong>The Background task</strong>: An n8n webhook takes the image, hits Gemini 2.5 Flash for visual tagging, and OpenAI for a 1536-dimension embedding.</p>
</li>
<li><p><strong>The Reveal</strong>: As soon as background processing is done, the database updates, and the item is shown into the wardrobe via a Supabase real-time subscription.</p>
</li>
</ol>
<p>The user thinks its magic. The reality is just <strong>asynchronous orchestration hygiene</strong>.</p>
<hr />
<p><strong>The bottom line</strong>: AI product is only as good as its data glue. With manual ingestion, your app is already dead, no matter how better the ui/ux is. By automating the extraction of vibes and not just categories, I've built the foundation for a true RAG experience.</p>
<p>Next in the series: <em>The Math of Style: Why I chose pgvector for 1536-dimension similarity matching.</em></p>
]]></content:encoded></item><item><title><![CDATA[Your AI Stream is Fast, but Your UI is Lagging. Here’s Why.]]></title><description><![CDATA[We’ve all seen it. You’re building a sleek AI-chat interface or a real-time agent dashboard. The tokens are flying in from the server at light speed, but the moment you try to scroll or click a "Stop"]]></description><link>https://blog.ankitapatil.dev/main-thread-hygiene-javascript-performance</link><guid isPermaLink="true">https://blog.ankitapatil.dev/main-thread-hygiene-javascript-performance</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[performance]]></category><category><![CDATA[#AIOptimization]]></category><category><![CDATA[genai]]></category><category><![CDATA[Core Web Vitals]]></category><dc:creator><![CDATA[Ankita Patil]]></dc:creator><pubDate>Sat, 18 Apr 2026 09:00:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69916e90a0e1de594226e1d0/b4bd7404-a6f0-499c-9d81-72f1682bc987.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We’ve all seen it. You’re building a sleek AI-chat interface or a real-time agent dashboard. The tokens are flying in from the server at light speed, but the moment you try to scroll or click a "Stop" button, the tab feels like it's stuck in mud.</p>
<p>Most devs look at their React code or their Tailwind classes. They’re looking in the wrong place.</p>
<p>The problem isn't your UI library. The problem is that you’re <strong>Microtask-smothering</strong> your browser.</p>
<h2>The 2026 Conundrum: The "Infinite" VIP Line</h2>
<p>In the "classic" web, we had a few promises here and there. In 2026, we have <strong>Streams</strong>.</p>
<p>Every time your AI agent emits a token, or your local vector DB returns a similarity match, you’re likely firing a <code>Promise.resolve()</code> or an <code>await</code>. In JavaScript land, these are <strong>Microtasks</strong>.</p>
<p>Here’s the dirty secret: The Event Loop is a pushover. It sees a Microtask and says, <em>"Come right in, VIP!"</em> The problem? If your AI is streaming 60 tokens per second and you’re updating state for every single one, that "VIP line" never ends.</p>
<p>The browser wants to paint the next frame (the Macrotask). It’s literally begging to show the user the scroll animation. But the Event Loop says, <em>"Sorry, I have 400 more microtasks to finish first."</em></p>
<img src="https://cdn.hashnode.com/uploads/covers/69916e90a0e1de594226e1d0/84c6e0c8-7937-44ff-bdc0-108907a694ee.png" alt="diagram of the JavaScript Event Loop comparing a &quot;UI Rendering&quot; macrotask waiting in line while a stream of &quot;AI Token&quot; microtasks zoom past a bouncer." style="display:block;margin:0 auto" />

<h2>Why <code>setTimeout</code> is Obsolete</h2>
<p>For years, we "fixed" this by throwing a <code>setTimeout(fn, 0)</code> into the loop. It worked, but it was a blunt instrument. It forced the continuation to the back of the <em>entire</em> macrotask queue, losing its priority context.</p>
<p>In 2026, we use the <strong>Prioritized Task Scheduling API</strong>.</p>
<p><code>await scheduler.yield()</code> is the surgical way to breathe. Unlike <code>setTimeout</code>, it yields to the browser to let it paint, but then it picks your task back up with the <strong>same priority</strong> it had before. You get the fluidity of a paint-cycle without the overhead of re-queuing from scratch.</p>
<h2>Defending the INP (Interaction to Next Paint)</h2>
<p>If you’re building a Product, you aren't just "writing code"—you're managing <strong>Interaction to Next Paint (INP)</strong>.</p>
<p>If your AI loop consumes the thread for more than 50ms, your INP is going to blow past the 200ms "Good" threshold. You might think your app is fast because the tokens are appearing, but your user thinks it's broken because they can't click "Settings" while the AI is talking.</p>
<p><strong>Main Thread Hygiene</strong> is the new performance frontier.</p>
<p>JavaScript</p>
<pre><code class="language-plaintext">// Don't just loop. Orchestrate.
async function processAIStream(stream) {
  let lastYield = performance.now();

  for await (const token of stream) {
    updateUI(token);

    // If we've been working too long (50ms+), 
    // give the browser a chance to paint and handle user clicks.
    if (performance.now() - lastYield &gt; 50) {
      // The 2026 approach: Yield while maintaining priority
      if (typeof scheduler !== 'undefined' &amp;&amp; scheduler.yield) {
        await scheduler.yield();
      } else {
        // Fallback for older environments
        await new Promise(resolve =&gt; setTimeout(resolve, 0));
      }
      lastYield = performance.now();
    }
  }
}
</code></pre>
<h2>The Bottom Line</h2>
<p>In an era where AI can write the logic for you, your value as an engineer isn't in writing the loop—it's in <strong>orchestrating the execution.</strong> If you can't keep your INP under 200ms while running a local LLM or a heavy stream, you aren't building a 2026 product; you’re building a legacy site with a modern coat of paint.</p>
<p>Stop treating the Event Loop like a black box. <strong>Start yielding.</strong></p>
]]></content:encoded></item><item><title><![CDATA[Breaking Down a 68MB React Build: Architecture Fixes That Cut It to 21MB]]></title><description><![CDATA[Our React production build was 68MB.Not a typo. Sixty. Eight. MB.
Deployments were slow. Uploads were painful. And the easy excuse was:“Big app. Happens.”
Nah.
So I opened the bundle and started pulli]]></description><link>https://blog.ankitapatil.dev/react-build-size-optimization</link><guid isPermaLink="true">https://blog.ankitapatil.dev/react-build-size-optimization</guid><category><![CDATA[React]]></category><category><![CDATA[performance]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[bundle-optimization]]></category><category><![CDATA[webpack]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[System Architecture]]></category><category><![CDATA[System Design]]></category><dc:creator><![CDATA[Ankita Patil]]></dc:creator><pubDate>Mon, 09 Mar 2026 15:14:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69916e90a0e1de594226e1d0/d033e9f2-98a3-4524-8c7e-0e1d2cd92a51.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Our React production build was 68MB.<br />Not a typo. Sixty. Eight. MB.</p>
<p>Deployments were slow. Uploads were painful. And the easy excuse was:<br />“Big app. Happens.”</p>
<p>Nah.</p>
<p>So I opened the bundle and started pulling it apart.</p>
<p>This post walks through:</p>
<ul>
<li><p>How I analyze a production build when something feels off</p>
</li>
<li><p>What usually hides inside large React bundles</p>
</li>
<li><p>The architectural fixes that brought it down from <strong>68MB to 21MB</strong></p>
</li>
</ul>
<p>Because this isn’t about shaving numbers for fun.<br />It’s about faster deploys, better caching, quicker rollbacks — and not panicking on release day.</p>
<p>If you're shipping frontend to production, this stuff matters.</p>
<hr />
<h2>Step 1 — Quick Win: Kill Source Maps in Production</h2>
<p>First check: production was generating source maps.</p>
<p>Turned that off.</p>
<p><strong>68MB → 25MB instantly.</strong></p>
<p>Good win.<br />But that still left 25MB of actual application code.</p>
<p>So the real question became:</p>
<p><strong>What exactly are we shipping to users?</strong></p>
<hr />
<h2>Step 2 — What Are We Actually Shipping?</h2>
<p>A production bundle is basically a giant, minified blob.</p>
<p>Manually scanning files isn’t realistic.<br />So I generated build stats and ran them through a Webpack bundle analyzer.</p>
<p>The treemap visualization made it very obvious where the weight was coming from.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69916e90a0e1de594226e1d0/f79ab705-c9d5-427c-bc4e-50e10f3b4253.png" alt="Webpack Bundle Analyzer treemap showing Highcharts and other heavy dependencies inside the main chunk" style="display:block;margin:0 auto" />

<p>In a clean, role-based SaaS architecture:</p>
<ul>
<li><p>The main bundle should contain only core app logic</p>
</li>
<li><p>Feature libraries should load with their routes</p>
</li>
<li><p>Heavy libraries (charts, PDF tools, editors, etc.) should not sit in the entry chunk unless truly global</p>
</li>
</ul>
<p>The visualization made one thing clear — that separation wasn’t strict.</p>
<p>And that’s where the real fixes started.</p>
<hr />
<h2>Step 3 — Keep Route-Level Dependencies Out of the Main Bundle</h2>
<p>While reviewing the treemap, one thing stood out.</p>
<p><strong>Highcharts was inside the main bundle.</strong></p>
<p>That didn’t make sense.</p>
<p>Charts were used only inside dashboards.<br />Dashboards were lazy-loaded per role.</p>
<p>After tracing imports, I found this in the root entry file:</p>
<pre><code class="language-javascript">import Highcharts from "highcharts";

Highcharts.setOptions({
  chart: {
    style: {
      fontFamily: "'Poppins', sans-serif",
    },
  },
});
</code></pre>
<p>That one import was enough.</p>
<p>Even though routes were lazy-loaded, the dependency wasn’t.</p>
<p>So every single user — even those who never opened a dashboard — was downloading a charting library.</p>
<h3>Fix</h3>
<p>I moved the chart configuration into a dedicated chart module and imported it only inside chart-related components.</p>
<p>Rebuilt.</p>
<p><strong>25MB → 21MB.</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/69916e90a0e1de594226e1d0/caefd33e-c5cb-4f37-92c0-24f43983293d.png" alt="React Webpack Bundle Analyzer treemap after removing Highcharts from main bundle" style="display:block;margin:0 auto" />

<p>More importantly:</p>
<ul>
<li><p>Highcharts was removed from the main bundle</p>
</li>
<li><p>It now loads only when a dashboard loads</p>
</li>
<li><p>Route-based code splitting started behaving correctly</p>
</li>
</ul>
<p>This wasn’t just about shaving 4MB.</p>
<p>It was about making sure users download only what they actually use.</p>
<hr />
<h2>Step 4 — Don’t Ship Two Libraries for One Problem</h2>
<p>During dependency audit, both <code>moment</code> and <code>dayjs</code> were present.</p>
<p>Classic enterprise pattern.</p>
<p>Features get added. Libraries stick around.</p>
<p>But duplicate libraries mean:</p>
<ul>
<li><p>Bigger bundle</p>
</li>
<li><p>More cognitive load</p>
</li>
<li><p>More maintenance surface area</p>
</li>
</ul>
<p>Before removing anything, I evaluated:</p>
<ul>
<li><p>Actual feature usage in the codebase</p>
</li>
<li><p>Edge cases (timezones, parsing, formatting)</p>
</li>
<li><p>Bundle size impact using tools like Bundlephobia</p>
</li>
</ul>
<p><code>dayjs</code> covered our requirements with a smaller footprint.</p>
<p>So I standardized on <code>dayjs</code>, replaced remaining <code>moment</code> usage, and removed it.</p>
<p>Result:</p>
<ul>
<li><p>Leaner dependency graph</p>
</li>
<li><p>Smaller production build</p>
</li>
<li><p>Cleaner architectural boundary</p>
</li>
</ul>
<p>Optimization isn’t always about clever tricks.</p>
<p>Sometimes it’s about deliberate decisions.</p>
<hr />
<h2>Step 5 — Static Assets Matter Too</h2>
<p>JavaScript isn’t the only thing that grows silently.</p>
<p>Over time, the <code>/public</code> folder had accumulated unused images and static assets — old design iterations, deprecated illustrations, leftover icons.</p>
<p>These don’t show up in JS bundle analyzers, but they:</p>
<ul>
<li><p>Increase deployment size</p>
</li>
<li><p>Slow down uploads</p>
</li>
<li><p>Add unnecessary storage and CDN overhead</p>
</li>
</ul>
<p>A quick audit removed unused files and cleaned up the directory structure.</p>
<p>Performance isn’t just about code.</p>
<p>It’s about everything being shipped.</p>
<hr />
<h2>What I Look For in Enterprise Bundle Optimization</h2>
<p>From this experience, these are the patterns I now actively check:</p>
<h3>1. Entry Point Imports</h3>
<p>Anything imported in <code>index.tsx</code> or root files becomes part of the main bundle.</p>
<p>Global configuration imports must be intentional.</p>
<h3>2. Feature Isolation</h3>
<p>Route-based apps should reflect in bundle structure.</p>
<p>If dashboards are lazy-loaded, their heavy dependencies must be lazy-loaded too.</p>
<h3>3. Duplicate Libraries</h3>
<p>Multiple libraries solving the same problem should be evaluated and standardized.</p>
<h3>4. Heavy Libraries</h3>
<p>Charts, PDF tools, editors, icon packs — common bloat sources.</p>
<p>They should be lazy-loaded, dynamically imported, or replaced with lighter alternatives.</p>
<h3>5. Raw Code vs Dependency</h3>
<p>Sometimes small utilities are better implemented in-house instead of importing an entire package.</p>
<p>Dependency discipline matters in enterprise systems.</p>
<hr />
<h2>Final Results</h2>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Before</th>
<th>After</th>
</tr>
</thead>
<tbody><tr>
<td>Production Build</td>
<td>68MB</td>
<td>21MB</td>
</tr>
<tr>
<td>Minified Main Bundle</td>
<td>529.57 KB</td>
<td>431.56 KB</td>
</tr>
<tr>
<td>Main Bundle Content</td>
<td>Included route-level chart library</td>
<td>Clean core bundle</td>
</tr>
<tr>
<td>Date Libraries</td>
<td>Two</td>
<td>One</td>
</tr>
<tr>
<td>Deployment Time</td>
<td>Slower</td>
<td>Improved</td>
</tr>
</tbody></table>
<hr />
<h2>Key Learnings</h2>
<ul>
<li><p>Bundle size reflects architecture decisions.</p>
</li>
<li><p>Lazy loading works only when dependencies are isolated correctly.</p>
</li>
<li><p>Root-level imports can silently defeat code splitting.</p>
</li>
<li><p>Regular dependency audits are essential in growing codebases.</p>
</li>
<li><p>Optimization isn’t about removing libraries — it’s about structuring intentionally.</p>
</li>
</ul>
<p>This experience changed how I think about performance in React systems.</p>
<p>Instead of optimizing at the end, bundle structure is now treated as part of architecture design from day one.</p>
]]></content:encoded></item></channel></rss>