<?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>DEV Community: DeadLocker</title>
    <description>The latest articles on DEV Community by DeadLocker (@deadlocker).</description>
    <link>https://dev.to/deadlocker</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%2F3836362%2F4d9fd380-14cf-4e1e-9ed4-38978cb28084.png</url>
      <title>DEV Community: DeadLocker</title>
      <link>https://dev.to/deadlocker</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/deadlocker"/>
    <language>en</language>
    <item>
      <title>Why I Stopped Using LangGraph</title>
      <dc:creator>DeadLocker</dc:creator>
      <pubDate>Thu, 23 Apr 2026 16:54:32 +0000</pubDate>
      <link>https://dev.to/deadlocker/why-i-stopped-using-langgraph-4jo2</link>
      <guid>https://dev.to/deadlocker/why-i-stopped-using-langgraph-4jo2</guid>
      <description>&lt;p&gt;Most small LLM applications don't need a state graph framework. I know this because I used LangGraph in 8 out of 10 AI projects I built—and eventually replaced it in most of them.&lt;/p&gt;

&lt;p&gt;I want to be clear upfront: LangGraph is well-built software. The team behind it is sharp, the abstractions are thoughtful, and for genuinely complex multi-agent workflows, it earns its place. This isn't a takedown. It's a case for matching tools to problem size.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Pulled Me In
&lt;/h2&gt;

&lt;p&gt;The graph metaphor is compelling. Nodes for LLM calls, edges for transitions, a typed state object flowing through everything. When I first saw LangGraph's architecture diagram, it felt like someone had finally imposed order on the chaos of LLM non-determinism. As someone with a traditional software engineering background, that was reassuring.&lt;/p&gt;

&lt;p&gt;My first project was a document processing pipeline—upload PDFs, extract information, run multiple analysis passes, generate reports. Real branching logic, real dependencies between steps, real error handling concerns. LangGraph handled it well. I could visualize the flow, trace state through the graph, and reason about the whole system by looking at the diagram.&lt;/p&gt;

&lt;p&gt;For that use case, it was the right tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Turned
&lt;/h2&gt;

&lt;p&gt;The friction didn't come from a single failure. It accumulated gradually as overhead.&lt;/p&gt;

&lt;p&gt;The first sign was when I added multi-document comparison to that pipeline. My rigidly typed state object—clean and purposeful in v1—suddenly needed to handle lists of documents, keyed dictionaries of extracted data, and conditional type guards everywhere. Every node had to branch on "one document vs. many." The state schema became a maintenance burden.&lt;/p&gt;

&lt;p&gt;Then I started a new project: a simple chatbot. Answer questions over a knowledge base, remember conversation history, escalate to humans when confidence is low. I reached for LangGraph out of habit and built a state graph with nodes for retrieval, context assembly, response generation, confidence scoring, and escalation—plus conditional edges to route between them.&lt;/p&gt;

&lt;p&gt;When I stepped back and looked at what I'd built, the mismatch was clear.&lt;/p&gt;

&lt;p&gt;The code worked. But I had wrapped a linear pipeline with one branch in a state machine framework that required me to maintain type definitions, node signatures, and graph topology every time I wanted to tweak a prompt or adjust a threshold. The overhead of the framework was exceeding the complexity of the actual problem.&lt;/p&gt;

&lt;p&gt;The issue was that I'd been confusing "structured" with "complex." These applications weren't complex—they were sequential operations dressed up in graph because the framework made them feel more rigorous.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Question Worth Asking First
&lt;/h2&gt;

&lt;p&gt;Before reaching for LangGraph on any given project, ask one question: &lt;strong&gt;do I have a genuinely complex workflow with many conditional paths and coordination between agents, or am I building a pipeline?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A chatbot is a pipeline. Retrieve context, generate response, check confidence. A document processor is a pipeline. Extract, transform, output. A summarization tool is a pipeline. Feed in text, get summary. These are sequences of operations with maybe a branch or two. They don't need a state graph—they need well-structured code with clear separation of concerns.&lt;/p&gt;

&lt;p&gt;If the answer is "pipeline," you don't need LangGraph. You need functions, interfaces, and dependency injection.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Replaced It With
&lt;/h2&gt;

&lt;p&gt;I switched to the Vercel AI SDK with a hexagonal architecture—ports and adapters. The core idea is simple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM providers become adapters behind a shared interface:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/adapters/src/llm/types.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;EmbeddingModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;LanguageModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Tool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;LLMProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;largeModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LanguageModel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;mediumModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LanguageModel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;smallModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LanguageModel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;embeddingModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EmbeddingModel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;webSearchTool&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Tool&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenAI, Gemini, Ollama—they all implement this. Domain code never knows which one is running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents take a model through constructor injection:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/core/src/agents/SummaryAgent/agent.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generateText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LanguageModel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SummaryAgent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LanguageModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&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;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No graph, no state schema, no framework runtime. A class that takes a model and does its job. Swap Gemini for Ollama by changing what you inject. The agent doesn't change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory and embeddings follow the same pattern:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/adapters/src/memory/createFirestoreMemory.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EmbeddingModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;embed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createFirestoreMemory&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;embedFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;embeddingModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FirestoreAgentMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;embedFn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;collectionName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The domain layer gets a memory interface. It doesn't know Firestore is involved or which embedding model is running.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Actually Changed
&lt;/h2&gt;

&lt;p&gt;The difference isn't code length—it's that complexity stays proportional to the problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing became straightforward.&lt;/strong&gt; Pass a mock model into an agent constructor and test the logic. No graph runtime to simulate, no state transitions to set up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provider swaps are config changes.&lt;/strong&gt; Switching from OpenAI to Gemini meant updating one file in &lt;code&gt;packages/adapters&lt;/code&gt;. Zero changes to agent code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding features is low friction.&lt;/strong&gt; A new step in a pipeline means writing a function and calling it. No state schema updates, no node signature changes, no topology verification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Onboarding is fast.&lt;/strong&gt; The codebase uses patterns every TypeScript developer already knows—constructors, interfaces, composition. No library-specific mental model required.&lt;/p&gt;

&lt;h2&gt;
  
  
  When LangGraph Is Still the Right Call
&lt;/h2&gt;

&lt;p&gt;There are cases where LangGraph's value is clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-agent systems with complex coordination.&lt;/strong&gt; When agents need to share state and route to each other based on dynamic conditions, a graph model is a natural fit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflows with heavy human-in-the-loop patterns.&lt;/strong&gt; LangGraph's checkpointing and interrupt/resume capabilities are purpose-built for this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex decision trees with many conditional branches.&lt;/strong&gt; If your flow diagram looks like a subway map, a graph framework earns its keep.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building one of these, LangGraph is likely the right choice.&lt;/p&gt;

&lt;p&gt;But if you're building a chatbot, a RAG pipeline, a summarization tool, or a document processor—verify that you're solving your actual problem, not the framework's problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Heuristic
&lt;/h2&gt;

&lt;p&gt;Start with plain functions and dependency injection. Add a framework when the complexity of your coordination logic genuinely exceeds what straightforward code can express.&lt;/p&gt;

&lt;p&gt;For most small LLM applications, you'll never reach that point.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>langgraph</category>
      <category>agents</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I Joined My First Job and the Homepage Took Forever to Load</title>
      <dc:creator>DeadLocker</dc:creator>
      <pubDate>Sat, 21 Mar 2026 05:44:49 +0000</pubDate>
      <link>https://dev.to/deadlocker/i-joined-my-first-job-and-the-homepage-took-forever-to-load-e9o</link>
      <guid>https://dev.to/deadlocker/i-joined-my-first-job-and-the-homepage-took-forever-to-load-e9o</guid>
      <description>&lt;p&gt;Thinking back to my first real frontend job still stings.&lt;/p&gt;

&lt;p&gt;I wasn't a total coding newbie—I had a CS degree and plenty of full-stack school projects. But professional work? Zero experience. No production traffic, and no QA breathing down my neck about real users bouncing.&lt;/p&gt;

&lt;p&gt;When I joined, the e-commerce homepage felt brutally slow. QA kept dropping the same messages week after week:&lt;/p&gt;

&lt;p&gt;"Homepage lagging again 😭"&lt;/p&gt;

&lt;p&gt;"Hero + product grid take forever."&lt;/p&gt;

&lt;p&gt;"Users are leaving because of the 3s+ delay."&lt;/p&gt;

&lt;p&gt;I'd open the reports, see those agonizing waterfalls, and just feel paralyzed.&lt;/p&gt;

&lt;p&gt;The stack looked modern on paper: S3 + CloudFront + Single Page App (SPA). To make it "modular," we used iframes for sections like recommendations or banners. In theory, it was plug-and-play. In practice, it was a heavy, fragile mess.&lt;/p&gt;

&lt;p&gt;I could sense something was off, but I didn't have the vocabulary yet to explain why: &lt;strong&gt;every iframe was exposing a massive, redundant dependency graph that our monolith had hidden.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The seductive simplicity of the monolith
&lt;/h2&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%2F4kbfphui4cqwr2c2cupm.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%2F4kbfphui4cqwr2c2cupm.png" alt="iframe embedded every e-commerce pages" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A single SPA feels clean when you first build it. One repo, one deployment. If you need to reuse a feature elsewhere, an iframe seems like a pragmatic shortcut: wrap the app, drop it in, ship fast.&lt;/p&gt;

&lt;p&gt;That's the trap. An iframe doesn't just embed a UI; it embeds a runtime. We weren't just dropping a "banner" into the page; we were dropping an entire massive React ecosystem into a tiny window. Every "modular" section was forcing the browser to boot up a whole new instance of our giant store app.&lt;/p&gt;

&lt;p&gt;The system was technically modern and architecturally overloaded at the same time. &lt;strong&gt;The iframes didn't create this overload—they made it impossible to ignore.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The weight problem: too much code for too little UI
&lt;/h2&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%2F24jycupmhod4krm2mlay.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%2F24jycupmhod4krm2mlay.png" alt="The iframe trap" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is usually where the pain shows up first.&lt;/p&gt;

&lt;p&gt;A user opens a simple promotion page, but the browser ends up downloading the entire store application: product listing logic, product detail logic, event handling, routing, shared components — and whatever else had quietly accreted over time.&lt;/p&gt;

&lt;p&gt;That means you pay the cost of the whole mall just to visit one kiosk.&lt;/p&gt;

&lt;p&gt;A monolithic SPA often creates this problem because its bundle grows with the number of features, not with the size of the page the user is actually viewing. In practice, that leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;larger JavaScript payloads&lt;/li&gt;
&lt;li&gt;longer parse and execution time&lt;/li&gt;
&lt;li&gt;slower first interaction&lt;/li&gt;
&lt;li&gt;more memory pressure on weaker devices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The iframe made the mismatch &lt;strong&gt;visible&lt;/strong&gt;. Instead of loading a tiny, purpose-built promotion surface, the browser had to initialize a full app inside an embedded context. The user sees one feature, but the machine does full-system work. We had one big system masquerading as several smaller ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core performance lie:&lt;/strong&gt; the UI looks small, but the runtime cost is large—and the iframe architecture exposed this lie by forcing us to pay that cost multiple times on one page.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The invisible content problem: what crawlers can't easily see
&lt;/h2&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%2Fzn1ex7cw4tz2k2yh9rxn.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%2Fzn1ex7cw4tz2k2yh9rxn.png" alt="Crawler hidden content" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second failure mode is less visible, which is why teams often ignore it for too long.&lt;/p&gt;

&lt;p&gt;The crawlability problem started with our client-rendered SPA architecture.&lt;br&gt;
A client-rendered SPA already makes indexing fragile — search engines can execute JavaScript, but relying on that for your core product surface is a bet you probably shouldn't make. The iframe compounded this by adding another layer of indirection on top of an already shaky foundation.&lt;/p&gt;

&lt;p&gt;That's the distinction worth making: the SPA made content invisible by default. The iframe made it harder to fix by scattering content across multiple embedded contexts.&lt;br&gt;
If you strip out the iframe but keep a fully client-rendered product page, you still have a crawlability problem. The fix isn't just "remove the iframe" — it's "server-render the content, whether it's on the main page or the endpoint the iframe points to."&lt;/p&gt;




&lt;h2&gt;
  
  
  The part I got wrong
&lt;/h2&gt;

&lt;p&gt;For a while, I assumed the main problem was "performance" in the generic sense.&lt;/p&gt;

&lt;p&gt;So I looked for the usual suspects: network latency, image sizes, CDN headers, cache behavior. Those mattered — but they were not the whole story.&lt;/p&gt;

&lt;p&gt;The deeper issue was architectural shape.&lt;/p&gt;

&lt;p&gt;The app had too many responsibilities, too much shared code, and too many surfaces crammed inside one deployment unit. The iframe didn't create this problem—it &lt;strong&gt;revealed&lt;/strong&gt; it by making us instantiate our monolith multiple times. It exposed one big system that we had been pretending was several smaller ones.&lt;/p&gt;

&lt;p&gt;That was the mistake: blaming the messenger instead of the message.&lt;/p&gt;

&lt;p&gt;I had to admit I didn't have the answer yet — and then ask for time to fix the system instead of just firefighting its symptoms.&lt;/p&gt;

&lt;p&gt;So I went to my manager with a proposal: give the site a diet.&lt;/p&gt;

&lt;p&gt;Not a vague "let's optimize." A real plan.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reduce the bundle&lt;/li&gt;
&lt;li&gt;remove unused modules&lt;/li&gt;
&lt;li&gt;split pages into smaller pieces&lt;/li&gt;
&lt;li&gt;improve the build pipeline&lt;/li&gt;
&lt;li&gt;stop making every page carry the weight of every other page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That conversation mattered as much as the code changes.&lt;/p&gt;

&lt;p&gt;Sometimes ownership starts when you stop waiting for permission to understand the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually helped — and what we didn't do yet
&lt;/h2&gt;

&lt;p&gt;I want to be honest about the scope here.&lt;br&gt;
We didn't tear the monolith down completely. We didn't migrate to micro-frontends or rebuild from scratch. What we did was make the system smaller, cleaner, and easier to reason about — so that the next architectural decision wouldn't feel impossible.&lt;/p&gt;

&lt;p&gt;Think of it as load-bearing surgery before the real renovation.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Tree shaking and dead-code removal
&lt;/h3&gt;

&lt;p&gt;We stripped away modules that were bundled but never used.&lt;/p&gt;

&lt;p&gt;This sounds mundane, but dead code isn't harmless. It still has to be discovered, bundled, transmitted, parsed, and often evaluated. Every unused import is a small tax on runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SWC for faster builds
&lt;/h3&gt;

&lt;p&gt;We introduced SWC to improve build speed.&lt;/p&gt;

&lt;p&gt;That didn't just save developer time. It changed behavior. When builds are slow, people batch changes, avoid refactors, and hesitate to split code. When builds are fast, iteration feels cheaper. That makes architectural cleanup more realistic.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SWC didn't directly shrink our bytes, but it gave us the 10x faster build loops we needed to aggressively experiment with code-splitting without losing our minds.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Page splitting and better code splitting
&lt;/h3&gt;

&lt;p&gt;Instead of making every page carry the whole app, we split features into smaller delivery units.&lt;/p&gt;

&lt;p&gt;The rule was simple: if a user doesn't need a feature to view the current page, don't make them download it now.&lt;/p&gt;

&lt;p&gt;This is where the unit of deployment matters. If the app surface is large, the bundle shouldn't pretend otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;p&gt;The transformation wasn't subtle.&lt;/p&gt;

&lt;p&gt;Deployment time dropped from 10 minutes to about 2.&lt;/p&gt;

&lt;p&gt;Loading time fell from more than 3 seconds to under 1 second.&lt;/p&gt;

&lt;p&gt;Those aren't vanity metrics. They change how teams work. Faster deploys mean faster feedback. Faster feedback means more confidence. More confidence leads to better architecture decisions because people are no longer afraid to touch the code.&lt;/p&gt;

&lt;p&gt;And better architecture decisions compound.&lt;/p&gt;




&lt;h2&gt;
  
  
  The lesson: optimize by shrinking what you ask the browser to carry
&lt;/h2&gt;

&lt;p&gt;Here's the real takeaway.&lt;/p&gt;

&lt;p&gt;If your app is big enough to contain multiple product experiences, promotion systems, and routing behaviors, then embedding it via iframe for modular reuse is usually the wrong default—&lt;strong&gt;not because iframes are bad, but because they force you to pay the full cost of your monolith multiple times.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not because iframes are inherently bad.&lt;/p&gt;

&lt;p&gt;Not because SPAs are inherently bad.&lt;/p&gt;

&lt;p&gt;But because &lt;strong&gt;a bloated monolith embedded multiple times on one page creates a multiplicative performance disaster.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A better architecture usually does one or more of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;serves distinct product experiences as distinct pages with properly split bundles&lt;/li&gt;
&lt;li&gt;uses SSR or prerendering where crawlability matters&lt;/li&gt;
&lt;li&gt;splits bundles by route and by feature so no page carries weight it doesn't need&lt;/li&gt;
&lt;li&gt;keeps iframes for truly isolated embeds with minimal shared dependencies, not as a way to reuse a full application&lt;/li&gt;
&lt;li&gt;reduces shared runtime between unrelated surfaces&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is simple: make the browser load only what the user needs, let search engines see what matters, and make failures local instead of global.&lt;/p&gt;

&lt;p&gt;That's the bet I'd make again.&lt;/p&gt;

&lt;p&gt;Not that every frontend should be micro-frontends.&lt;/p&gt;

&lt;p&gt;Not that every SPA should be thrown out.&lt;/p&gt;

&lt;p&gt;But that when your app starts behaving like a whole ecosystem, &lt;strong&gt;the right move is to make the monolith small enough that embedding it doesn't break your performance.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The hardest part of this project wasn't the bundle size or the deployment time. It was admitting that the architecture was making the load-time slower and the product worse.&lt;/p&gt;

&lt;p&gt;Once I stopped treating the problem as a series of isolated bugs, I could see the pattern: too much code, too much coupling, and an iframe strategy that multiplied those sins instead of solving them.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I used AI assistance for this post, but the experiences and ideas are genuine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>frontend</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
