Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Events
Videos
Audiobooks
Packt Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds

WebDevPro

72 Articles
Kinnari Chohan
01 Jun 2026
14 min read
Save for later

WebDevPro #142: Thinking in Transitions: The mental shift React 19 makes hard to ignore

Kinnari Chohan
01 Jun 2026
14 min read
Crafting the Web: Tips, Tools, and Trends for Developers Advertise with Us|Sign Up to the Newsletter @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } WebDevPro #142 Thinking in Transitions: The mental shift React 19 makes hard to ignore Grow your Mac app with Setapp. Get around 30K unique impressions in the first days after your app’s release Setapp makes sure your app isn’t just listed, but seen. Plus, we handle the stuff you don’t like: distribution, licensing, billing, taxes, and customer support. You build great software; we bring you revenue and valuable feedback to help your app grow. Hope is not a growth strategy. Join Setapp. Share your app Meet the author This piece is written by Rodrigo Lobenwein. With a background in full stack development across .NET and React, Rodrigo leads a team of senior developers and QA specialists, focusing on the architectural decisions that sit between engineering and product. His work centers on helping teams develop the judgment to make the right technical calls not just follow the right frameworks. Rodrigo Lobenwein Tech Lead · Marlabs Brasil React developers learn a reliable reflex early on: update state, let the component re-render, trust the output. That model still holds. The friction starts when the work behind a state update grows large enough for users to actually feel it: a search input that lags on every keystroke while thousands of rows refilter behind it, a tab change that completes instantly in the code but arrives just late enough to make the UI feel sluggish. The instinct at that point is to reach for useMemo or useCallback. Those tools still matter, but they address unnecessary work. Sometimes the problem is different: React is being asked to treat urgent and non-urgent work as if they carry equal weight. Since version 18, the framework has had a better vocabulary for that distinction, and React 19 extends it further. Developing an intuition for when to apply it is what this piece is about. Before we dig deeper into this, here's a TL;DR you'll need: 🤖 Google I/O 2026 pushes deeper into the agentic AI era 📚 Storybook 10.4 brings improvements for component-driven development 🧩 Designing component architecture for React Server Components 🚀 Migrating from Express to Next.js with AI agents 🗺️ What clustering map tiles can teach us about problem solving Not Every Update Has the Same Urgency Consider a search screen. A user types a character into an input. Two things happen simultaneously: the text field shows the new value, and the results list re-renders against the new query. Those updates are related but not equally pressing. The input sits directly under the user’s fingers — any delay there registers immediately as a broken experience. The results list matters too, but a brief lag is far less perceptible than a laggy cursor. Most users won’t notice 100ms of stale results. They will notice 100ms of input latency. Instead of thinking only in terms of “state changed, render now,” concurrent React asks you to think about which update is urgent and which can wait for a better moment. That distinction is the conceptual foundation of concurrent rendering. Figure 01 - The two tiers of update urgency in concurrent React What Automatic Batching Actually Solved Before transitions make sense, it helps to separate them from automatic batching, which is an adjacent improvement that solves a different problem. In React 17, batching was largely confined to synchronous React event handlers. Multiple state updates inside a Promise callback, a setTimeout, or a fetch response were often processed as separate renders. More renders than necessary, without much upside. The createRoot API in version 18 extended automatic batching across more update sources: timers, Promise handlers, and most async callbacks. Many apps shed redundant render cycles without changing a single component. Batching is a reduction in render quantity. Transitions are about render priority. Even with batching in place, a single batched update that includes expensive list rendering can make an input feel sticky. Batching has no way to distinguish which part of that update the user is waiting on. Transitions provide that signal. Marking Lower-Priority Work with startTransition The startTransition API lets you label a state update as non-urgent. “Non-urgent” does not mean “unimportant” — it means React does not need to hold up higher-priority feedback to process it first. In a search interface, the pattern looks like this: const handleOnChange = (event) => { setInputValue(event.target.value); // urgent: runs first startTransition(() => { setQuery(event.target.value); // deferred: can wait }); }; Figure 2 — Without transitions, an expensive list render blocks the input. With transitions, the input stays instant and list work is interruptible. The transition wraps the update that causes the expensive rendering, not the expensive code itself. React is not being told to skip the work; it’s being given the context to sequence it correctly. A reliable heuristic: reach for a transition when a state update can trigger a large render, and the user does not need to see the result of that update instantaneously. Filtering a long list is a candidate. Updating the visible value in an input is not. When the UI needs to acknowledge that transition work is still in progress, useTransition returns both pieces: const [isPending, startTransition] = useTransition(); Use isPending for lightweight signals: dimming stale content, showing a small spinner. Resist the urge to let it take over the layout, its value is in supporting the interaction, not replacing it. When useDeferredValue Fits Better Both startTransition and useDeferredValue address the same class of problem. The choice between them turns on where you have control in the component tree. Figure 3 — The decision comes down to code ownership. Both APIs produce equivalent scheduling outcomes When the component owns both the input and the expensive update, startTransition is usually the cleaner solution: function SearchPage() { const [inputValue, setInputValue] = useState(""); const [query, setQuery] = useState(""); const [isPending, startTransition] = useTransition(); const handleOnChange = (event) => { const nextValue = event.target.value; setInputValue(nextValue); startTransition(() => { setQuery(nextValue); }); }; return ( <> <input value={inputValue} onChange={handleOnChange} /> <Results query={query} dimmed={isPending} /> </> ); } The component has direct access to both setState calls, so it can be explicit about which one should yield. When the value arrives from outside, from a parent, a form library, route state, or a shared component, the results component can’t reach the original setState. It can, however, choose to render against a deferred version of the value: function ResultsPanel({ query }) { const deferredQuery = useDeferredValue(query); const isStale = query !== deferredQuery; const items = useMemo( () => filterLargeList(deferredQuery), [deferredQuery] ); return <Results items={items} dimmed={isStale} />; } Here, query updates immediately in the parent; deferredQuery trails behind when rendering is busy. The expensive filtering follows the deferred value, so the input stays responsive regardless of how long the results take. How This Changes Component Design Concurrent rendering does not mean every piece of state needs a priority label. Most updates are cheap, and most components need no transitions whatsoever. The practical design question is more targeted: what part of this interaction must feel instant, and what part can follow slightly behind? Asking it tends to clarify both component boundaries and state placement. State driving the element the user is actively touching belongs on the urgent path. State driving a large subtree, expensive filtering, or a complex visual transformation is often a better fit for transition work. One underrated benefit of this model is interruptibility. Transition rendering can be abandoned. If a user types another character before the previous results render finishes, React discards the stale render and restarts with the latest input. That’s a meaningful improvement over older setTimeout-based workarounds, where React had no clear signal about which work was still relevant. The Shift Worth Internalizing The APIs themselves are small. startTransition wraps a state update. useDeferredValue returns a lagging version of a value. The more durable change is in how you reason about responsiveness. Older React code tends to treat rendering as a single block: state changes, render fires, UI reflects the result. Concurrent React asks for a slightly different mental model — one closer to how interface designers think about interaction. The thing under the user’s control responds first. Work that improves the screen but doesn’t need to be immediate follows behind. React performance is not only about preventing work. It is also about sequencing work so the app feels responsive where it matters most. Key Takeaways React 18 expanded automatic batching via createRoot, cutting unnecessary renders across async update sources. startTransition schedules a state update as lower priority without preventing it from running. useTransition returns [isPending, startTransition], letting you give subtle in-progress feedback while expensive work continues. useDeferredValue fits when a component receives a value from outside and can tolerate rendering against a slightly older version. Transition rendering is interruptible — stale work is abandoned when newer, more urgent updates arrive. The concurrent model reframes performance: not only as avoiding unnecessary work, but as sequencing necessary work by urgency. Want to read more on the topic? React and React Native, Sixth Edition covers React 19 and React Native from the ground up. 🎁 GIVEAWAY — JUNE 2026 Build AI Products Faster with Cursor, Lovable & Windsurf Subscribe to BuildWithAI and get our complete Vibe Coding with Cursor, Windsurf, and Lovable delivered free to your inbox. 📚 The Complete Vibe Coding Playbook Learn the exact workflows builders are using to: ✅ Turn ideas into working products in hours ✅ Build MVPs without getting stuck in code ✅ Use Cursor, Lovable & Windsurf effectively together ✅ Launch faster with AI-assisted development Subscribe to BuildWithAI and claim your free copy. Get instant access → This Week in the News 🤖 Google I/O 2026 pushes deeper into the agentic AI era: Google used I/O 2026 to double down on AI across its entire ecosystem, with major updates to Gemini, Search, developer tools, and agent-based workflows. The company introduced new models like Gemini Omni and Gemini 3.5 Flash, expanded its agent platform, and continued reshaping Search around AI-powered experiences. The bigger theme was clear: Google is moving beyond AI as a feature and positioning it as the foundation across products, workflows, and developer tooling. 📚 Storybook 10.4 brings improvements for component-driven development:Storybook 10.4 introduces a range of updates aimed at improving the developer experience around building, testing, and documenting UI components. The release continues Storybook’s focus on making component-driven development more efficient, with enhancements across workflows, tooling, and framework support. As frontend applications grow in complexity, tools like Storybook are becoming increasingly important for maintaining consistency and speeding up UI development. 🚀 Astro 6.4 goes full Rust on Markdown: Astro 6.4 joins the Rust rewrite club with Sätteri, a new Markdown processor that cut over a minute off real-world build times. It's opt-in for now via the new markdown.processor API, but the team has flagged it as the likely future default, migration cost being the remark/rehype plugin compatibility. 🤖 Claude Opus 4.8 drops as an incremental but meaningful upgrade: Better benchmark scores, sharper agentic judgment, and notably improved honesty (4× less likely to let code flaws slip by unremarked). Pricing stays the same. Also shipping alongside it: effort controls on claude.ai, and a "dynamic workflows" feature in Claude Code that can spin up hundreds of parallel subagents for codebase-scale tasks. 🌐 Web platform catches up on quality-of-life: April's Baseline digest brought some solid platform wins: contrast-color() auto-picks readable text against any background, Math.sumPrecise() fixes floating-point drift in array sums, the <search> element now hands you an ARIA landmark for free, and ARIA attribute reflection means cleaner element.ariaExpanded syntax over setAttribute. Beyond the Headlines ⚡ How is Linear so fast? A technical breakdown: Linear has become the benchmark for fast, responsive web applications, and this deep dive explores the engineering decisions behind that experience. From frontend architecture to rendering strategies and performance optimizations, the article breaks down the techniques that help the product feel almost instantaneous. The broader lesson is that performance is rarely the result of a single breakthrough. It comes from dozens of deliberate decisions across the stack, all working together to reduce friction and keep users in flow. 🧩 Designing component architecture for React Server Components: React Server Components change more than just where code runs. They also influence how applications are structured and how responsibilities are divided between components. This article explores architectural patterns for organizing components in an RSC-based application and avoiding common pitfalls. As more frameworks embrace server-first rendering, understanding these patterns is becoming increasingly important. The challenge is no longer just building components, but deciding which ones belong on the server and which ones truly need to run on the client. 📄 Bringing AI workflows to PDF-heavy applications: As AI agents become more capable, one challenge remains consistent: documents. This article explores how Foxit’s MCP Server connects AI systems with PDF workflows, enabling tasks such as document analysis, extraction, and processing without requiring developers to build custom integrations from scratch. The bigger trend is the rise of infrastructure that helps AI agents interact with existing business systems. Rather than generating text in isolation, agents are increasingly being equipped to work with the documents and workflows that power day-to-day operations. 🚀 Migrating from Express to Next.js with AI agents: This case study explores using AI agents to help migrate an application from Express to Next.js. Rather than focusing solely on code generation, it examines how agents can assist with larger engineering tasks such as understanding existing architecture, planning migrations, and implementing changes across a codebase. The interesting takeaway is that AI is increasingly being used as a collaborator on complex development projects. The challenge is no longer whether AI can write code, but how effectively it can help developers navigate and transform existing systems. 🗺️ What clustering map tiles can teach us about problem solving: In this post, Cassidy Williams walks through the challenge of clustering map tiles and the thought process behind solving it. Rather than focusing solely on the final solution, the article highlights the experimentation, trade-offs, and iterative thinking that go into tackling a seemingly simple problem. It's a good reminder that software engineering is often less about finding the perfect algorithm and more about breaking complex problems into manageable pieces and refining solutions along the way. The Developer Toolbox 🔥 Build reactive web apps without a framework runtime:Modern frontend frameworks often trade simplicity for abstractions. Flue takes a different approach, offering a reactive UI framework that focuses on minimal overhead, direct DOM updates, and a lightweight development experience. The project aims to deliver fine-grained reactivity without the complexity or runtime costs typically associated with larger frameworks. If you're interested in exploring alternative approaches to building fast, reactive web applications, Flue is worth a look. ⌨️ Handle keyboard shortcuts with minimal overhead:Keyboard shortcuts can quickly become messy as applications grow. Tinykeys is a lightweight JavaScript library that makes it easy to define and manage keyboard shortcuts with a simple, intuitive API. At under 1 KB, it supports complex key combinations and sequences without adding unnecessary weight to your application. If you're building power-user features, command palettes, or productivity-focused interfaces, Tinykeys offers a clean way to handle keyboard interactions. That’s all for this week. Have any ideas you want to see in the next article? Hit Reply! Cheers! Editor-in-chief, Kinnari Chohan 👋 Advertise with us Interested in sponsoring this newsletter and reaching a highly engaged audience of tech professionals? Simply reply to this email, and our team will get in touch with the next steps. 📢 Important: WebDevPro is Moving to Substack WebDevPro will soon move to Substack. Future issues will come from packtwebdevpro@substack.com so please add it to your contacts or whitelist it to keep receiving the newsletter without interruption. SUBSCRIBE FOR MORE AND SHARE IT WITH A FRIEND! *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}.image_block img+div{display:none}sub,sup{font-size:75%;line-height:0}#converted-body .list_block ol,#converted-body .list_block ul,.body [class~=x_list_block] ol,.body [class~=x_list_block] ul,u+.body .list_block ol,u+.body .list_block ul{padding-left:20px} @media (max-width: 100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width: 100%;display:none;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}.social_block .social-table{display:inline-block!important}} @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }
Read more
  • 0
  • 0

Kinnari Chohan
25 May 2026
18 min read
Save for later

WebDevPro #141: The CSS-First Mindset Modern Frontends Need Again

Kinnari Chohan
25 May 2026
18 min read
Crafting the Web: Tips, Tools, and Trends for Developers Advertise with Us|Sign Up to the Newsletter @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } WebDevPro #141 The CSS-First Mindset Modern Frontends Need Again Hi , For years, frontend development has carried a quiet assumption: when an interface needs to respond, animate, toggle, validate, or remember state, JavaScript should take the lead. That instinct made sense for a long time. Browsers were inconsistent, CSS was limited, and many interaction patterns genuinely needed script-heavy solutions to feel polished. That landscape has changed. Modern HTML and CSS now handle far more than layout and decoration. They can respond to form state, respect system preferences, manage disclosure patterns, animate page transitions, create motion paths, style modal backdrops, and support accessible behavior that developers previously had to rebuild from scratch. The question is no longer “Can JavaScript do this?” because, of course, it can. The better question is “Should JavaScript be responsible for this?” A CSS-first mindset does not mean rejecting JavaScript. It means giving the browser a fair chance before adding another dependency, state handler, event listener, or UI library. It asks developers to start with semantic HTML, layer CSS for presentation and interaction, and reserve JavaScript for the work it is uniquely good at: data fetching, complex application logic, persistence, coordination across systems, and genuinely dynamic behavior. That small shift can change the quality of a frontend codebase. Interfaces become lighter, easier to reason about, more resilient when something fails, and often more accessible by default. For intermediate developers, this is one of the most useful habits to build: not writing less code for the sake of minimalism, but choosing the browser-native path when it gives users a better result. Before we dig deeper into this, here's a TL;DR you need: 🦀 Bun’s massive Rust rewrite reignites the JavaScript tooling conversation 🔒 TanStack’s supply chain compromise shows how fragile npm security still is 🌐 Chrome and Edge explore a native <install> element for PWAs 🔒Shai Hulud returns with another package supply chain hit 🎥 TanStack Start enters the framework race with a different philosophy 🪶 Critical 8.0 brings CSS performance back to first paint The cost of reaching for JavaScript too early JavaScript is powerful, but power has a cost. Every script has to be downloaded, parsed, compiled, and executed. Every custom interaction adds more behavior to maintain. Every dependency introduces another layer of updates, bundle impact, and possible failure. On a fast machine and a strong connection, those costs may feel invisible. On a slower device, a busy network, or a page already carrying analytics, ads, and third-party widgets, they become much easier to feel. The issue is not that JavaScript is bad. The issue is that many teams use it as a default even when the browser already has the behavior built in. A simple accordion becomes a component with state. A modal becomes a focus-management exercise. A navigation highlight becomes a DOM query. A theme switcher becomes a script-first feature before CSS has been allowed to handle the visual logic. This matters because users experience the final result, not the elegance of our tooling. A lighter interface often loads faster, responds sooner, and fails more gracefully. It also tends to be easier for the next developer to understand. When structure lives in HTML, behavior comes from native elements, and styling expresses state through CSS, the codebase has fewer hidden moving parts. CSS is becoming contextual, not just decorative One of the most important changes in modern CSS is that it can respond to context. The :has() selector is a good example. For a long time, CSS could style children based on parents, descendants based on ancestors, and siblings in limited directions, but it struggled with parent-level conditions. Developers often added JavaScript just to say, “Style this container differently when something inside it is checked, active, hovered, or present.” That kind of logic now fits naturally into CSS. A fieldset can react when a checkbox inside it is selected. A navigation item can show an indicator only when it contains a submenu. A card list can blur non-hovered cards when one card is active. A layout can switch when a form control changes state. These are visual responses to visual conditions, which makes CSS a better home for them than JavaScript. The real takeaway is not simply that :has() is useful. It is that CSS is moving closer to how designers and developers think about interfaces. Components rarely exist in isolation. Their styling depends on state, nearby elements, available space, user preferences, and interaction context. The more CSS can express those relationships directly, the less we need to create artificial classes or scripts just to bridge the gap. There is still a performance and maintainability angle to consider. Broad relational selectors can ask the browser to do more work than necessary, especially on large pages. A focused selector that checks for a nearby .active element is easier to maintain than one that searches through a deeply nested structure. Like any powerful feature, :has() rewards restraint. Native elements carry behavior we often forget to value A CSS-first approach becomes even stronger when paired with semantic HTML. Elements such as <dialog>, <details>, and <summary> are not just markup conveniences. They carry interaction behavior that many teams have spent years recreating. Take modals. A custom modal usually needs background blocking, focus handling, Escape-key behavior, ARIA wiring, open and close logic, and backdrop styling. The native <dialog> element handles much of this through the browser. With showModal(), the page gets modal behavior without a large custom system. CSS can then style the dialog and its ::backdrop, while a small amount of JavaScript handles the moment it opens. Accordions tell a similar story. The <details> and <summary> elements provide disclosure behavior without building state from scratch. They work with keyboard interaction, keep content structured, and remain understandable even when CSS enhancements are unavailable. When multiple details elements share a name, they can behave like a grouped accordion where opening one closes another. That is a meaningful reduction in custom logic. This is where developers can level up quickly. Instead of asking, “Which package should I install for this UI pattern?” ask, “Does HTML already have a primitive for this?” Native primitives may not cover every product requirement, but they often provide a stronger baseline than a custom implementation written under deadline pressure. Accessibility improves when the browser keeps its job Many accessibility problems come from replacing native behavior with custom behavior and then forgetting to rebuild all the pieces users rely on. A div that behaves like a button still needs keyboard support, focus styles, roles, states, and expected interaction patterns. A custom accordion still needs to communicate its expanded state. A custom modal still needs to manage focus without trapping users in awkward ways. CSS-first and HTML-first thinking reduces that risk. Native controls already know a lot about input methods, assistive technologies, system preferences, and user expectations. A range input can be dragged, focused, and adjusted with the keyboard. A summary element can be opened without a custom click handler. A dialog can announce itself with the right labeling when structured properly. This does not remove responsibility from developers. Accessibility still requires care. Motion should respect prefers-reduced-motion. Theme choices should preserve contrast. Generated content should not be the only place important information exists, because pseudo-elements are not always announced. A CSS-generated counter, for example, may need an accessible label or equivalent text in the markup. The practical lesson is simple: let native behavior do as much as it reasonably can, then enhance it thoughtfully. Accessibility is not a polish step at the end. It is often a direct result of choosing the right primitive at the beginning. Progressive enhancement is back in a practical way Modern CSS features arrive at different speeds across browsers. That can make teams cautious, especially when they remember older eras of painful browser inconsistencies. The healthier approach is not to avoid new CSS entirely. It is to use progressive enhancement deliberately. Some features are safe because unsupported browsers simply fall back to a usable experience. Smooth scrolling can fall back to normal anchor jumps. Accordion animations can fall back to instant open and close behavior. A masked comparison slider can still expose a native range input with a simpler presentation. View transitions can be ignored by unsupported browsers while navigation continues to work. Feature queries such as @supports help developers layer enhancements only where browsers understand them. User preference queries such as prefers-color-scheme and prefers-reduced-motion let sites adapt to the person using them rather than forcing one visual experience on everyone. This is not just technical neatness. It is a more respectful way to build for a web that runs across different devices, settings, and constraints. The best progressive enhancement does not make the baseline feel broken. It starts with functional HTML, improves the experience with widely supported CSS, and adds newer capabilities where they make the interface smoother or more expressive. The result feels modern without becoming fragile. Motion needs purpose, not just possibility CSS can now create polished motion without JavaScript timers or heavy libraries. Infinite logo sliders, animated borders, view transitions, motion paths, and scroll-driven effects can all be handled mostly or entirely through CSS. That is exciting, but motion should earn its place. A partner logo strip that moves continuously may add energy, but it should pause on hover or focus so users can inspect it. Page transitions can make navigation feel smoother, but they should not slow users down or become disorienting. A glowing animated border may draw attention to a feature card, but the interface should still feel calm when motion is reduced. This is where CSS gives developers a useful advantage. Animations can be expressed declaratively, paused with state selectors, and disabled through media queries. Rather than manually coordinating timers, listeners, and cleanup, the browser can manage the animation lifecycle. That usually leads to smoother performance and simpler code. The deeper takeaway is that CSS motion works best as communication. It can show continuity, guide attention, reveal relationships, or make a state change feel less abrupt. Motion that exists only because it is possible tends to age badly. Motion that helps users understand what changed is much more likely to belong. The future frontend stack may be smaller than we expected The frontend ecosystem often moves by adding layers. More state management, more rendering strategies, more build tools, more component abstractions. Many of those tools solve real problems, especially in application-heavy environments. But the browser itself has also been moving forward, and sometimes our habits have not caught up. A smaller stack does not mean a less capable interface. It can mean choosing CSS variables for theming before inventing a theme engine. It can mean using prefers-color-scheme before writing theme detection logic. It can mean using <dialog> before importing a modal package. It can mean using scroll-margin-top before writing scroll offset JavaScript. It can mean allowing CSS counters to number content instead of maintaining numbers by hand. The strongest teams will not be the ones that avoid JavaScript. They will be the ones that know when not to spend it. JavaScript should still handle persistence for a saved theme choice, fetch fresh data, coordinate complex interactions, and power application logic. But it should not automatically inherit every small UI responsibility simply because it can. A CSS-first mindset gives developers more options. It helps teams ship less code, lean on browser behavior, and build interfaces that survive better across conditions. Most importantly, it brings the craft of frontend development closer to the web’s original strength: resilient layers that work together rather than one layer trying to do everything. Takeaways Modern CSS is no longer just a styling layer. It can express state, context, user preferences, motion, and responsive interaction patterns that once required JavaScript. Intermediate developers should treat CSS and semantic HTML as active parts of interface architecture, not just the final presentation pass. The best use of JavaScript is intentional. Reach for it when the interface needs data, persistence, complex logic, or behavior the browser cannot provide on its own. For many everyday UI patterns, native HTML and CSS now provide a lighter, more accessible, and more maintainable starting point. Progressive enhancement is the safest way to adopt newer features. Build a solid baseline first, then layer modern selectors, animations, masks, transitions, and preference-aware behavior where support allows. Users get a functional experience everywhere, and a richer one in capable browsers. The real mindset shift is simple: before adding script, pause and ask whether the browser already understands the job. More often than many developers expect, the answer is yes. This Week in the News 🤖 Google open-sources Ax for distributed AI agents:Google has open-sourced Ax, a distributed runtime designed for building and coordinating AI agents across systems. The project focuses on managing agent execution at scale, reflecting growing interest in infrastructure for multi-agent workflows rather than standalone AI interactions.It’s another sign that the AI ecosystem is shifting from individual prompts toward orchestrated agent-based systems. 🧨 Bun’s Rust rewrite finally lands: Bun’s long-discussed Rust rewrite has now been merged, which gives the runtime another big moment in the JavaScript tooling conversation. The interesting part is not only the rewrite itself, but the debate around how it was produced. With AI-ported code under scrutiny, this becomes a useful reminder that speed still needs engineering taste, review discipline, and trust in the codebase. For developers, Bun remains one of the most exciting projects to watch, but this merge also shows how closely the community is now examining the way modern tooling gets built. 🌐 Browsers may soon get a trusted PWA install button: Chrome and Edge are exploring a new <install> HTML element that would let browsers render a trusted install button for PWAs. That might sound like a small interface detail, but it points to a bigger shift in how seriously browsers want to treat installable web apps. PWA adoption has always had a discoverability problem. A native, browser-controlled install element could make the experience feel clearer, safer, and more consistent for users, while giving developers a cleaner path to promote app installation without relying on custom prompts. 🎥 Fireship explains how one PR hijacked the npm registry: The npm ecosystem had another uncomfortable security lesson, and Fireship’s breakdown captures why this matters for everyday developers. A single compromised or malicious pull request can ripple through the dependency graph faster than most teams can react. JavaScript’s strength has always been its massive open ecosystem, but that same scale makes supply chain security everyone’s problem. 🔒 Shai Hulud returns with another package supply chain hit: Shai Hulud struck again this week, affecting hundreds of packages and adding another serious entry to the JavaScript supply chain security story. At the same time, node-ipc was also infected with a credentials stealer, which makes this feel less like an isolated scare and more like a pattern developers need to plan around. The practical question for teams is no longer whether package compromise can happen. It is how quickly they can detect, contain, rotate credentials, and recover when it does. Beyond the Headlines 📺 TanStack Start takes its shot at the framework conversation: Tanner Linsley’s conversation on TanStack Start is worth watching because it is a candid look at what it takes to compete in a space where Next.js already shapes many developers’ expectations. TanStack has earned trust through focused, composable tools, and Start is trying to bring that same philosophy into full-stack app development. The bigger story is how much appetite still exists for frameworks that give developers more control without asking them to give up modern conventions. 🧩 Browser engines quietly patch the web for popular sites: Some browsers ship built-in tweaks for specific popular sites when those sites do not render correctly by default. Firefox and Safari both do this, which says a lot about the messy reality behind the “web standards” ideal. Developers often talk about browser compatibility as though it is only about specs, but the web also runs on pragmatism. When a major site breaks, browser teams sometimes bend a little to preserve the user experience. It is a fascinating glimpse into the invisible maintenance work that keeps the modern web usable. 🧠 The limits of native code deserve a closer look: Artem Loenko’s piece on native code lands at a useful time, especially as more web developers think about performance through the lens of Rust, WebAssembly, and lower-level tooling. Native code can be powerful, but it is not a magic escape hatch. Complexity, portability, safety, developer experience, and maintenance still matter. The best engineering choices usually come from knowing where native code helps, where it complicates things, and where higher-level tools already do the job well enough. 🌱 Open source projects can die in surprisingly ordinary ways:: This piece is a sharp reminder that open source projects do not always fail because of dramatic technical problems. Sometimes they fade because maintainers burn out, governance is unclear, onboarding is painful, or the project quietly stops feeling worth the effort. For developers who depend on open source every day, it is worth reading with empathy. Healthy projects need more than stars, downloads, and GitHub activity. They need people, process, trust, and a path for new contributors to become part of the work. 🧬 How Node.js and V8 keep each other working: Joyee Cheung’s deep dive into Node.js and V8 is a great read for anyone who wants to understand the machinery behind the runtime we use so casually. Node’s relationship with V8 is not a simple dependency story. It is an ongoing coordination effort across APIs, internals, performance work, compatibility, and release timing. This is the kind of behind-the-scenes engineering that rarely gets the spotlight, but it directly shapes what JavaScript developers can build and how reliably their apps run. Practical AI for Working Developers AI is moving fast, and for a lot of developers, keeping up still feels like learning through trial and error. BuildWithAIis Packt’s newsletter for engineers who want to move beyond AI headlines and start using it in real projects. Backed by Packt’s 7,000+ tech books, courses, and expert resources across 1,000+ technologies, each issue brings you practical workflows, carefully chosen resources, and implementation guidance you can actually apply. Subscribehere. The Developer Toolbox ⚡ Critical 8.0: Addy Osmani’s Critical 8.0 helps extract and inline above-the-fold CSS into HTML, which makes it useful for anyone still paying attention to real-world page speed. Performance work often gets buried under bigger framework conversations, but rendering fast still matters. Critical is a reminder that some of the highest-impact optimizations are practical, focused, and close to the browser’s first paint. For teams working on content-heavy sites, landing pages, or performance-sensitive web apps, this is a tool worth revisiting. 🗓️ SVAR Calendar brings a flexible calendar component across frameworks: SVAR Calendar offers a calendar component for React, Svelte, and Vue, with an MIT-licensed core and a commercial version for extended use cases. Calendar UI is one of those deceptively hard pieces of product development. It looks familiar until you need recurring events, custom views, localization, styling, and framework compatibility. SVAR’s cross-framework approach makes it interesting for teams that want a polished calendar foundation without building every interaction from scratch. The live demo is worth a quick look. That’s all for this week. Have any ideas you want to see in the next article? Hit Reply! Cheers! Editor-in-chief, Kinnari Chohan 👋 Advertise with us Interested in sponsoring this newsletter and reaching a highly engaged audience of tech professionals? Simply reply to this email, and our team will get in touch with the next steps. 📢 Important: WebDevPro is Moving to Substack WebDevPro will soon move to Substack. Future issues will come from packtwebdevpro@substack.com so please add it to your contacts or whitelist it to keep receiving the newsletter without interruption. SUBSCRIBE FOR MORE AND SHARE IT WITH A FRIEND! *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}.image_block img+div{display:none}sub,sup{font-size:75%;line-height:0}#converted-body .list_block ol,#converted-body .list_block ul,.body [class~=x_list_block] ol,.body [class~=x_list_block] ul,u+.body .list_block ol,u+.body .list_block ul{padding-left:20px} @media (max-width: 100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width: 100%;display:none;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}.social_block .social-table{display:inline-block!important}} @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }
Read more
  • 0
  • 0

Kinnari Chohan
18 May 2026
18 min read
Save for later

WebDevPro #140: The Hidden Complexity Behind "Simple" Three.js Scenes

Kinnari Chohan
18 May 2026
18 min read
Crafting the Web: Tips, Tools, and Trends for Developers Advertise with Us|Sign Up to the Newsletter @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } WebDevPro #140 The Hidden Complexity Behind "Simple" Three.js Scenes 📢 Important: WebDevPro is Moving to Substack We’ll be moving WebDevPro to Substack soon.Once the transition is complete, all future issues will come from packtwebdevpro@substack.com. To make sure the newsletter continues reaching your inbox, please add this address to your contacts or whitelist it in your mail client. No other action is needed. You’ll keep receiving WebDevPro on the same weekly schedule. Substack will also give you more control over your subscription preferences if you decide to adjust them later. Three.js projects often begin with a deceptively simple moment. A cube appears on screen. A light illuminates the scene. The camera moves smoothly. An animation loop runs continuously. At that stage, the experience feels manageable. The structure seems straightforward: create a scene, add geometry, configure lighting, and render continuously. The early feedback loop is rewarding because visible progress happens quickly. A functioning 3D scene can come together in relatively little code, which creates the impression that the difficult part has already been solved. But complexity in Three.js rarely appears immediately. It emerges gradually through interaction between systems: lighting affects material perception, camera positioning changes spatial readability, shadows introduce rendering overhead, responsiveness alters composition, animation timing affects scene coherence, and asset complexity impacts performance. The difficult part is not rendering the first object. The difficult part is maintaining coherence as the scene evolves. This is why many impressive Three.js demos struggle to become stable, scalable product experiences. The challenge is rarely the first rendered frame. The challenge is managing the growing interaction between rendering, animation, lighting, responsiveness, and scene organization over time. Before we get into it, here’s a sneak peek of this week’s highlights: 🛡️Next.js patches 13 vulnerabilities that teams should not leave to the firewall 🏗️ Anthropic’s Colossus deal puts AI infrastructure back in the spotlight 🤖 AgentField turns multiple coding agents into one TypeScript workflow 🧬 React2Shell shows how curiosity can become an ecosystem-level security story ⚛️ React Server Components in five minutes Small scenes feel deceptively manageable Most Three.js projects begin with a minimal scene setup: a scene, a camera, a renderer, geometry, and a light source. At this stage, the mental model feels clean and predictable. Each component appears isolated and understandable. The renderer draws frames. The camera controls perspective. Geometry defines visible structure. The scene acts as the container that brings everything together. A basic setup often looks like this: const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); const renderer = new THREE.WebGLRenderer(); renderer.setSize( window.innerWidth, window.innerHeight ); document.body.appendChild( renderer.domElement ); At this point, the scene feels conceptually simple because very few systems are interacting simultaneously. The objects are few, the camera is predictable, and the renderer has a clear job: draw what exists in the scene from the camera perspective. Then geometry is added. The code still looks approachable because geometry, material, and mesh map neatly to what appears on screen: const geometry = new THREE.BoxGeometry(); const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 }); const cube = new THREE.Mesh( geometry, material ); scene.add(cube); The result feels immediate and satisfying. Something visible exists on screen. The rendering pipeline works. The scene responds correctly. This early success is useful, but it can also mislead. Developers often assume that scaling a Three.js project simply means adding more objects or more visual detail. In reality, complexity in 3D scenes rarely grows in a straight line. It compounds through interaction between systems. That distinction becomes increasingly important as scenes evolve beyond isolated demos. The first object is usually easy. The ongoing challenge is keeping the scene predictable as more rendering decisions begin depending on one another. Complexity appears through interaction, not object count One of the biggest misconceptions in Three.js development is that complexity scales primarily through object count. Object count matters, but it is not the whole story. In practice, complexity scales through interaction between systems. A single additional light source can change material appearance, shadow calculations, rendering cost, scene mood, and spatial readability. A camera adjustment can change composition, movement perception, interaction feel, and depth relationships. An animation update can change render timing, synchronization behavior, responsiveness, and motion continuity. This interconnected behavior is what makes seemingly small scenes difficult to maintain. A change that looks isolated at code level may affect how the entire scene feels to the user. A simplified dependency chain often looks like this: Geometry -> Material appearance -> Lighting interaction -> Shadow rendering -> Performance impact -> Perceived scene quality The important detail is that changes rarely remain localized. A developer may adjust lighting to improve realism, only to discover that materials now appear overly reflective, shadows become visually noisy, scene contrast becomes inconsistent, or frame rate drops unexpectedly. The original change was small. The resulting system impact is much larger. This is one of the defining characteristics of Three.js applications: systems continuously influence one another. The difficult part is no longer understanding individual features in isolation. The difficult part is understanding how rendering systems behave collectively once the scene becomes more sophisticated. That same coordination challenge is starting to shape how developers use AI tools in day-to-day engineering work. The value is not simply asking an assistant to generate code. It is learning how to structure implementation, inspect output, debug issues, and keep the workflow understandable as the project becomes more complex. That is the focus of Learn Claude Code by Building Live, a live hands-on workshop built around practical AI-assisted development workflows. You’ll see how developers are using Claude Code across coding, debugging, reviewing AI-generated output, managing implementation workflows, and shipping software faster with structured practices. Register now with a 40% early bird discount using code WEB40. Lighting changes everything Lighting is one of the clearest examples of hidden scene complexity. In early Three.js experiments, lighting often feels secondary. Developers focus primarily on geometry because visible objects create immediate visual progress. But lighting determines how the entire scene is perceived. Even simple lighting adjustments dramatically alter realism, atmosphere, depth perception, visual hierarchy, and readability. A scene with the same geometry can feel flat, dramatic, realistic, or confusing depending on how light is positioned and balanced. For example, adding a directional light initially appears straightforward: const light = new THREE.DirectionalLight( 0xffffff, 1 ); light.position.set(5, 10, 7.5); scene.add(light); At first glance, this looks simple. The light has a color, an intensity, and a position. It is added to the scene, and the object becomes easier to read. But lighting configuration quickly expands into broader considerations: shadow softness, contrast balance, reflection behavior, intensity calibration, color temperature, and rendering overhead. The complexity becomes more visible once multiple lights interact together. Adding ambient light may improve visibility while simultaneously flattening scene depth. Increasing directional light intensity may improve realism while creating harsh contrast in unintended areas. Enabling shadows may improve depth but introduce performance cost. This is why lighting rarely behaves like an isolated feature. It becomes part of the overall visual architecture of the scene. As projects mature, developers spend less time asking whether the light works and more time asking whether the lighting supports the experience consistently across the entire scene. Materials introduce hidden rendering complexity Materials often appear straightforward early in development. A developer selects a material type, applies a color or texture, and the object renders correctly. At prototype stage, that feels sufficient. But materials are deeply connected to lighting behavior, reflections, transparency, render ordering, texture consistency, and performance characteristics. Material choices determine not only how an object looks, but how that object responds to the rest of the scene. A simple material configuration might look like this: const material = new THREE.MeshPhysicalMaterial({ color: 0xffffff, metalness: 0.8, roughness: 0.2 }); The complexity is not immediately visible because the material appears correct in isolation. But as scenes evolve, materials begin interacting with multiple light sources, different environment maps, transparency layers, post-processing effects, and varied asset types. This creates a coordination problem rather than a simple rendering problem. One object may appear overly reflective under certain lighting conditions. Another may absorb too much light. Texture resolution differences may become visually distracting once assets appear together inside the same scene. These are not isolated rendering problems. They are scene consistency problems. This is why material systems often become harder to manage over time than geometry itself. Geometry defines structure. Materials define how that structure is visually interpreted, and that interpretation changes continuously depending on the rest of the rendering environment. Animation coordination becomes difficult quickly Animation is another area where hidden complexity emerges gradually. A single rotating object feels simple because the animation loop is easy to understand. The frame updates, the object rotates, and the renderer draws the updated scene: function animate() { requestAnimationFrame(animate); cube.rotation.x += 0.01; cube.rotation.y += 0.01; renderer.render(scene, camera); } animate(); At prototype stage, this loop appears manageable because very little else is competing for synchronization. There is one object, one simple state change, and one render call. But animation systems rarely remain isolated. As scenes evolve, animation begins interacting with camera movement, user interaction, lighting updates, physics systems, UI overlays, and asynchronous asset loading. This creates synchronization challenges that are far more difficult than the initial animation setup itself. A simplified render relationship often looks like this: User interaction -> Animation updates -> Scene state changes -> Renderer updates frame -> Camera reflects new state The difficult part is no longer making objects move. The difficult part is ensuring movement remains coherent across the entire scene. Animations that feel smooth individually may conflict once multiple systems update simultaneously. Timing inconsistencies become more noticeable. Interaction latency becomes more visible. Camera movement can make object animation feel too fast or too slow. UI overlays can make scene motion feel visually noisy. This is one of the reasons polished 3D applications feel dramatically different from simple demos. The challenge is not merely creating animation. The challenge is coordinating animation behavior across the entire rendering system. Scene organization matters more over time Small Three.js scenes tolerate loose organization. Objects can exist directly inside the scene hierarchy without creating immediate problems. Early prototypes often remain understandable even with minimal structure. As projects grow, however, organization becomes increasingly important. Grouping related objects improves transformation management, animation coordination, visibility control, scene readability, and maintainability. A simple grouping structure might look like this: const group = new THREE.Group(); group.add(mesh1); group.add(mesh2); scene.add(group); Initially, this seems like a small architectural choice. Over time, decisions like this become foundational. Without deliberate structure, animation logic becomes fragmented, object relationships become unclear, scene traversal grows more complicated, and interaction systems become difficult to manage. The complexity of Three.js scenes often comes less from rendering itself and more from maintaining clarity as systems expand. This is where product-level thinking begins mattering more than individual rendering techniques. Performance problems are usually architectural Performance discussions around Three.js often focus heavily on rendering power. It is easy to assume that performance issues are mainly about the GPU, object count, or whether the browser can draw the scene quickly enough. But many performance problems emerge from scene architecture rather than raw rendering capability. Examples include unnecessary object updates, excessive shadow calculations, uncontrolled geometry complexity, redundant render work, and poorly coordinated animation systems. These issues accumulate gradually. A scene may perform well initially and degrade slowly as additional systems are introduced. Because the degradation is incremental, the underlying architectural cause is often harder to identify. This is why optimization in Three.js is rarely about one dramatic fix. It usually involves improving coordination between systems: reducing unnecessary updates, simplifying lighting interactions, organizing scene hierarchy more effectively, and minimizing expensive rendering calculations. The difficult part is not merely making a scene render. The difficult part is maintaining predictable performance as scene complexity grows. Real complexity emerges through accumulation One of the most important characteristics of Three.js development is that complexity compounds incrementally. Rarely does a project become difficult because of one major rendering problem. Instead, one additional light is added, another animation loop appears, another material system is introduced, another interaction layer becomes necessary, and another responsive adjustment is required. Each decision appears manageable independently. Together, they create an interconnected rendering system that requires significantly more coordination than the original prototype suggested. This is why simple demos often feel deceptively complete. The early version demonstrates rendering capability. The mature version requires architectural discipline. The difficult part is maintaining coherence As Three.js scenes evolve, developers increasingly spend time maintaining coherence across rendering behavior, material consistency, animation timing, interaction responsiveness, scene organization, and performance stability. This is where the nature of development changes. The challenge is no longer: Can we render this object? The challenge becomes: Can we keep the entire scene predictable, performant, and visually coherent as the system grows? That is a fundamentally different kind of problem. It requires structure, restraint, coordination, iterative refinement, and long-term thinking. These are architectural concerns rather than rendering concerns. Key takeaways Simple Three.js scenes become complex through interaction between systems, not just object count. Lighting, materials, camera behavior, animation, and performance continuously influence one another. Demos can appear complete before the underlying scene structure is ready for real application growth. Performance issues often emerge from scene organization and repeated updates rather than rendering alone. The long-term challenge is maintaining visual and structural coherence as the scene evolves. Conclusion Three.js scenes often appear simple during the early stages of development. A few objects, lights, and animations can quickly create impressive visual output. But the hidden complexity of 3D applications emerges through interaction between systems. Lighting affects materials. Animations affect rendering behavior. Responsiveness changes composition. Scene hierarchy affects maintainability. Performance depends on coordination across all of them. The difficult part is not rendering the first object. The difficult part is maintaining coherence as rendering, interaction, lighting, animation, and performance begin influencing one another simultaneously. That is where simple scenes stop behaving like demos and start behaving like real applications. This Week in the News 🥖 Bun’s million-line leap from Zig to Rust: Bun just made one of the boldest runtime moves we’ve seen in a while: a 1 million line PR that ports its codebase from Zig to Rust, only days after the branch was described as “experimental.” The reason is practical enough. Bun’s team has been chasing memory leaks and unpredictable crashes, and Rust gives them stronger language-level guardrails for exactly those problems. Still, swapping the foundation of a project this fast is bound to make developers nervous. 🧨 TanStack’s npm compromise shows why fresh installs need a safety buffer:A malicious npm publish hit 42 @tanstack/* packages this week, with 84 compromised versions live for a short window before being caught. The incident was contained quickly, but it still exposed a real risk for teams running fresh installs in local or CI environments. The practical move is to add friction around brand-new package releases, using safeguards like npm’s min-release-age, pnpm’s minimumReleaseAge, and GitHub Actions checks with tools such as zizmor. 🛡️Next.js patches 13 vulnerabilities that teams should not leave to the firewall:Next.js shipped a coordinated security release covering 13 vulnerabilities across denial of service, middleware and proxy bypass, SSRF, cache poisoning, and XSS. Several issues cannot be fully mitigated by cloud firewalls, so the recommendation is straightforward: update to 15.5.18 or 16.2.6. There is also a related React Server Functions denial-of-service issue fixed in React RSC packages 19.2.6. 🏗️ Anthropic’s Colossus deal puts AI infrastructure back in the spotlight:Anthropic’s latest compute expansion shows how much frontier AI now depends on access to massive infrastructure. SpaceX and xAI are making Colossus 1 available to Anthropic, giving Claude more capacity as demand grows. The bigger signal is that model competition is no longer only about architecture or benchmarks. It is also about who can secure enough compute, power, and data center capacity to keep products reliable at scale. 🤖 AgentField turns multiple coding agents into one TypeScript workflow:AgentField is positioning itself as a TypeScript harness for composing Claude Code, Codex, Gemini, and other coding agents into repeatable workflows. The interesting shift is from prompting one assistant at a time to coordinating agents with recipes, constraints, and reusable patterns. For teams experimenting with agentic development, this points toward a more structured way to test what coding agents can actually do together. Beyond the Headlines ⚖️ Tokenmaxxing asks whether AI is increasing progress or just output:This piece pushes back on the idea that more AI-generated work automatically means better engineering. Faster coding can help teams explore more ideas, but it can also create more noise, more unfinished features, and more coordination overhead. The useful question is not “how much can we generate?” but “what is worth keeping?” 🧬 React2Shell shows how curiosity can become an ecosystem-level security story:This write-up traces the story behind React2Shell, an unauthenticated RCE in React Server Components. The value is not only in the vulnerability itself, but in how the discovery unfolded: curiosity, protocol digging, and a fast-moving disclosure process. It is a useful read for anyone working near modern React infrastructure. 🔍 AI is putting pressure on old vulnerability disclosure norms:Jeff Kaufman looks at how AI changes the balance between quiet security fixes and coordinated disclosure. When AI can scan commits and infer security impact faster, “silent” fixes become easier to spot, and long embargoes become harder to manage. The result is a security culture clash that maintainers may not be able to ignore. 🧩 Library patterns keep shaping what the web platform becomes:This piece looks at how library patterns have influenced the web platform over time. The takeaway is simple: when developers repeatedly solve the same problem in userland, it often points to a missing browser primitive. Today’s popular library workaround can become tomorrow’s platform feature. ⚛️ React Server Components in five minutes:This short deep dive is a useful companion to the React2Shell discussion because it focuses on the underlying React Server Components model. For developers who want a quick refresher, it helps connect the security conversation back to the architecture: what runs on the server, what reaches the client, and why those boundaries matter so much in modern React apps. Practical AI for Working Developers AI is moving fast, and for a lot of developers, keeping up still feels like learning through trial and error. BuildWithAIis Packt’s newsletter for engineers who want to move beyond AI headlines and start using it in real projects. Backed by Packt’s 7,000+ tech books, courses, and expert resources across 1,000+ technologies, each issue brings you practical workflows, carefully chosen resources, and implementation guidance you can actually apply. Subscribehere. Tool of the Week 🐀 Ratty makes the terminal weird, visual, and fun again Ratty is a terminal emulator with inline 3D graphics and a spinning rat cursor. It is playful, strange, and clearly built with a sense of humor, but that is what makes it worth a look. Developer tools do not always need to be purely utilitarian. Sometimes the experiment itself is the point. That’s all for this week. Have any ideas you want to see in the next article? Hit Reply! Cheers! Editor-in-chief, Kinnari Chohan 👋 Advertise with us Interested in sponsoring this newsletter and reaching a highly engaged audience of tech professionals? Simply reply to this email, and our team will get in touch with the next steps. SUBSCRIBE FOR MORE AND SHARE IT WITH A FRIEND! *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}.image_block img+div{display:none}sub,sup{font-size:75%;line-height:0}#converted-body .list_block ol,#converted-body .list_block ul,.body [class~=x_list_block] ol,.body [class~=x_list_block] ul,u+.body .list_block ol,u+.body .list_block ul{padding-left:20px} @media (max-width: 100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width: 100%;display:none;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}.social_block .social-table{display:inline-block!important}} @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }
Read more
  • 0
  • 0

Kinnari Chohan
11 May 2026
16 min read
Save for later

WebDevPro #139: The Developer’s Edge in an AI-Assisted Workflow

Kinnari Chohan
11 May 2026
16 min read
Crafting the Web: Tips, Tools, and Trends for Developers Advertise with Us|Sign Up to the Newsletter @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } WebDevPro #139 The Developer’s Edge in an AI-Assisted Workflow 📢 Important: WebDevPro is Moving to Substack We’ll be moving WebDevPro to Substack soon.Once the transition is complete, all future issues will come from packtwebdevpro@substack.com. To make sure the newsletter continues reaching your inbox, please add this address to your contacts or whitelist it in your mail client. No other action is needed. You’ll keep receiving WebDevPro on the same weekly schedule. Substack will also give you more control over your subscription preferences if you decide to adjust them later. Welcome to this week’s issue of WebDevPro! Today’s piece features insights from Mark Price,a Microsoft Certified Solutions Developer and former Microsoft Certified Trainer with more than 30 years of experience. Mark is also a bestselling author of programming books across .NET, C#, Python, and modern web development, Mark brings a practical, developer-first lens to building strong technical foundations. Today’s piece is drawn from his book Web Development with an AI Sidekick. AI has changed the texture of web development work. A few years ago, most of us bounced between docs, Stack Overflow, GitHub issues, and half-finished notes in our own repos. Now, many developers open a chat window first. We ask for an explanation, a starter function, a refactor, a regex fix, or a quick way to debug a failing script. That shift is real, and it is already shaping how people learn and build. Still, the real advantage does not come from generating code faster. It comes from using AI without giving away the thinking that makes you a stronger developer. That matters most at the intermediate stage. Once you are past the basics, progress stops being about syntax recall alone. The actual challenge is understanding behavior, data flow, failure modes, and the trade-offs that sit behind everyday implementation choices. AI can support that process beautifully. It can also make it easier to skip it. The difference comes down to one thing: your mental model. Before we get into it, here’s a sneak peek of this week’s highlights: 🚀 Node.js 26 is here with Temporal by default, V8 14.6, and new JavaScript capabilities. 🌀 Remix 3 beta feels like a full reset, moving toward a web standards-first framework without React at the center. 🧭 VS Code 1.119 sharpens agent workflows with smoother browser access, telemetry, and sandbox improvements. 🌐 Mozilla makes the case for trustworthy JavaScript and stronger guarantees around delivered browser code. ⏱️ Time to Yield revisits frontend responsiveness and why performance is also about keeping the interface alive. ✨ Anime.js keeps web animation approachable with timelines, SVG animation, scroll effects, and draggable interactions. JavaScript still lives or dies on behavior In frontend work, JavaScript remains the place where user behavior becomes actual application behavior. A click updates a count. A form input changes local state. A toggle shows hidden content. A validation rule decides whether something can be submitted. These are ordinary interactions, but they expose the central challenge of frontend development: the browser is always reacting to changing state. That is why JavaScript becomes harder long before it becomes “advanced.” The difficulty usually is not the syntax. It is the chain of cause and effect. One event triggers a function. That function updates a variable. The UI reads that value and renders it. Another function depends on that same value later. At that point, you are no longer writing isolated lines of code. You are managing behavior. const button = document.querySelector('#save'); const status = document.querySelector('#status'); button.addEventListener('click', async () => { status.textContent = 'Saving...'; try { await saveDraft(); status.textContent = 'Saved successfully'; } catch (error) { status.textContent = 'Something went wrong'; console.error(error); } }); This is a small example, but it shows the real shape of frontend work. The question is not just “does it run?” The question is “what should the user see while something is happening, and what should happen when it fails?” That is also where AI can be genuinely useful. I find it most helpful when it is asked to explain behavior, not just produce output. If you ask it, “Why might this click handler fail silently?” or “What edge cases should I think about here?” you get much better value than you do from “write this feature for me.” The browser still rewards developers who can reason clearly about state, events, and side effects. AI helps, but it does not remove that requirement. Debugging is still a first-class skill One quiet risk with AI-generated code is that it can make clean-looking code feel trustworthy before it has earned that trust. A function can look polished and still rely on DOM elements that do not exist. A promise can be written cleanly and still swallow an error. A generated solution can solve the visible symptom while leaving the original logic problem untouched. That is why debugging remains one of the most important skills in modern web work. The console is still your friend. So are breakpoints, network inspection, and reading stack traces without panic. AI can help interpret an error, but the developer still needs to inspect the real runtime conditions. const form = document.querySelector('#signup-form'); console.log('Form found?', !!form); form?.addEventListener('submit', (event) => { event.preventDefault(); console.log('Form submitted'); }); This snippet is simple, but it reflects a useful habit: check your assumptions in the environment where the code actually runs. That habit matters more in AI-assisted workflows. If you paste an error into a chatbot before you inspect the actual state of the page, you risk outsourcing the wrong question. The better sequence is this: reproduce the issue, inspect the DOM or runtime values, form a hypothesis, then ask AI to help test or refine that hypothesis. That turns AI into a debugging partner rather than a guess machine. TypeScript makes fuzzy assumptions visible JavaScript gives you freedom. TypeScript pushes back when that freedom becomes vague. That is why TypeScript matters even for developers who already “know JavaScript.” Its biggest benefit is not prestige or complexity. It is visibility. It reveals where your assumptions are too loose. If a user may not have a profile image, the code should say so. If a function returns either success data or an error object, the code should say so. If a property can only be one of three allowed strings, the code should say so. That sounds obvious, but teams often carry these assumptions informally. AI-generated code tends to make that worse because it often fills in the blanks with something plausible rather than something verified. type SurveyStatus = 'draft' | 'published' | 'closed'; interface Survey { id: number; title: string; status: SurveyStatus; responseCount?: number; } function getStatusLabel(survey: Survey): string { if (survey.status === 'draft') return 'Work in progress'; if (survey.status === 'published') return 'Live now'; return 'No longer accepting responses'; } This is not flashy code, but it is valuable code. It makes the shape of the data easier to understand at a glance. It also gives your editor a chance to protect you from sloppy assumptions. This is where AI becomes much more effective, too. When you prompt with clear data shapes, expected states, and explicit constraints, the quality of the generated response improves immediately. Good prompts in development often look a lot like good system design. They define what is allowed, what is optional, and what should happen when something is missing. Developers sometimes treat TypeScript as overhead because the application seems small. The problem is that applications rarely stay small in the ways that matter. They grow in decisions, edge cases, and invisible dependencies. TypeScript helps bring those decisions into the open. Backend work rewards structure earlier than people expect On the backend, the same pattern appears in a different form. It is easy to write a Python script that works once. It is much harder to build backend logic that stays understandable after a few rounds of changes, debugging, and feature growth. That is why the backend conversation should not just be about language choice or framework speed. It should be about structure. Small functions, sensible modules, meaningful variable names, clear error handling, and isolated responsibilities all matter much earlier than most people expect. def calculate_completion_rate(total_responses, completed_responses): if total_responses == 0: return 0 return round((completed_responses / total_responses) * 100, 2) A tiny function like this is not interesting because it is clever. It is useful because it is readable, testable, and easy to reuse. That is often the better way to judge backend code quality. Not “How compact is it?” but “Can I understand it six weeks later?” AI can help a lot here. It can suggest cleaner names, extract a helper function, explain a traceback, or show a better way to handle a repeated block of logic. Still, the best outcomes come when you already know what kind of structure you want. If you ask AI to “write backend code for analytics,” you may get something that works. If you ask it to “separate data access, business logic, and formatting concerns,” you are much more likely to get something maintainable. That difference matters. Good AI usage looks more like collaboration than delegation There is a lot of talk about prompt engineering, but for most working developers, the real skill is much simpler: knowing how to ask better technical questions. Strong AI-assisted development usually includes some version of the following habits: asking for an explanation before asking for a solution requesting alternatives and trade-offs, not just one answer giving the model the data shape, edge cases, and constraints up front reviewing generated code line by line before accepting it testing in the real environment, not just trusting a neat-looking response Those habits keep you in the driver’s seat. Bad prompt: "Write a form validation function." Better prompt: "Write a TypeScript function that validates a survey form. Fields: title (required, min 5 chars), email (optional, valid format if present), questions (must contain at least 1 item). Return an object with field-specific error messages." The real pattern is bigger than any one language What I find most interesting is that the same deeper ideas keep showing up across the stack. In JavaScript, you are reasoning about user interaction and state changes. In TypeScript, you are making assumptions explicit so those changes stay manageable. In Python, you are organizing logic so that behavior can scale without becoming confusing. Different syntax, same underlying discipline. That is why AI works best for developers who are trying to strengthen their mental model, not bypass it. A developer with a clear mental model asks better questions: What state is changing here? What data shape does this function expect? What happens when the input is missing or invalid? Where should this responsibility live? How should the system fail? Is this solution easier to maintain than the last one? Those questions improve your code even before AI answers them. Then AI becomes useful in the right way. It helps you refine, test, explore, and debug. It stops being a code vending machine and starts acting more like a second pair of eyes. Takeaways AI is now part of the everyday web development toolkit, and that is not changing. The developers who benefit most will not be the ones who hand over every task blindly. They will be the ones who use AI to sharpen their understanding of the stack. JavaScript still demands a clear grasp of browser behavior. TypeScript still rewards developers who make assumptions explicit. Python still works best when logic is structured early and cleanly. Across all three, the strongest habit is the same: treat generated output as something to interrogate, not something to trust by default. That is the mindset worth building. AI can speed up the path to a solution. Your mental model determines whether the solution actually holds up. Continue learning with a hands-on guide If this way of thinking about AI-assisted development resonates with you, Web Dev with an AI Sidekick builds on the same idea in a more structured, practical way. You can pre-order the book on Amazon here: Web Dev with an AI Sidekick Join Anna J McDougall, Field CTO at HashiCorp / IBM, for a live workshop on how engineering careers are evolving in the AI era. Learn which skills are becoming more valuable, how AI-native teams are operating, and how developers can future-proof their careers. 📅 May 30 | Live Online Workshop Register Now This Week in the News 🚀 Node.js 26 lands as the new Current release: Node.js 26 is here, with the Temporal API enabled by default, V8 14.6, and Undici 8. Developers also get new JavaScript capabilities such as Map.prototype.getOrInsert() and Iterator.concat(). This is the cutting-edge Current release until October, when Node.js 26 is expected to move to LTS. It is a good time to test compatibility, explore the runtime changes, and see what might affect production workflows later this year. 🌀 Remix 3 beta preview takes a sharper turn: Remix has had a long journey: it was created by the team behind React Router, positioned as an alternative to Next.js, acquired by Shopify, and later folded into React Router v7. Now Remix 3 is stepping out in a new direction: a full-stack, web standards-first framework with its own UI component model and no React at the center. It feels less like a version bump and more like a reset. 🧪A Bun experiment turns into a bigger debate: Jarred Sumner added a Zig to Rust porting guide to the Bun repo, and Hacker News quickly turned it into a much larger conversation about Bun’s future. The speculation got intense enough that Jarred stepped in and said the whole thread was an overreaction. It is a small reminder of how closely developers watch fast-moving tools. Even an experiment can look like a strategy shift when the project has this much attention. 🧭 VS Code 1.119 sharpens agent workflows: VS Code 1.119 focuses on smoother agent interactions, better observability, and fewer workflow interruptions. Agents can now request access to shared browser tabs, Copilot Chat sessions can emit OpenTelemetry traces, and sandboxed agents get more practical network and temporary-file permissions. It is another step toward making AI coding agents feel less separate from the editor. Beyond the Headlines 🔐 Security through obscurity deserves a better conversation: This piece challenges the usual dismissal of “security through obscurity.” The argument is not that obscurity should replace strong security, but that it can still be useful as one extra layer. It is a thoughtful read for anyone who has seen good security advice turn into rigid slogans. 🥯 A developer worries about Bun’s future: This post looks at Bun with both admiration and concern. The runtime is fast, ambitious, and widely loved, but its future now carries bigger questions around ownership, priorities, and long-term stewardship. The piece is a developer asking what happens when a tool people rely on becomes part of a much larger company story. 🌐 Mozilla makes the case for trustworthy JavaScript: Mozilla explores how web apps can make their delivered JavaScript more trustworthy. The core concern is simple: users need a way to know that the code running in the browser is the code developers intended to ship. This matters most for high-trust apps, especially where privacy and encryption are involved. The open web needs stronger ways to make invisible tampering harder to hide. 🎥 A useful watch for your developer queue: I would keep this as a short watchlist item unless you want to share the title or main theme. Without that, the safest version is simple and curiosity-led. This video is worth saving for a slower watch, especially if you like technical talks that connect engineering decisions with the way developers actually build, debug, and maintain software. ⏱️Time to Yield revisits frontend responsiveness: This piece looks at yielding and responsiveness from a practical frontend angle. It is less about chasing benchmark wins and more about how JavaScript work affects the user’s experience. That framing is useful because performance is not only about speed. It is also about when work happens, how much room the browser gets, and whether the interface still feels alive under pressure. Practical AI for Working Developers AI is moving fast, and for a lot of developers, keeping up still feels like learning through trial and error. BuildWithAI is Packt’s newsletter for engineers who want to move beyond AI headlines and start using it in real projects. Backed by Packt’s 7,000+ tech books, courses, and expert resources across 1,000+ technologies, each issue brings you practical workflows, carefully chosen resources, and implementation guidance you can actually apply. Subscribe here. Tool of the Week Anime.js keeps web animation approachable ✨ Anime.js gives developers a polished way to build expressive motion on the web, from timelines and SVG animation to scroll-triggered effects and draggable interactions. It is useful when animation needs to feel intentional rather than decorative. The API stays approachable, but there is enough depth for teams building more crafted, interactive interfaces. Crashcat makes physics demos surprisingly fun 🐱 Crashcat is a JavaScript 3D rigid body physics library for games, simulations, and interactive web experiences. It also has the kind of homepage you remember: a cat in a convertible driving through endless obstacles. That playful first impression works. The examples make the library feel inviting before the technical details take over. That’s all for this week. Have any ideas you want to see in the next article? Hit Reply! Cheers! Editor-in-chief, Kinnari Chohan 👋 Advertise with us Interested in sponsoring this newsletter and reaching a highly engaged audience of tech professionals? Simply reply to this email, and our team will get in touch with the next steps. SUBSCRIBE FOR MORE AND SHARE IT WITH A FRIEND! *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}.image_block img+div{display:none}sub,sup{font-size:75%;line-height:0}#converted-body .list_block ol,#converted-body .list_block ul,.body [class~=x_list_block] ol,.body [class~=x_list_block] ul,u+.body .list_block ol,u+.body .list_block ul{padding-left:20px} @media (max-width: 100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width: 100%;display:none;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}.social_block .social-table{display:inline-block!important}.row .side{display:none}} @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }
Read more
  • 0
  • 0

Kinnari Chohan
04 May 2026
17 min read
Save for later

WebDevPro #138: The agentic coding loop: PRD → task planner → parallel agents

Kinnari Chohan
04 May 2026
17 min read
Crafting the Web: Tips, Tools, and Trends for Developers Advertise with Us|Sign Up to the Newsletter @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } WebDevPro #138 The agentic coding loop: PRD → task planner → parallel agents By Alexandre Zajac Catch the latest HubSpot Developer Platform updates in Spring Spotlight Spring Spotlight 2026 is live and we’ve rounded up the top updates for developers. See what's new for the HubSpot Developer Platform! Ship faster with AI coding tools like Cursor, Claude Code, and Codex. Build MCP-powered AI connectors, run serverless functions with support for UI extensions, and use date-based versioning to streamline roadmap planning. Explore Now 📢 Important: WebDevPro is Moving to Substack We’ll be moving WebDevPro to Substack soon.Once the transition is complete, all future issues will come from packtwebdevpro@substack.com. To make sure the newsletter continues reaching your inbox, please add this address to your contacts or whitelist it in your mail client. No other action is needed. You’ll keep receiving WebDevPro on the same weekly schedule. Substack will also give you more control over your subscription preferences if you decide to adjust them later. Welcome to this week’s issue of WebDevPro! Today’s piece comes from Alexandre Zajac, an engineer from Prime Video who’s been thinking deeply about how AI coding workflows actually behave in the real world. Alex doesn’t just experiment at the surface level; he digs into the friction points, the failure modes, and the patterns that emerge when you push agents beyond toy problems. He built this PDF starter kit to help you go through it! In this article, he walks through a practical system for making agentic coding reliable, drawing from firsthand experience and real constraints. It’s thoughtful, grounded, and immediately useful if you’ve ever felt your AI sessions spiral after a few dozen turns. Before we get into it, here’s a sneak peek of this week’s highlights: 🧠 A new contender just entered the coding model race ⚙️ Cursor just turned its agents into an API ⚛️ React is still trying to automate performance 🟢 Node 26 hit a delay, but Temporal is still close 🧠 Where the goblins in ChatGPT actually came from 🧩 Coding is shifting from prompts to orchestration 👋 Hi! It's Alex! Let's imagine this scene. You open a new chat. You describe the feature. For twenty minutes, the AI is cooking. Good code, too. Around turn 30, it starts fixing a bug it introduced three turns ago. It imports a module you deleted. It references a file path you renamed ten messages back. You're not building anymore. You're managing. Feeding corrections back in, watching the agent regenerate the same broken pattern with different variable names. This is the death loop, and it's not a prompt quality problem. Session length is the variable that matters. I created a simple system to ship simple changes with agents (almost) autonomously, and in this article, I want to show the why and how I built it. Context erosion The Anthropic Claude Code docs say it directly: a single debugging session can generate tens of thousands of tokens, and when the context window fills up, Claude starts forgetting earlier instructions and making more mistakes. A 10-turn session is fine. A 50-turn session drifts. At 80 turns, you're spending more time correcting than building, and at that point, you might as well have written the code yourself. Birgitta Böckeler, writing in the Martin Fowler engineering series, said it after months of daily agentic coding: the longer a session gets, the more hit-and-miss it becomes, regardless of the rigor in prompting. Assumption stacking Agents make assumptions about your codebase. Most are correct. When one is wrong and goes unchallenged, the agent builds on it. Then you build on the agent's code. Three levels in, the wrong assumption is load-bearing. Documented example from the same Martin Fowler series: an agent diagnosed a Docker build failure as an architecture mismatch and changed the Docker settings. The actual cause was node_modules built for the wrong platform. Classic error. Any developer who's been burned by it once would catch it in 30 seconds. Without that catch, the agent would have spent hours deepening a wrong diagnosis into increasingly creative fixes. Agents are confidently wrong with no built-in correction mechanism. Filesystem collisions Two agents, one working directory: agent-1: git add . && git commit -m "fix auth middleware" agent-2: git add . && git commit -m "refactor API layer" # agent-2 just committed agent-1's half-finished auth changes Every agent sees every other agent's uncommitted changes. Files get overwritten mid-edit. One agent's npm install blows away another's node_modules. Commits become incoherent. Open two Cursor tabs on the same repo and give it 20 minutes. Productive at first. Then a mess that takes longer to untangle than doing the work sequentially. So how do we make this consistently okay? Stage 1: The PRD A PRD is not a 47-page corporate artifact. It's a short document you write before opening your coding tool. Its job: prevent assumption stacking before the agent gets a chance to start. I always use these for medium-to-large features. It’s similar to the Harper Reed workflow published in February 2025. It got traction because it was concretely different from how most people worked at the time. His first move is a conversational spec session where an LLM asks him one question at a time until the idea is fully elaborated. The prompt: "Ask me one question at a time so we can develop a thorough, step-by-step spec for this idea. Each question should build on my previous answers, and our end goal is to have a detailed specification I can hand off to a developer. Let's do this iteratively and dig into every relevant detail. Remember, only one question at a time." Here's the idea: [IDEA] When the questions run dry: "Now that we've wrapped up the brainstorming process, can you compile our findings into a comprehensive, developer-ready specification? Include all relevant requirements, architecture choices, data handling details, error handling strategies, and a testing plan so a developer can immediately begin implementation." Output goes to spec.md. The whole thing takes 15 minutes. The Doozy founders shipped a 300k-line Next.js monorepo with 3 to 6 parallel agents at any given time. They do it differently. They built a /discussion command that runs before any code is written. Subagents explore the codebase and look up dependencies. The command produces no edits, only a written summary in .context/context.md. Other agents reference that file. What goes in the spec # spec.md ## Problem What you're building and why, in one sentence. ## Constraints What the agent should NOT touch. What patterns to follow. ## Files to read src/auth/middleware.ts src/routes/users.ts prisma/schema.prisma ## Definition of done npm run typecheck && npm test passes with no new failures. 150 to 300 words. The format matters less than writing it before you start prompting. One test: if you can't describe what the feature should not do, keep writing. Where it lives: spec.md in the repo root, a context section in CLAUDE.md, or .context/context.md. Pick whatever your tool loads by default. Watch out for CLAUDE.md files that balloon. The Claude Code docs are explicit about this: bloated CLAUDE.md files cause Claude to ignore your actual instructions. Treat it like code. Prune it. If you added a rule and Claude's behavior didn't change, the rule is noise. Delete it. Stage 2: The task planner The task planner converts a spec into a sequenced list of bounded tasks. Harper Reed uses a reasoning model for this step, and I do too. Not for code, just for decomposition. A possible prompt: Draft a detailed, step-by-step blueprint for building this project. Then break it down into small, iterative chunks that build on each other. Go another round to break it into small steps. Review and make sure the steps are small enough to implement safely, but big enough to move the project forward. Make sure each prompt builds on the previous prompts, and ends with wiring things together. There should be no hanging or orphaned code that isn't integrated into a previous step. [SPEC] Output goes to prompt_plan.md. Then he asks for a todo.md checklist the execution agent can check off. That's how state persists across sessions without re-reading the full conversation history. Anthropic describes this as the orchestrator-workers pattern: a central LLM breaks down tasks, delegates them to worker LLMs, and synthesizes results. The reasoning is that you can't always predict the subtasks, especially in code, where the number of files and the nature of changes depend on the task. What "agent-sized" means A task is agent-sized if it fits in one context window, produces a green lint/typecheck, and has a binary done/not-done signal. These will send your agent spiraling: ❌ "Refactor the authentication system" ❌ "Add tests" ❌ "Improve performance" These work: ✅ "Add a useAuth hook to src/hooks/auth.ts that wraps the existing authService and exposes login, logout, and isAuthenticated. Update src/components/LoginButton.tsx to use it. Done when: npm run typecheck passes." ✅ "Add a POST /api/users/:id/preferences endpoint to src/routes/users.ts that validates the body against UserPreferencesSchema and writes to user_preferences. Done when: the new route test passes." Specific file. Specific interface. Specific check. The Pane team scores every plan on a 1-10 confidence scale for one-pass implementation success. Below 8, iterate. Their rules: no aspirations, only instructions. No open questions (if something is unresolved, stop and research first). And if a plan is too big for one session, split it. Oversized plans are where the death loop starts. Stage 3: Parallel agents Here are the 3 concepts I use the most to make parallel agents work. Once you have a dependency-ordered task list, tasks that don't share files can run at the same time. The thing that used to block this was filesystem collisions. The fix has been sitting in git since 2015. Git worktrees One command. Sub-second. The new directory is a fully independent working tree on its own branch, sharing the same .git object store. No re-cloning. Disk cost is just for your working files. git worktree add .worktrees/feature-auth -b session/feature-auth Each agent gets a clean branch, zero visibility into other agents' uncommitted changes, and full ability to run tests independently. When done: git worktree remove --force .worktrees/feature-auth The branch stays if there are commits to review. Otherwise, it's gone. This primitive from 2015 that nobody cared about until this year now underpins several independent tools: amux runs up to 30 Claude Code agents in parallel. Uses SQLite compare-and-swap for atomic task claiming. No Redis. No Kubernetes. Just a WHERE clause and SQLite's write lock. Emdash (YC W26) is an open-source desktop app supporting 20+ CLI providers, including Claude Code, Codex, and Gemini CLI, with direct integration into Linear, GitHub, and Jira, plus SSH to remote machines. Pane is open-source, from the Doozy founders. Same team behind the 300k-line monorepo. Cross-platform, agent-agnostic, keyboard-first. All of these solutions are great, but in reality, to get started, none of them are required. A task file and a terminal per worktree gets it done. Scoping agents Every agent session starts with the task description from the plan, the specific files listed in the task, and the relevant section of the spec. # Task: Add useAuth hook Files to read: - src/services/authService.ts (existing service to wrap) - src/hooks/ (existing hook patterns to follow) - src/components/LoginButton.tsx (file to update) Constraints: Do not modify authService.ts. Done: npm run typecheck passes, LoginButton uses useAuth. The agent can't drift into adjacent work because adjacent work isn't in its context. It either completes the task or fails clearly. Ten 20-turn sessions instead of one 200-turn session. Integration checks TypeScript strict mode, npm run typecheck, and lint are your coordination layer across parallel agents. An agent's work isn't done until the checks pass. This prevents a type error in one worktree from silently propagating when branches merge. This scales across languages. Try to enforce this per task: implement, typecheck, lint, format, and fix all issues before proceeding. After all tasks finish, a reviewer subagent reruns everything and verifies that the plan was completed. Human review stays in the loop for architectural decisions, ambiguous acceptance criteria, and code that's syntactically valid but semantically wrong. Automated checks catch everything describable as a rule. They don't catch everything. What this still doesn't fix Agents still hallucinate library APIs. They'll write syntactically correct code against an API that changed in the last major version. The planning stage reduces this by including research tasks with URLs, but it doesn't eliminate it. Verify unfamiliar library usage against docs before shipping. The spec quality ceiling is your knowledge. If you don't understand your codebase well enough to write a clear spec, the agent won't either. The planning phase surfaces this. If you can't finish the plan without open questions, you need to learn more before you start. Merge conflicts still happen when two tasks share a file the plan didn't account for. Worktrees stop filesystem collisions. They don't do design work. The tooling layer is moving fast. Git worktrees are stable. The orchestration tools built on top of them aren't. Build your workflow around git worktree add, not the wrapper tool. Context pollution goes both ways. Too little context and the agent hallucinates your conventions. Too much, and the real rules get buried. The same principle applies to task files: dense, explicit, and short. Entropy compounds. Incomplete refactors, mixed conventions, dead code. All of it degrades agent performance on the next feature because the agent has to reason about a messier codebase. This workflow doesn't fix that. Where to start this week Don't adopt all three stages at once. Start with Stage 2. Before your next AI coding session, spend 10 minutes writing a task plan. Three things: Which files does this touch (name them) What it should NOT do (scope the blast radius) The done signal (what command passes when it's complete?) No new tools. No worktrees yet. Write it down first. Once that's a habit, add Stage 1. The spec forces you to resolve assumptions before the agent makes them for you. This + a 15-minute planning session will save you 2+ hours of correction. For parallel execution: git worktree add before you open a second tab. One command, isolated branch, no collisions. I put together a starter kit with every template from this post: the spec, the task planner prompt, the agent scoping format, the worktree setup script, and a lean CLAUDE.md ready to drop into any repo. This Week in the News 🧠 A new contender just entered the coding model race:Jason Warner, GitHub’s former CTO’s startup, Poolside has released a new set of agentic coding models built specifically for software engineering workflows. This is a focused bet on where development is heading, with models designed for control, performance, and real-world coding tasks rather than general-purpose use. ⚙️ Cursor just turned its agents into an API: Cursor’s new TypeScript SDK lets you invoke its coding agents from CI pipelines, backend services, or even your own product. This moves agents out of the editor and into the system itself, where they can run tasks, automate workflows, and become part of how software gets built. ⚛️ React is still trying to automate performance:A year into the React Compiler experiment, the direction is becoming clearer. The goal is to shift performance optimization away from developers by letting the compiler handle memoization and rendering decisions, reducing the need for manual tuning in complex applications. 🟢 Node 26 hit a delay, but Temporal is still close: Node 26.0 was expected to land with the Temporal API enabled by default, but amacOS-related problem led to a last-minute delay. A fix is already in progress and a new release candidate is available, keeping Temporal on track to become part of the standard Node experience. 🧠 Where the goblins in ChatGPT actually came from:OpenAI dug into a strange behavior where its models kept referencing goblins and other creatures in unrelated contexts. The cause wasn’t a bug in the usual sense. It came from subtle training incentives, especially around personality tuning, where certain kinds of metaphors were rewarded more than expected and then spread across the model. It’s a small, almost funny example, but it points to something deeper. Model behavior is shaped by many tiny signals, and those signals don’t always stay contained. Beyond the Headlines 🧩 Coding is shifting from prompts to orchestration:This piece makes it clear that the real shift is not better prompts, but better coordination. Codex is evolving into a system that orchestrates multiple steps, tools, and agents to complete work. The focus is no longer on generating code, but on managing how that code gets produced. ⚠️ When output looks right but meaning drifts:A small example shows how generated output can subtly drift from the original intent while still looking correct. Nothing is obviously broken, which is exactly why it gets through. This is the kind of failure that does not show up in syntax or tests, but in meaning. 🧠 Work that looks complete but lacks depth:This piece explores how modern tools can produce outputs that resemble real knowledge work without the depth behind them. The results look finished, but the reasoning is often shallow. That gap becomes clear the moment judgment is required. 🔐 SVGs are still an easy way to get security wrong: SVGs look harmless, but they can carry scripts and unexpected behavior if not handled carefully. This write-up shows how common sanitization approaches fail and why this issue keeps slipping into production systems. ⚛️ Most React accessibility issues are small and avoidable:Accessibility problems in React apps rarely come from complex logic. They come from small gaps like missing labels, broken semantics, and poor keyboard support. These are easy to miss and just as easy to fix once you know where to look. Tool of the Week 🎬 Scroll-driven animations that finally feel natural Most scroll animations feel disconnected because they rely on timelines instead of user input. This guide shows how to tie motion directly to scroll position, which makes interactions smoother and easier to reason about. It’s a small shift in approach that fixes a surprisingly common UI problem. That’s all for this week. Have any ideas you want to see in the next article? Hit Reply! Cheers! Editor-in-chief, Kinnari Chohan 👋 Advertise with us Interested in sponsoring this newsletter and reaching a highly engaged audience of tech professionals? Simply reply to this email, and our team will get in touch with the next steps. SUBSCRIBE FOR MORE AND SHARE IT WITH A FRIEND! *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}.image_block img+div{display:none}sub,sup{font-size:75%;line-height:0}#converted-body .list_block ol,#converted-body .list_block ul,.body [class~=x_list_block] ol,.body [class~=x_list_block] ul,u+.body .list_block ol,u+.body .list_block ul{padding-left:20px} @media (max-width: 100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width: 100%;display:none;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}.social_block .social-table{display:inline-block!important}} @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }
Read more
  • 0
  • 0

Kinnari Chohan
27 Apr 2026
14 min read
Save for later

WebDevPro #137: Why Blocking Code Breaks Node.js Performance

Kinnari Chohan
27 Apr 2026
14 min read
Crafting the Web: Tips, Tools, and Trends for Developers Advertise with Us|Sign Up to the Newsletter @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } WebDevPro #137 Why Blocking Code Breaks Node.js Performance Crafting the Web: Tips, Tools, and Trends for Developers Catch the latest HubSpot Developer Platform updates in Spring Spotlight Spring Spotlight 2026 is live and we’ve rounded up the top updates for developers. See what's new for the HubSpot Developer Platform! Ship faster with AI coding tools like Cursor, Claude Code, and Codex. Build MCP-powered AI connectors, run serverless functions with support for UI extensions, and use date-based versioning to streamline roadmap planning. Explore Now 📢 Important: WebDevPro is Moving to Substack We’ll be moving WebDevPro to Substack soon.Once the transition is complete, all future issues will come from packtwebdevpro@substack.com. To make sure the newsletter continues reaching your inbox, please add this address to your contacts or whitelist it in your mail client. No other action is needed. You’ll keep receiving WebDevPro on the same weekly schedule. Substack will also give you more control over your subscription preferences if you decide to adjust them later. Welcome to this week’s issue of WebDevPro! Node.js is often chosen for its ability to handle concurrent workloads efficiently. Its event-driven, non-blocking architecture allows applications to process multiple operations without waiting for each one to complete. This model is particularly effective in I/O-heavy systems where responsiveness matters more than sequential execution. Because of this, there is a widespread assumption that Node.js applications will perform well under load by default. In practice, that assumption depends on one critical condition: the application must remain non-blocking.Once blocking behavior is introduced, the system begins to behave very differently. Blocking code does not always cause immediate failures. In developmentenvironments,where concurrency is limited, the system may appear stable. Requestscomplete,responses are returned, and nothing seems obviously wrong. This creates a false sense of confidence in how the application will behave in production. Under real-world conditions, however, multiple operations occur simultaneously, and the event loop becomes a shared dependency across all of them. At that point, the cost of blocking becomes visible. Delays accumulate, responsiveness drops, and the system begins to struggle under load. The issue is not simply that blocking code exists, but how it interacts with the event loop and how that interaction scales. Before we get into it, here’s this week at a glance: ⚛️ TanStack Start adds experimental React Server Components support 🤖 Claude introduces “routines” for repeatable workflows 🧩 GitHub introduces stacked PRs in private preview 🎨 Why AI still struggles with frontend development 🧹 Mastering ESLint rules for better code quality 🎤 Inside npmx, a new way to explore the npm ecosystem What blocking means in practice In Node.js, all JavaScript execution happens on a single thread, coordinated by the event loop. The event loop continuously processes tasks by retrieving them from a queue, executing them, and delegating work when possible so that execution can continue without interruption. This model works efficiently because it assumes that tasks will be short-lived and non-blocking. Blocking code violates this assumption. When a blocking operation is executed, the event loop cannotproceedto the next task until the current one completes. This does not simply delay a single operation. It prevents all other pending tasks from executing during that time, effectively pausing the system’s ability to make progress. The impact is broader than it first appears. Callbacks that are ready to run remain in the queue. Timers do not fire at expected intervals. Incoming requests must wait before they can even begin execution. What appears to be a local delay becomes a system-wide bottleneck. This shared delay is what makes blocking code particularly problematic in Node.js. In systems with multiple threads, delays can be absorbed or distributed. In Node.js, the delay is centralized. A single blocking operation affects everything else that depends on the event loop. Why synchronous APIs are risky in Node.js Synchronous APIs are often attractive because they simplifyreasoning aboutcode. Execution flows in a straight line, making it easier to follow and debug. This simplicity can be beneficial in scripts or isolated tasks where concurrency is not a concern. In an event-driven system like Node.js, however, synchronous APIs introduce a significant limitation. Because theyexecute onthe main thread, they block the event loop until they complete. During this time, no other operations can be processed, regardless of how unrelated they may be. This creates a disconnect between how the code appears and how it behaves under load. The code may look efficient and predictable, but its execution forces all operations into a sequential pattern. Instead of handling multiple tasks concurrently, the system processes them one at a time. The result is reduced flexibility in how work is handled. As more operations depend on the event loop, the impact of synchronous APIs becomes more pronounced. What begins asa simple designchoice can evolve into a system-wide constraint. CPU-intensive work and hidden blocking Blocking behavior is not limited to I/O operations. CPU-intensive tasks can have an even greater impact, particularly when executed synchronously. These operations consume the main thread for their duration, preventing the event loop from processing other tasks. The book illustrates this with a cryptographic key generation example, where a synchronous function is executed repeatedly. Each iteration runs on the main thread, and the cumulative effect is a prolonged period during which theeventloop is unavailable. performance.mark("start-sync"); for (leti= 0;i< 10000;i++) { generateKeyPairSync("rsa", { modulusLength: 1024, }); } performance.mark("end-sync"); performance.measure("generateKeyPairSync", "start-sync", "end-sync"); In this scenario, the cost is not just the execution time of a single operation, but the accumulation of blocking across many iterations. The event loopremainsoccupied for the entire duration, preventing any other work from progressing. performance.mark("start-async"); for (leti= 0;i< 10000;i++) { generateKeyPair("rsa", {modulusLength: 1024 }, () => {}); } performance.mark("end-async"); performance.measure("generateKeyPair", "start-async", "end-async"); The asynchronous version changes how the work is executed. Instead of occupying the main thread, the operations are delegated, allowing the eventloop toremain available. This difference has a direct impact on system responsiveness, especially under load. The performance gap is not theoretical The difference between synchronous and asynchronous execution is not just conceptual. The bookdemonstratesthat the impact can be measured andobservedin practice. The synchronous version of the operation takes significantly longer and blocks execution entirely. The asynchronous version, by contrast, allows the system to continue processing other tasks while the work is being performed. This leads to betterutilizationof system resources and improved responsiveness. This distinction changes how performance should be evaluated. In Node.js, performance is not only about how quickly a single operation completes. It is about whether the system can continue to process other work during that time. A fast operation that blocks the event loop can still degrade system performance if it prevents other tasks from progressing. Conversely, a slower operation that does not block may have less overall impact on system responsiveness. Why this matters more under load Blocking behavior becomes more problematic as concurrency increases. In low-load environments, tasks are processed with minimal overlap, and delays may not be noticeable. The system appears stable because there is little competition for the event loop. As the number of concurrent operations grows, the situation changes. Multiple tasks begin to depend on the event loop at the same time. Each task expects to be processedin a timely manner, and blocking operationsdisruptthis expectation. When a blocking operation runs, it prevents the event loop from servicing other tasks. These tasks begin to accumulate in the queue, increasing waittimesand reducing overall throughput. The system becomes less responsive as more work is added. This is why blocking issues often appear only under load. The system needs sufficient concurrency for the delays to become visible. Once that threshold is reached, the impact becomes more pronounced, and performance begins to degrade more rapidly. Async wrappers do not remove blocking A common misconception is that using asynchronous syntax automatically makes an operation non-blocking. Wrapping a function in async/await or a callback does not change how the underlying work is executed. The book emphasizes that async syntax controls how results are handled, not how the work itself is performed. If the underlying operation is synchronous and CPU-intensive, it will still block the event loop. This distinction highlights the difference between code structure and execution behavior. A function may appear asynchronous in form while still behaving in a blocking manner in practice. Understanding this difference is essential foridentifyingperformance issues. Changing syntax without addressing the underlying execution does not resolve the problem. Reducing work instead of changing execution In some cases, improving performance is not about changing how an operation executes, but reducing how often it runs. The bookdemonstratesthis with an example where repeated computation is replaced with a caching approach. Instead of recalculating an expensive result for every request, the system stores the result and updates it periodically. This reduces the number of times the operation is executed, lowering the load on the event loop. letcachedSignature= null; const app =Express(); constsigningMiddleware= (_req, res, next) => { if (!cachedSignature) { console.info("Signature is not cached"); cachedSignature=signData(Date.now().toString()); setInterval( () => (cachedSignature=signData(Date.now().toString())), 10000); } res.setHeader( "X-Signature", `data=${cachedSignature.data.toString()};kid=${cachedSignature.keyId};sha512=${cachedSignature.signature}`); next(); }; This approach shifts the focus from execution style to execution frequency. By reducing repeated work, the system becomes more efficient without changing how the operation itself is implemented. The real trade-off Blocking code is often easier to write and reasonabout. Its linear execution model provides clarity and predictability, making it attractive in many situations. However, this simplicity comes at a cost in systems that rely on concurrency. Blocking operations limit the ability of the event loop to process multiple tasks efficiently, reducing overall system responsiveness. The trade-off is not simply between synchronous and asynchronous code. It is between local simplicity and system-wide performance. A piece of code that is easy to understand in isolation may introduce constraints that affect the entire application. As systems scale and concurrencyincreases, this trade-off becomes more significant. Decisions that seem minor at the code level can havesubstantialimpactatthe system level. Key Takeaways Blocking code prevents the event loop from processing other tasks, creating system-wide delays. Synchronous APIs introduce constraints that limit concurrency. CPU-intensive operations can block execution even when usedwith asyncsyntax. Performance in Node.js depends onmaintainingevent loop availability. Reducing execution frequency can improve efficiency without changingexecutionstyle. Final Thought Node.js relies on a responsive event loop to handle concurrent operations effectively. Blocking code disrupts this model by introducing delays that affect all tasks, not just the one being executed. The impact of blocking behavior is not always immediate, but it becomes clear under load. As concurrency increases, the system’s ability to process work efficiently depends on keeping the event loop free. Ifyou’dlike to read more on this, Node.js Design Patterns explores these ideas in depth, especially around asynchronous behavior, performance, and the architectural decisions behind scalable Node.js systems. This Week in the News 🧠 Tokenmaxxing is the new productivity metric developers are gaming:There’s a new habit showing up in AI-heavy workflows. Teams are starting to track and even optimize for token usage, treating it as a signal of productivity. Gergely Orosz digs into why that framing breaks down quickly. More tokens don’t mean better outcomes, just more input. It’s the same pattern developers have seen before with lines of code and story points, now resurfacing in an AI-shaped form. What’s interesting here is not the trend itself, but how quickly it appeared. Even with new tools, the instinct to measure the wrong thing hasn’t changed. ⚡ TypeScript 7.0 goes 10x faster with a Go rewrite: The TypeScript team has released the 7.0 beta after spending the past year porting the entire compiler to Go. The result is a version that’s roughly 10x faster than TypeScript 6.0, while keeping the type-checking behavior structurally the same. For developers, that means no major migration or new errors to worry about. Just significantly faster builds out of the box. 🟢 Node.js moves toward Temporal API and stabilizes key features:Node.js is preparing to support the Temporal API by default, likely landing in the upcoming v26 release. This brings a modern, more reliable alternative to JavaScript’s existing Date handling into the runtime. At the same time, Node.js 24.15.0 (LTS) marks require(esm), and the module compile cache as stable, and introduces a new --max-heap-size flag. Together, these updates signal continued progress in both runtime capabilities and performance tooling. 📧 React Email 6 simplifies a fragmented ecosystem:React Email 6 introduces a major update focused on cleaning up versioning issues across its ecosystem. The release makes it easier to manage dependencies and ensures the CLI and components stay in sync. For teams building email templates with React, this should reduce friction and make the overall workflow more predictable. Beyond the Headlines ⚡ A new Angular compiler built on Oxc:This post explores an experimental Angular compiler powered byOxc, a Rust-based toolchain focused on performance. The goal is to significantly speed up builds and modernize the compilation pipeline. It’sanother signal that JavaScript tooling is steadily moving toward Rust-based infrastructure to push performance boundaries. 🚨 Investigating fake stars in GitHub repos: This investigation looks into how some GitHub repositories inflate their popularity using fake stars. It highlights how easily perception can be manipulated and why star counts aren’t always a reliable signal of quality. For developers,it’sa reminder to evaluate projects based on code, activity, and community, not just metrics. 🎥 Rethinking modern web architecture: This talk dives into how modern web architecture is evolving, covering trade-offs in performance, complexity, and developer experience. It offers a broader perspective on how current patterns scale in real-world applications. A useful watch ifyou’rethinking beyond frameworks and into long-term system design. 🧩 Features worth borrowing from npmx: This article breaks down specific ideas from the npmx project that could inspire better developer tools. It highlights practical features that improve how developers explore and interact with packages. The takeaway is simple. Good tooling often comes from rethinking small UX details. 🛠️ Why app stability matters more than ever:Cursor shares insights into building stable applications, especially in environments where rapid iteration and AI-assisted development are becoming the norm. The focus is on reducing breakage and maintaining reliability as systems evolve. It’sa reminder that speed is valuable, but stability is what keeps users and teams productive. Tool of the Week 🎬 Add unique animations to your React apps Building engaging UIs often means going beyond basic transitions.Animataoffers a collection of 100+ animation-focused React components, including effects like animated beams, spreading cards, and even a Slack-style intro screen. It’sa handy resource if you want to add more personality to your UI without building complex animations from scratch. That’s all for this week. Have any ideas you want to see in the next article? Hit Reply! Cheers! Editor-in-chief, Kinnari Chohan 👋 Advertise with us Interested in sponsoring this newsletter and reaching a highly engaged audience of tech professionals? Simply reply to this email, and our team will get in touch with the next steps. SUBSCRIBE FOR MORE AND SHARE IT WITH A FRIEND! *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}.image_block img+div{display:none}sub,sup{font-size:75%;line-height:0}#converted-body .list_block ol,#converted-body .list_block ul,.body [class~=x_list_block] ol,.body [class~=x_list_block] ul,u+.body .list_block ol,u+.body .list_block ul{padding-left:20px} @media (max-width: 100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width: 100%;display:none;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}.social_block .social-table{display:inline-block!important}} @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }
Read more
  • 0
  • 0
Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at €18.99/month. Cancel anytime
Kinnari Chohan
22 Apr 2026
12 min read
Save for later

WebDevPro #136: Agent Continuous Learning Framework: Build Traces, Refine Context, Iterate with a Harness (like DeerFlow), then Fine Tune the Model

Kinnari Chohan
22 Apr 2026
12 min read
Crafting the Web: Tips, Tools, and Trends for Developers Advertise with Us|Sign Up to the Newsletter @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } WebDevPro #136 Agent Continuous Learning Framework: Build Traces, Refine Context, Iterate with a Harness (like DeerFlow), then Fine‑Tune the Model By Daniel He, Co‑Author of DeerFlow Hi , Welcome to this week’s bonus issue of WebDevPro! DeerFlow is an open‑source SuperAgent framework based on LangGraph, focusing on multi‑agent orchestration, with 60K+ stars on GitHub. Core contributor Daniel He has long been deeply involved in agent workflow design and stateful graph execution, and he is dedicated to pushing autonomous agents to the true limits of production environments. This article is a speaker feature for an upcoming FREE live session hosted by Packt, where the DeerFlow team will walk through how these ideas translate into real systems. Build AI Agents with DeerFlow 2.0 Expect live demos, including agents that review pull requests and generate full research reports from a single prompt, along with a closer look at how DeerFlow coordinates models like GPT, Gemini, and DeepSeek behind the scenes. The event is designed for engineers, AI practitioners, product teams, and anyone exploring autonomous workflows or open source agent systems. It also includes insights from the maintainers on how the project evolved from DeerFlow 1.0 to 2.0, what is coming next, and how to get involved. 📅 Wednesday, May 6 • 9 AM – 10:30 AM EDT 🌐 Free online event by Packt Publishing Register now How exactly can an agent system "continuously become stronger"? It relies on training data, training infrastructure, and evaluation loops, which makes this kind of continuous improvement a platform-level capability rather than something a typical product team can iterate on frequently. Recently, Harrison Chase, the founder of LangChain, posted an X thread that breaks down the Agent Continuous Learning system into three layers: Model (model weights), Harness (execution mechanism), and Context (configurable memory). He then combined this with cutting-edge work such as Meta-Harness and LangChain Deep Agents to analyze the learning methods, implementation costs, and applicable scenarios of each layer. Based on this analysis, a possible action path for product teams is: first, get Traces right, then do Context learning, then establish a Harness optimization loop, and finally consider model fine-tuning. The core argument of this article is very clear: For AI agents, "continuous learning" should not be understood as merely updating model weights. An agent system can actually evolve continuously on three levels: Model, Harness, and Context. Core Framework: Three-Layer Agent Learning In his thread, Harrison broke down the agentic system into three layers: Model: The underlying model itself, which is the weights. Harness: The "shell" that drives the model's operation, including agent code, fixed tools, fixed hints, execution loops, etc. Context: A configurable context located outside of the harness, such as memory files, skills, user configurations, and team configurations. The value of this definition lies in the fact that it expands "learning" from a single model training problem into a complete systems engineering problem. Layer-by-layer interpretation 1. Model layer: The most traditional, but also the most complex layer. This layer corresponds to continuous learning, which is most familiar to everyone: Update weights usingmethods such as SFT and RL. It is also possible to use a more fine-grained adaptation method, such asLoRA. The goal is to make the model perform better on new tasks. However, an old problem persists: catastrophic forgetting. That is, after the model learns new things, its old abilities actually degenerate. Daniel’s judgment is: For most teams, the Modellayer has the highest continuous learning cost. It relies on training data, training infrastructure, evaluation loop, and model deployment mechanism. This is more like a platform-level capability than a routine method that a typical product team can frequently iterate upon. So while Harrison acknowledges the importance of the model layer, his real focus is not there. 2. Harness Layer: The most underestimated leverage point in agent engineering over the past year. “Harness” does not refer to the model itself, but rather "how the model is used": How to write system prompts How to expose tools to the model How to organize loop calls When to truncate the context, when to retry, and when to determine if the task is complete Which logs and traces are saved for later analysis Harrison specifically mentions the work at https://yoonholee.com/meta-harness/ in his article. Its main idea can be summarized as follows: Run the agent on a batch of tasks. Collect complete execution logs and scores. Store these historical candidates, source code, and execution traces in the filesystem. Then,have a coding agent read these materials and propose new harness modifications. Evaluating the new harness and continuing its iterations. This is important because it illustrates: Agent evolution can be achieved without changing the model, only the runtime framework. The truly high-value optimization targets are often the "execution mechanism" rather than the "parameters". The more complete the traces, the more harness optimization resembles an engineering iteration rather than a matter of guesswork and prompt adjustments. The official Meta-Harness page also presents strong results: it emphasizes its key difference by allowing the optimizer to see the complete historical code, scores, and execution traces, rather than just a summary. The authors claim that this "filesystem-level context" can increase the available diagnostic information for each round of optimization by an order of magnitude compared to traditional methods. 3. Context Layer: Closest to the business logic, and best suited for initial implementation Harrisondefines Context as content located outside of the harness used to configure the agent, for example: Instructions Skills Tools Memory files User preferences Team rules The key to this layer is not "what the model has learned", but "what the system has remembered and how to continue using it in subsequent sessions". The official documentation for LangChain Deep Agents explains this in great detail. It supports: Agent-scoped memory: All users share the same agent memory. User-scoped memory: Each user has their own independent memory. Online update: Write directly to memory during session Backend organization: Performing consolidation outside the session Skills are a form of procedural memory, loaded only when needed. This shows that the Context layer is not as simple as "adding more prompts", but a persistent, hierarchical, readable, writable, and searchable memory system. Two examples from the author Harrison used two mappings in the article, first for Claude Code: Models: Claude Sonnet, and so on Harness: Claude Code itself User context: CLAUDE.md, /skills, mcp.json This breakdown is very practical because it directly illustrates: Your perception that "Claude Code has become smarter" may not necessarily be due to changes in the model weights; It's also possible that the harness has been changed; Or perhaps you've configured its context better; And again for OpenClaw: Model: Can accept multiple models Harness: Pi (powers OpenClaw) plus some running scaffolding Agent context: SOUL.md and skills from ClawHub OpenClaw's public documentation says: SOUL.md is officially defined as the personality and tone configuration file for the agent ClawHubis defined as a public registry of OpenClaw skills/plugins This precisely confirms Harrison's point: the "continuous learning" of many agent systems essentially occurs at the configurable context layer, rather than the model fine-tuning layer. The paradigm shift this article aims to promote To summarize this article into a single sentence: An agent's continuous learning is shifting from "training the model" to "optimizing the entire system". There are at least three changes here: 1. The learning objective shifts from weights to system behavior. Traditional LLM continuous learning focuses more on: Has the loss decreased? Has the benchmark improved? Agent continuous learning focuses more on: Are they better at using tools? Are they better at planning steps? Can we improve our execution strategies by learning from past failures? Do we have a better understanding of the preferences of current users, teams, or organizations? 2. The learning unit is shifting from a single model to a hierarchical architecture. Within the same agent system, the learning frequency and cost of different layers are completely different: Model: Low frequency, high cost, platform level Harness: Mid-frequency, engineering-driven, measurable Context: High frequency, business-driven, most likely to occur online This means that continuous learning should not have just one master switch, but rather three different mechanisms. 3. Traces become a unified fuel. Harrison repeatedly emphasizes "traces" at the end of the article. This is one of the most crucial infrastructure assessments in the entire text. The reason is straightforward: To modify the model, you need traces as a source of training/preference data To improve harnessing, you need traces as diagnostic material for failures To modify the context, you need traces as source material for experience extraction In other words, without high-quality traces, there is no high-quality agent learning loop. Some personal judgments 1. The Context layer will be the first to become widespread. Daniel believes that the Context layer, rather thanthe Modellayer, will be the first of the three layers to be widely implemented. Reasons: No training required The most direct impact on business returns Isolation can be done by user / team / org Easy to manage permissions and rollback Easier to meet the controllability requirements of enterprise systems Many capabilities that are packaged today as "the agent can remember" essentially belong to this level. 2. The Harness layer will become the focus of the next round of agent infrastructure competition. If 2024 was mainly about who could get their agent up and running first, then 2026 will be more about: Whose harness is more stable? Whose traces are more complete? Whose evaluation and replay system is more closed-loop? Who can quickly incorporate failure cases into the next version of agent behavior improvement? This is whywork like Meta-Harness is worth paying attention to. It represents a very engineering-oriented approach: letting the agent help you change the agent. Practical reflections on your team's development of agent products If your team is planning to incorporate "continuous learning" into its agent roadmap, Daniel recommend proceeding in the following order: Phase 1: First, get the traces right Unified recording of task input, tool calls, key intermediate states, output results, and human feedback Preserve reviewable evidence for failed tasks, rather than just reporting an error Add user/org/task/version dimension tags to traces Phase 2: Prioritize context learning Start with user preferences, team rules, glossary, and standard operating procedures (SOPs) Distinguish between read-only memory and writable memory Define the scope: which is user-level and which is org-level Support both online writing and offline defragmentation update paths Phase 3: Establish the Harness optimization loop Run the agent continuously on a standard task set Perform A/B testing and automated evaluation on the harness version The coding agent reads traces to help propose candidates for harness modification Establish a rollback mechanism to prevent situations where changing the prompt might seem convenient in the short term, but ultimately leads to decreased overall stability Phase 4: Consider model-level learning only as a last resort Only when you have accumulated enough high-quality traces Furthermore, optimizations at the harness/context layer are nearing their limits A reusable decision framework When faced with the question "How should an agent undergo learning?", you can first ask four questions: What needs to be changed this time? Is it the model capabilities, the operating mechanism, or the configurable memory? Should this change apply to theagent, user, or org scope? Should thisupdate occur immediately during runtime, or should it be processed in an offline task before taking effect? Arethere enough complete traces to support an assessment of whether this learning was truly effective? If these four questions cannot be answered clearly, the so-called "continuous learning" is most likely just a vague slogan. Conclusion The value of Harrison Chase's post and accompanying article lies not in proposing a completely new algorithm, but in breaking down agent continuous learning into a more practical three-layer framework: Model learning addresses underlying capabilities Harness learning addresses the execution mechanism Context learning addresses memory and personalization The two things that the product team should act on immediately are not training the model, but rather: Build the infrastructure for traces Productize the context and harness learning loop This is also Daniel’s core conclusion for this article: stronger agents in the future will not necessarily come from larger models, but are more likely to come from systems that are better at "reviewing, remembering, and reconstructing". Want to know more about how to turn your prompts into workflows with an agent harness? Join us for a live demonstration of DeerFlow 2.0 on 6th May! Register now! That’s all for this week. Have any ideas you want to see in the next article? Hit Reply! Cheers! Editor-in-chief, Kinnari Chohan 👋 Advertise with us Interested in sponsoring this newsletter and reaching a highly engaged audience of tech professionals? Simply reply to this email, and our team will get in touch with the next steps. SUBSCRIBE FOR MORE AND SHARE IT WITH A FRIEND! *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}.image_block img+div{display:none}sub,sup{font-size:75%;line-height:0}#converted-body .list_block ol,#converted-body .list_block ul,.body [class~=x_list_block] ol,.body [class~=x_list_block] ul,u+.body .list_block ol,u+.body .list_block ul{padding-left:20px} @media (max-width: 100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width: 100%;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}} @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }
Read more
  • 0
  • 0

Kinnari Chohan
20 Apr 2026
11 min read
Save for later

WebDevPro #135: MCP Is Redefining How We Expose Backend Capabilities

Kinnari Chohan
20 Apr 2026
11 min read
Crafting the Web: Tips, Tools, and Trends for Developers Advertise with Us|Sign Up to the Newsletter @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } WebDevPro #135 MCP Is Redefining How We Expose Backend Capabilities Crafting the Web: Tips, Tools, and Trends for Developers Lessons Learned from Security Incidents in Mobile Apps Join Security Researcher and Pentester,Jan Seredynski, onMay 12as he dissects real-world security incidents in banking, food delivery, and e-commerce. From face verification bypass to location spoofing, he'll break down the anatomy of a breach and what teams can do differently to address them. Register today! 📢 Important: WebDevPro is Moving to Substack We’ll be moving WebDevPro to Substack soon.Once the transition is complete, all future issues will come from packtwebdevpro@substack.com. To make sure the newsletter continues reaching your inbox, please add this address to your contacts or whitelist it in your mail client. No other action is needed. You’ll keep receiving WebDevPro on the same weekly schedule. Substack will also give you more control over your subscription preferences if you decide to adjust them later. Welcome to this week’s issue of WebDevPro! For years, web development has revolved around a stable contract: define endpoints, shape responses, and let clients call into your system. That model still works, but it starts to feel strained when the client is no longer just a browser or a mobile app, but an AI system that needs to discover, interpret, and use your backend dynamically. Most AI integrations today are still improvised. Teams wire up function calling, patch together tool layers, and build thin wrappers around internal APIs. The result, therefore, is predictable: duplication, brittle integrations, and systems that are hard to extend. MCP, on the other hand, introduces a different approach. Instead of treating AI as an add-on, it defines a standard way to expose capabilities so clients can discover and use them reliably. This is not just an AI story. It is an architectural shift that web developers should understand early. Before we get into it, here’s this week at a glance: ⚛️ TanStack Start adds experimental React Server Components support 🤖 Claude introduces “routines” for repeatable workflows 🧩 GitHub introduces stacked PRs in private preview 🎨 Why AI still struggles with frontend development 🧹 Mastering ESLint rules for better code quality 🎤 Inside npmx, a new way to explore the npm ecosystem From Endpoints to Capabilities Traditional APIs are built around endpoints. You define routes, attach handlers, and document how clients should call them. MCP shifts the focus from endpoints to capabilities. Instead of exposing a set of URLs, you expose: Tools Resources Templates They form the core building blocks of MCP, separating computation, context, and reusable instruction patterns. Endpoints assume the client already knows what exists. Capabilities allow the client to discover what is available at runtime. That removes a lot of implicit coupling between systems. It also reduces the need to hardcode assumptions into every integration. A Familiar Model, Framed Differently At a high level, MCP follows a structure that will feel familiar: A server exposes functionality A client connects and invokes it A host provides the environment where interactions happen Communication happens through structured messages, with an explicit handshake where both sides exchange capabilities before doing any work. The initialize-initialized handshake ensures both client and server agree on capabilities before any interaction begins. For web developers, this is closer to: API discovery combined with schema introspection Contract negotiation before execution Runtime awareness instead of static assumptions The difference is that this model is designed for AI clients that interpret intent rather than strictly follow instructions. Why This Matters in Practice The biggest problem MCP addresses is not performance or scale. It is integration friction. Without a standard: Each AI integration defines its own schema Tool selection becomes unreliable Context handling becomes inconsistent Systems become harder to compose The source material highlights three recurring pain points: Fragmented context and limited windows Tool and memory integration complexity Lack of composability across domains MCP tackles these by standardizing how capabilities are described and invoked. For web developers, this is similar to the role OpenAPI played for REST. It does not replace APIs. It makes them easier to integrate and reason about. The Real Design Challenge: Clarity Over Cleverness One of the most practical insights from the material is how much naming and structure influence behavior. In MCP systems: Tool descriptions guide selection Parameter names influence argument extraction Return shapes affect how results are used Vague naming leads to incorrect tool calls. Overloaded schemas introduce ambiguity. Overly complex resources increase cost and reduce accuracy. This is not new, but the consequences are sharper. In traditional APIs, poor naming slows developers down. In AI-driven systems, it changes how the system behaves. That forces a shift toward: Clear, literal naming Minimal and explicit schemas Stable, predictable outputs This is API design discipline applied in a stricter environment. Transport, Deployment, and the Web Stack Another reason this matters for web developers is where these systems end up. Local development may start with simple transports, but production systems move toward HTTP-based communication, aligning with existing infrastructure. That brings familiar concerns back into play: Routing and endpoints Authentication and token validation Rate limiting and gateway policies Observability and request tracing The recommendation to move toward a single HTTP endpoint for MCP traffic reflects a clear alignment with modern backend practices. It is an extension of the web stack and not just parallel ecosystem. Authentication Is No Longer Per Request One subtle but important shift is how authentication is handled. Instead of validating each request independently, MCP systems often establish trust at the connection or session level, with tokens validated before interaction begins. That changes how you think about: Access control Role mapping Scope enforcement It also pushes developers to consider: Which tools should be publicly accessible Which require elevated permissions How to isolate sensitive operations This is closer to service-level authorization than traditional request-level checks. From Local Tooling to Production Systems One of the strengths of the MCP approach is how it spans environments. The same server can: Run locally for testing Be inspected through development tools Be integrated into host environments Be deployed over HTTP for production use This continuity reduces the gap between experimentation and deployment. For web developers, that means fewer environment-specific rewrites, easier validation workflows, and more predictable rollout paths It also makes it easier to introduce AI-driven capabilities incrementally instead of rewriting entire systems. Where MCP Fits in a Web Developer’s Mental Model It helps to place MCP alongside existing concepts rather than treating it as something entirely new. Think of it as: An extension of API design for AI-native clients A structured way to expose backend capabilities A protocol for discovery, not just execution It does not replace REST or GraphQL. It complements them by sitting one layer above, where interpretation and routing happen. That positioning makes it easier to adopt without overhauling your architecture. Key Takeaways MCP reframes backend design from static endpoints to discoverable capabilities Tools, resources, and prompts map cleanly to actions, context, and reusable logic Clear naming and schema design directly influence system behavior HTTP-based deployment keeps MCP aligned with existing web infrastructure Authentication shifts toward session-level trust and scoped permissions The protocol reduces integration friction by standardizing how capabilities are exposed MCP is still early, but the direction is clear. AI clients are becoming another consumer of backend systems, and they require more than just endpoints. For web developers, this is less about learning a new tool and more about recognizing a familiar pattern evolving into something more dynamic. If you want to explore these ideas in more depth, check out Ship an MCP Server in Python FAST by Christoffer Noring. Building with AI? We want to hear from you 👀 Take our quick survey and help us shape content around real workflow bottlenecks. Take the survey This Week in the News ⚛️ TanStack Start adds experimental React Server Components support:TanStack Start now includes experimental support for React Server Components (RSC), bringing server-first patterns into its full-stack React framework. It’s another sign that RSC is slowly moving from theory into real-world tooling. 🤖 Claude introduces “routines” for repeatable workflows:Claude now supports routines, a way to define repeatable workflows for common tasks. Instead of prompting from scratch each time, developers can structure and reuse sequences, making interactions more consistent and efficient. 🎨 Hugo adds new CSS capabilities:Hugo’s latest updates introduce new CSS-related capabilities, expanding how styles can be handled within the static site generator. The changes aim to simplify styling workflows and give developers more flexibility when working with modern CSS setups. 🧩 GitHub introduces stacked PRs in private preview:GitHub has opened a private preview for stacked pull requests, adding native support for workflows where large changes are split into a chain of dependent PRs. This approach can make reviews more manageable and help teams ship complex features incrementally. Beyond the Headlines 🎨 Why AI still struggles with frontend development: While AI tools are getting better at generating code, frontend development remains a weak spot. This piece breaks down where things fall apart, especially around layout, styling, and the nuanced decisions that go into building good user interfaces. The bigger takeaway is that frontend work isn’t just about producing code. It involves visual judgment, edge cases, and context that are still hard for AI systems to consistently handle. 🏗️ Rethinking architecture with the vertical codebase: This article explores the idea of a “vertical codebase,” where features are organized end-to-end instead of being split across layers like components, services, and utilities. The approach focuses on grouping everything related to a feature in one place, making codebases easier to navigate and reason about. It’s a shift from traditional horizontal layering toward a structure that prioritizes ownership, clarity, and maintainability as applications grow. 🧹 Mastering ESLint rules for better code quality: This guide takes a deeper look at how to effectively use ESLint rules beyond basic setups. It focuses on understanding, customizing, and applying rules in a way that actually improves code quality instead of just adding noise. The key idea is that linting works best when it reflects team conventions and real-world usage, not just default configurations. 📦 Ten years of JavaScript modules on the web: It’s been ten years since the initial push to bring native JavaScript modules to the web platform. This retrospective looks at how far the ecosystem has come, from early proposals to widespread adoption across browsers and tooling. It’s a reminder of how foundational features evolve slowly, but end up reshaping how we structure and ship web applications. 🎤 Inside npmx, a new way to explore the npm ecosystem:This 50-minute conversation with the developers behind npmx dives into the thinking behind a new way to browse and interact with the npm registry. The project is gaining traction as an alternative approach to discovering and working with packages. It offers a closer look at how tooling around npm is evolving beyond installation into better exploration and developer workflows. Stop Prompting. Start Speccing. After a hugely successful Cohort 1, we're back with the Cohort 2 hands-on workshop! Here you'll build a real full-stack application using the same spec-first methodology used by MAANG engineering teams. 20 early bird seats at 40% off. Use code NL40 Tool of the Week 🎨 Extract color palettes directly from images Choosing the right color palette can be tricky, especially when working with images. Color Thief makes it simple by extracting dominant colors and palettes directly from images using a lightweight JavaScript library. It’s especially useful for building dynamic UIs, theming applications, or generating color schemes based on user content. That’s all for this week. Have any ideas you want to see in the next article? Hit Reply! Cheers! Editor-in-chief, Kinnari Chohan 👋 Advertise with us Interested in sponsoring this newsletter and reaching a highly engaged audience of tech professionals? Simply reply to this email, and our team will get in touch with the next steps. SUBSCRIBE FOR MORE AND SHARE IT WITH A FRIEND! *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}.image_block img+div{display:none}sub,sup{font-size:75%;line-height:0}#converted-body .list_block ol,#converted-body .list_block ul,.body [class~=x_list_block] ol,.body [class~=x_list_block] ul,u+.body .list_block ol,u+.body .list_block ul{padding-left:20px} @media (max-width: 100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width: 100%;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}} @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }
Read more
  • 0
  • 0

Kinnari Chohan
13 Apr 2026
14 min read
Save for later

WebDevPro #134: Rendering in Next.js is a system of trade-offs, not a feature checklist

Kinnari Chohan
13 Apr 2026
14 min read
Crafting the Web: Tips, Tools, and Trends for Developers Advertise with Us|Sign Up to the Newsletter @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } WebDevPro #134 Rendering in Next.js is a system of trade-offs, not a feature checklist Crafting the Web: Tips, Tools, and Trends for Developers Grow your mobile apps efficiently, without ads! Do you rely on paid ads for mobile growth? Don't the rising costs and limited visibility make it harder to scale? Insert Affiliate gives you another option. It lets you run an affiliate channel for your app, where partners, creators, or communities drive users via tracked links, and you can tie installs, purchases, and subscriptions back to the source. Start tracking affiliate revenue 📢 Important: WebDevPro is Moving to Substack We’ll be moving WebDevPro to Substack soon.Once the transition is complete, all future issues will come from packtwebdevpro@substack.com. To make sure the newsletter continues reaching your inbox, please add this address to your contacts or whitelist it in your mail client. No other action is needed. You’ll keep receiving WebDevPro on the same weekly schedule. Substack will also give you more control over your subscription preferences if you decide to adjust them later. Welcome to this week’s issue of WebDevPro! Most of us think rendering strategies were a matter of picking the “best” option. Server-side rendering felt powerful. Static generation felt fast. Client-side rendering felt flexible. The assumption was simple: choose one, commit, and move on. That mental model breaks down the moment your application grows beyond a few pages. In practice, rendering is not a decision you make once. It is a system of trade-offs you keep revisiting as your product evolves. Next.js does not force you into one approach. It gives you multiple levers and expects you to use them deliberately. The real shift is not technical. It is conceptual. You stop asking “Which rendering strategy should I use?” and start asking “Where should this piece of UI live, and when should it be rendered?” Before we get into it, here’s this week at a glance: 🧠 Copilot now asks another AI before trusting itself 🔐 Cloudflare just moved up the deadline for quantum-safe internet ⚙️ Node.js keeps shipping under-the-radar improvements ⚠️ How attackers are now targeting open source maintainers 🧩 Claude is not your software architect 🎥 The React hook most developers still ignore The illusion of a single rendering strategy Traditional frameworks leaned heavily in one direction. Server-rendered apps generated HTML on every request. Client-rendered apps shipped a JavaScript bundle and built everything in the browser. Static site generators pushed everything to build time. Each model worked well in isolation. Each also came with blind spots. Next.js breaks that boundary. You can render some pages at request time, others at build time, and still defer specific components to the client. This flexibility is powerful, but it also introduces a new responsibility: understanding the cost of each choice. Rendering is no longer about capability. It is about trade-offs across performance, scalability, user experience, and operational complexity. Server-side rendering is about control, not just freshness Server-side rendering still plays a critical role, especially when the content depends on the request itself. When a page is rendered on the server for every request, you gain control over what gets sent to the browser. You can safely access private APIs, validate data, and tailor responses per user. The browser receives fully formed HTML, which improves compatibility and helps search engines understand your content immediately. That control comes at a cost. Every request triggers computation. Every data dependency introduces latency. Even a well-optimized server will feel slower compared to serving a prebuilt file. Navigation between pages can feel heavier because each transition depends on server work. There is also an operational dimension. A server-rendered app is not just code. It is infrastructure. It scales with traffic, and that scaling has a cost. SSR shines when you genuinely need per-request freshness or personalization. It becomes a liability when used out of habit. A common mistake is defaulting to SSR for anything that “feels dynamic.” In reality, many of those pages do not need to be recomputed on every request. They only need to feel dynamic. Client-side rendering shifts responsibility to the browser Client-side rendering takes the opposite approach. Instead of sending meaningful HTML, the server delivers a minimal shell and a JavaScript bundle. The browser then builds the interface, fetches data, and manages state. This model feels fast after the initial load. Once the application is hydrated, navigation becomes seamless. Transitions are smooth. Interactions feel immediate. The app behaves more like a native experience. There is a reason this approach became popular. It reduces server workload significantly. The server becomes a delivery mechanism rather than a computation layer. You can scale more easily, especially in serverless environments. You also gain flexibility in how and when you fetch data. But the trade-offs are hard to ignore. The first load can be painfully slow on weak networks. Users may stare at an empty screen while JavaScript downloads and executes. Search engines see very little initial content, which can impact discoverability and performance scores . Client-side rendering works well when SEO is not a priority and when interactivity outweighs initial load performance. Think dashboards, internal tools, or authenticated user spaces. Even then, it should be a deliberate choice, not a default fallback. Static generation optimizes for speed and scale Static site generation flips the model again. Instead of rendering on the server per request or in the browser at runtime, you render at build time. The output is a static HTML file that can be served instantly. This approach is hard to beat in terms of performance. There is no computation at request time. The server simply returns a file. CDNs cache and distribute it globally. Latency drops dramatically. Scalability becomes almost trivial . It is also inherently secure. There are no runtime calls to sensitive APIs. No database queries during requests. Everything needed is already embedded in the generated output. The limitation is obvious. Static content does not change unless you rebuild. That constraint used to make static generation impractical for dynamic applications. Rebuilding an entire site for a small content change is inefficient and often unrealistic. Next.js softens that limitation through incremental static regeneration. Incremental regeneration changes the conversation Incremental static regeneration introduces a middle ground. You can generate a page once and then update it periodically without rebuilding the entire application. Instead of choosing between fully static and fully dynamic, you define how stale your data can be. A page might be regenerated every few minutes, hours, or based on traffic patterns. The first request after the revalidation window triggers a new render, and subsequent users receive the updated version . This model shifts the question from “Is this page static or dynamic?” to “How fresh does this data need to be?” Many applications do not require real-time updates. A slight delay is acceptable if it significantly improves performance and reduces load. ISR lets you capture that balance without overcommitting to server-side rendering. Rendering decisions are architectural decisions At an intermediate level, the real challenge is not understanding how each strategy works. It is knowing where to apply them. Rendering is tightly coupled with architecture. A marketing page benefits from static generation because it rarely changes and needs to load instantly. A product listing page might use a mix of static generation and periodic revalidation. A user dashboard leans toward client-side rendering for responsiveness. A checkout flow may rely on server-side rendering for security and consistency. These are not isolated decisions. They influence data flow, caching strategies, and even how teams structure their codebase. Rendering becomes part of the system design, not just a framework feature. The hidden cost of getting it wrong Misusing rendering strategies can degrade your application. Overusing server-side rendering can lead to unnecessary latency and higher infrastructure costs. The app works, but it feels slower than it should. Relying too heavily on client-side rendering can hurt first impressions. Users wait longer. Search engines struggle to index content. Performance scores drop. Using static generation without a plan for updates can create stale experiences. Content lags behind reality. Users notice. The tricky part is that each of these issues emerges gradually. They rarely show up in small projects or local environments. They appear under real traffic, real data, and real user expectations. That is why rendering decisions deserve more attention than they usually get. Hybrid rendering is not a feature but a mindset Next.js is often described as a hybrid framework. That description is accurate but incomplete. A single page can combine multiple approaches. The shell might be statically generated. Critical data might be fetched on the server. Interactive components might rely on client-side rendering. Each part of the page is treated independently based on its requirements. This is where the framework becomes powerful. You are not forced into a single pattern. You can optimize each piece of the experience. The challenge is maintaining clarity. Without clear boundaries, hybrid approaches can become messy. It is easy to lose track of where data is fetched, where rendering happens, and why certain decisions were made. What developers often miss At this stage, most developers understand the mechanics of SSR, CSR, and SSG. The gap is usually in decision-making. Three patterns tend to show up repeatedly. The first is defaulting to server-side rendering for anything dynamic. It feels safe, but it often introduces unnecessary overhead. The second is treating client-side rendering as a performance solution. It improves interactions but can hurt initial load and SEO. The third is underestimating static generation. Many pages that could be static end up being rendered dynamically simply because it feels easier. These patterns become problematic when applied without context. A more practical way to think about rendering Instead of categorizing pages by rendering type, it helps to evaluate them through a few simple questions: How often does this data change? Does this content need to be indexed by search engines? How important is initial load performance? Does this page require per-user personalization? What is the acceptable level of staleness? These questions lead you toward a more balanced approach. A page that rarely changes and needs strong SEO leans toward static generation. A page with user-specific data leans toward server or client rendering. A page with moderate updates fits well with incremental regeneration. Rendering stops being a technical choice and becomes a product decision. The real takeaway Rendering strategies are not competing features. They are tools with different costs and benefits. Next.js gives you the ability to combine them, but it does not make the decisions for you. That responsibility sits with the developer. When you align your rendering choices with the needs of your application, performance improves, infrastructure becomes more efficient, and the user experience feels intentional. When you do not, the issues show up slowly and compound over time. Rendering is one of those areas where small decisions have outsized impact. It is worth treating it as a first-class concern, not an afterthought. Enjoyed this piece? Take it further with Real-World Next.js by Michele Riva, and learn how to build scalable, high-performance modern web applications. Building with AI? We want to hear from you 👀 Take our quick survey and help us shape content around real workflow bottlenecks. Take the survey This Week in the News 🧠 Copilot now asks another AI before trusting itself:GitHub is experimenting with a “second opinion” system inside Copilot CLI. A separate model from a different family reviews the agent’s plan and code to catch blind spots and edge cases before execution. The bigger shift is architectural. AI tools are starting to validate each other, not just generate output. One model writes, another critiques, and the developer sits above both. 🔐 Cloudflare just moved up the deadline for quantum-safe internet: Cloudflare is accelerating its roadmap to make the entire platform post-quantum secure by 2029, including authentication, not just encryption. The urgency comes from recent breakthroughs suggesting current cryptography could be broken sooner than expected. The takeaway is simple. Quantum risk is no longer theoretical, and migration timelines are shrinking fast. ⚙️ Node.js keeps shipping under-the-radar improvements: Recent updates to the current release line focus on better memory control, improved test runner capabilities, and incremental API enhancements. Nothing headline grabbing, but that is the point. Node is doubling down on stability and developer ergonomics rather than chasing trends. 📦 A new way to explore npm without the CLI: Patak and Zeu introduced npmx, a fast, browser-based interface for exploring the npm ecosystem. It focuses on speed, discoverability, and a cleaner way to navigate packages without relying on terminal workflows. The idea is simple but interesting. As ecosystems grow, developer tooling is shifting from command-heavy interfaces to faster, more visual ways of understanding what’s out there. Beyond the Headlines ⚠️ How attackers are now targeting open source maintainers: Simon Willison breaks down a new wave of supply chain attacks that rely on social engineering, not exploits. Maintainers are being manipulated into merging malicious code through trust, urgency, and subtle persuasion. The pattern is uncomfortable but clear. The weakest point in the supply chain is no longer code. It is the human reviewing it. 🧩 Claude is not your software architect: This piece challenges a growing assumption that AI can design systems end-to-end. While models can generate code and suggest structures, they lack the context needed for real architectural decisions. The distinction matters. AI can assist implementation, but architecture still depends on trade-offs, constraints, and long-term thinking. 🔐 Your email is still easy to scrape: Email obfuscation techniques that used to work are now trivial to bypass. This article shows how bots extract addresses even from “protected” formats and what actually works today. The takeaway is practical. Security through obscurity is fading fast, especially when automation keeps improving. 🎥 The React hook most developers still ignore:useSyncExternalStore solves a specific but important problem: syncing external state with React without breaking consistency. It rarely shows up in everyday code, which is why many developers overlook it. But once you start working with shared state across systems, it becomes one of those tools that quietly fixes hard-to-debug issues. Stop Prompting. Start Speccing. After a hugely successful Cohort 1, we're back with the Cohort 2 hands-on workshop! Here you'll build a real full-stack application using the same spec-first methodology used by MAANG engineering teams. 20 early bird seats at 40% off. Use code NL40 Tool of the Week 🦴 Generate skeleton screens directly from your DOM Boneyard takes a snapshot of your existing DOMand automatically generates pixel-perfect skeleton screens. Instead of manually building placeholders, it mirrors your actual layout, saving time and keeping loading states visually consistent. It’s a small idea with a practical payoff, especially for apps where perceived performance matters as much as real speed. That’s all for this week. Have any ideas you want to see in the next article? Hit Reply! Cheers! Editor-in-chief, Kinnari Chohan 👋 Advertise with us Interested in sponsoring this newsletter and reaching a highly engaged audience of tech professionals? Simply reply to this email, and our team will get in touch with the next steps. SUBSCRIBE FOR MORE AND SHARE IT WITH A FRIEND! *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}.image_block img+div{display:none}sub,sup{font-size:75%;line-height:0}#converted-body .list_block ol,#converted-body .list_block ul,.body [class~=x_list_block] ol,.body [class~=x_list_block] ul,u+.body .list_block ol,u+.body .list_block ul{padding-left:20px} @media (max-width: 100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width: 100%;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}} @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }
Read more
  • 0
  • 0

Kinnari Chohan
06 Apr 2026
11 min read
Save for later

WebDevPro #133: Rethinking Backend Communication for Real-Time Systems

Kinnari Chohan
06 Apr 2026
11 min read
Crafting the Web: Tips, Tools, and Trends for Developers Advertise with Us|Sign Up to the Newsletter @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } WebDevPro #133 Rethinking Backend Communication for Real-Time Systems Crafting the Web: Tips, Tools, and Trends for Developers Job postings at Microsoft, Deloitte, Accenture, and KPMG are now listing Copilot Studio, Power Automate, and MCP as must-have skills. The fastest way to get there? Build a real enterprise AI agent in 2 days LIVE, with instructors from Microsoft and Confluent. Use code SAVE40 for 40% off. Grab your spot: April 18-19 → 📢 Important: WebDevPro is Moving to Substack We’ll be moving WebDevPro to Substack soon.Once the transition is complete, all future issues will come from packtwebdevpro@substack.com. To make sure the newsletter continues reaching your inbox, please add this address to your contacts or whitelist it in your mail client. No other action is needed. You’ll keep receiving WebDevPro on the same weekly schedule. Substack will also give you more control over your subscription preferences if you decide to adjust them later. Welcome to this week’s issue of WebDevPro! Most backend systems are built around a simple assumption: the client asks, the server responds, and the connection ends. That model has worked for decades because it fits neatly with how the web evolved. It scales well, it’s easy to reason about, and most infrastructure is designed around it. But the moment you step into real-time features, that assumption starts to break. Chat apps, collaborative tools, live dashboards, multiplayer systems, and even notifications all share one requirement: the server needs to speak without being asked. That single shift changes how you design your backend, how you think about state, and how you handle communication altogether. This is where event-driven systems enter the picture. Before we get into it, here’s this week at a glance: 🤖 Cursor 3 is built around an agent-first workflow ⚙️ Next.js moves beyond Vercel with a new Adapter API 🤖 AI debugging shifts toward agent-driven workflows at Sentry ⚠️ Copilot incident highlights risks in AI-assisted coding ⚡ Trigger.dev reports major performance gains after switching to Bun 📦 npm workspaces finally make sense Where Request-Response Falls Short Consider a typical chat system built with REST: Fetch messages with GET /messages Send messages with POST /messages Poll regularly to check for updates Polling is the problem. You either: Poll frequently and waste resources, or Poll less often and introduce lag Even long polling only stretches the same model. The client is still responsible for initiating communication. Event-driven systems remove that constraint. Instead of asking repeatedly, the client stays connected and receives updates as they happen. Persistent Connections Change the Rules WebSockets make this possible by keeping a connection open between client and server. Both sides can send messages at any time. This introduces three key shifts: Communication becomes bidirectional The server can initiate updates The connection now carries state That last point is the tradeoff. Stateless systems are easy to scale. Stateful connections require you to manage: Connection lifecycle Reconnection Failure handling You gain responsiveness, but complexity increases. Why Abstractions Like Socket.IO Matter WebSockets provide the foundation, but they are intentionally minimal. Production systems need more than just a raw connection. Socket.IO adds: Automatic reconnection Fallback to long polling Event-based APIs Broadcasting and grouping Logical channels via rooms This shifts your backend from handling endpoints to handling events. Thinking in Events Instead of Endpoints In REST, you design routes and responses. In event-driven systems, you design interactions. Instead of: POST /messages GET /messages You work with: socket.emit('chat.message', message) socket.on('chat.message', (msg) => { // handle message }) The server becomes an event hub, and clients react to changes as they occur. This model aligns better with systems where state evolves continuously. Broadcasting and Scope A core capability of event-driven systems is broadcasting: io.emit('chat.message', payload) This allows one event to reach many clients instantly. But sending everything to everyone rarely works in practice. You need scope. Rooms provide that control: socket.join(room) io.to(room).emit('chat.message', payload) This lets you: Segment users Isolate conversations Reduce unnecessary updates Rooms are not just a feature. They are how you model context in real-time systems. When You Still Need Responses Event-driven systems are asynchronous by default, but not everything fits that model. Sometimes you need a response, such as fetching user data or validating an action. Socket.IO supports this through acknowledgments: const userInfo = await socket.emitWithAck('user.info', socket.id) This creates a hybrid model where event-driven flows and request-response patterns coexist. In practice, most systems rely on both. Authentication Becomes a Connection Concern In REST, authentication happens per request. With persistent connections, it happens during the handshake: io.use((socket, next) => { // verify token }) Once established, the connection is trusted. This introduces new questions: What happens when tokens expire? How do you revoke access in real time? How do you store credentials securely? Even token storage becomes more critical, as insecure storage mechanisms can expose credentials. Authentication is no longer a repeated check. It becomes part of connection management. The Tradeoff: State and Scaling The biggest difference between REST and event-driven systems is state. REST systems are stateless, which makes scaling straightforward. Any server can handle any request. Event-driven systems maintain active connections. That means: Servers track clients Messages must reach specific connections Load balancing becomes more complex At scale, this often requires additional infrastructure like shared message layers or coordinated routing. This is where the simplicity of REST becomes hard to replace. Choosing the Right Model Event-driven systems are not a replacement for REST. They solve a different class of problems. They work best when: Data changes frequently Users expect immediate updates Interaction is continuous They add unnecessary complexity when: Data is mostly static Caching is effective Real-time behavior is not critical A blog platform benefits from REST. A collaborative editor does not. Instead of focusing on tools, focus on interaction patterns, ask: Does the server need to push updates? Do users depend on real-time feedback? Is the system driven by events rather than requests? If yes, an event-driven approach is worth the tradeoff. Key Takeaways Real-time systems are less about adopting new tools and more about changing how you think about communication. Once the server is no longer passive, the entire shape of your backend begins to evolve. If you want to explore how these concepts come together in a full application, Modern Full-Stack React Projects by Daniel Bugl is a solid next step. This Week in the News 🤖 Cursor 3 introduces a unified workspace for coding with agents: Cursor 3 is a full redesign built around managing AI agents in one place. You can run multiple agents in parallel, move work between local and cloud, and go from generated changes to merged PRs without leaving the interface. The shift is not about replacing coding. It is about raising the level of abstraction. You spend less time juggling tools and more time coordinating how work gets done. ⚙️ Next.js is finally decoupling from Vercel: Next.js is making a clear move toward platform independence. The new Adapter API creates a shared contract that lets any provider support Next.js reliably, backed by a public test suite and collaboration across Cloudflare, Netlify, AWS, and more. The direction is hard to miss. Next.js is no longer just a framework tied to one deployment model. It is becoming an infrastructure that can run anywhere without compromises. 🛠️ Sentry is turning debugging into agent workflows: Sentry’s new cookbook is a collection of ready-made “agent recipes” designed to help AI systems find, debug, and fix issues faster. Instead of treating observability as a dashboard, it becomes something agents can act on directly. The direction is clear. Debugging is no longer just about seeing errors. It’s about giving agents the context to resolve them automatically. ⚠️ Copilot briefly inserted an ad into a pull request: A developer noticed Copilot had edited an unexpected ‘ad’ into a PR, raising questions about how AI tools modify code. GitHub’s Martin Woodward later clarified what caused the behavior and confirmed the feature has now been disabled. It’s a small incident, but a reminder that AI-assisted coding still needs clear boundaries and visibility. 🧩 Run Codex inside Claude Code: The Codex plugin brings OpenAI’s Codex directly into Claude Code, combining two AI systems into a single workflow. It allows developers to delegate tasks like code reviews and bug fixes to background jobs while continuing to work. The interesting shift here is orchestration. Instead of one assistant, developers are starting to coordinate multiple agents for different tasks. ☁️ Cloudflare is building a WordPress alternative for the edge: Cloudflare’s EmDash is positioned as a modern successor to WordPress, designed to run on Cloudflare’s edge or any Node.js server. It focuses on performance, simplicity, and a more developer-friendly architecture. The bigger signal is where platforms are heading. Content systems are moving closer to the edge, with infrastructure and CMS becoming tightly integrated. Beyond the Headlines 🧠 What it’s like to actually use Claude Code: Claude Code is getting attention, but this deep dive looks past the hype into how it actually behaves in real workflows. From code navigation to multi-step reasoning, the experience feels less like autocomplete and more like collaborating with a system that understands intent. The gap between writing code and orchestrating it is starting to shrink. 📦 npm workspaces finally make sense: As projects grow beyond a single folder, things start to break. Shared code drifts, dependencies duplicate, and workflows get messy. This guide breaks down how npm workspaces actually solve that by managing multiple packages in one repo with shared dependencies and seamless imports. The value is not just convenience. It is about building systems that scale without turning your codebase into a maintenance problem. 🔥 Trigger.dev got 5x performance by replacing Node with Bun: : Trigger.dev swapped Node.js for Bun in a latency-critical service and saw throughput jump from ~2k to over 10k requests per second. The deeper insight is not just speed. It shows how runtime choices are starting to matter again, especially for systems handling high concurrency and long-lived connections. 🔍 Why ChatGPT waits before you can type: A deep reverse-engineering effort by Buchodi digs into why ChatGPT sometimes blocks input until a background check completes. The analysis suggests Cloudflare’s Turnstile is not just verifying the browser, but checking whether the app itself has properly initialized, including parts of the React state. The bigger shift is subtle but important. Bot detection is moving beyond “is this a real browser” to “is this a real user interacting with a real app.” Tool of the Week 📺 Build your own YouTube-style player with ArtPlayer (demo) ArtPlayer is a full-featured HTML5 video player that gives you fine-grained control over the playback experience. From custom controls and subtitles to plugins and styling, it is designed to be deeply customizable without adding unnecessary complexity. If you need more than a basic video tag but don’t want to build everything from scratch, this strikes a practical middle ground. That’s all for this week. Have any ideas you want to see in the next article? Hit Reply! Cheers! Editor-in-chief, Kinnari Chohan 👋 Advertise with us Interested in sponsoring this newsletter and reaching a highly engaged audience of tech professionals? Simply reply to this email, and our team will get in touch with the next steps. SUBSCRIBE FOR MORE AND SHARE IT WITH A FRIEND! *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}.image_block img+div{display:none}sub,sup{font-size:75%;line-height:0}#converted-body .list_block ol,#converted-body .list_block ul,.body [class~=x_list_block] ol,.body [class~=x_list_block] ul,u+.body .list_block ol,u+.body .list_block ul{padding-left:20px} @media (max-width: 100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width: 100%;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}} @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }
Read more
  • 0
  • 0
Kinnari Chohan
30 Mar 2026
12 min read
Save for later

WebDevPro #132: Async Code That Looks Fine but Fails in Production

Kinnari Chohan
30 Mar 2026
12 min read
Crafting the Web: Tips, Tools, and Trends for Developers Advertise with Us|Sign Up to the Newsletter @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } WebDevPro #132 Async Code That Looks Fine but Fails in Production Crafting the Web: Tips, Tools, and Trends for Developers Most Spring Boot projects stop at REST APIs Most Spring Boot developers stop at REST APIs. That’s enough to build demos but not to build systems that survive production. The real work sits beyond that. Service discovery, resilience, observability, config management are what separate working code from systems that hold up under pressure. You don’t pick this up from tutorials. It comes from building systems, making trade-offs, and seeing them run. Tomorrow, you get exactly that. Live. From scratch. With Simon Martinelli and Josh Long. Only 6 spots left. 🎟 Use code SPRING40 for 40% OFF 📢 Important: WebDevPro is Moving to Substack We’ll be moving WebDevPro to Substack soon.Once the transition is complete, all future issues will come from packtwebdevpro@substack.com. To make sure the newsletter continues reaching your inbox, please add this address to your contacts or whitelist it in your mail client. No other action is needed. You’ll keep receiving WebDevPro on the same weekly schedule. Substack will also give you more control over your subscription preferences if you decide to adjust them later. Welcome to this week’s issue of WebDevPro! Have you ever written async code that looked perfectly fine, only for it to behave unpredictably later? It’s one of those things that feels obvious while coding, but starts to fall apart once real conditions come into play. Asynchronous programming is fundamental to Node.js. The event loop keeps everything moving, delegating work and picking it back up when results return. On paper, it feels straightforward. You write code in sequence, so it should run that way too. But that’s where things get tricky. Execution is shaped by timing, scheduling, and resource contention. These are not visible in the code itself. What looks sequential can run concurrently. What feels predictable can change under load. Many real-world issues don’t come from syntax errors, but from how async behavior interacts with shared resources and execution order. This week’s deep dive breaks down where these assumptions fail and what it takes to make async systems behave reliably. Before we get into it, here’s this week at a glance: 🟦 TypeScript is preparing for a compiler rewrite 📦 pnpm is redesigning how dependencies are stored 🤖 Next.js is pushing toward AI-first app development ⚡ Claude dropped SSR for speed gains 🧩 Storybook is becoming AI-readable infrastructure When Async Execution Breaks Assumptions A common source of failure is incorrect assumptions about execution order. Consider a case where two operationsattempttowrite tothe same file. The code may appear sequential, but without explicit coordination, the operations execute independently. In such scenarios, the following pattern is oftenobserved: async functionraceCondition() { const filename = ... await unlink(filename) writeFile(filename, 'Written from first promise\n',{ flag: 'a' }) writeFile(filename, 'Written from second promise\n',{ flag: 'a' }) } At first glance, this code appears correct because two write operations are triggered one after the other.However, the absence of awaiting these operations means they run concurrently. Both operationsattemptto access the same file at the same time. This leads to inconsistent results, where the order of writes varies across executions. This behavior is known as a race condition. The issue is not syntax, but incorrect assumptions about how asynchronous execution works. Concurrency Does Not Guarantee Order In asynchronous systems, starting operations sequentially does not guarantee sequential execution. Each asynchronous call creates its own execution path, and these paths are resolved independently based on system timing. When multiple operations target the same resource, they compete for access. Without explicit coordination, the runtime does not guarantee which operationcompletesfirst. The outcome becomes dependent on timing rather than intent. This variability may not appear during development but becomes more prominent under production load, where multiple operationsexecutesimultaneously. Coordinating Access: Controlling Async Behavior To prevent race conditions, access to shared resources must be controlled. One approach is to introduce a mechanism that ensures only one operation interacts with the resource at a time. The following structuredemonstratesthis approach: classFileWriter{ #isWriting = false static instance = null constructor() { if (!FileWriter.instance) { FileWriter.instance= this } return FileWriter.instance } asyncwriteFile(filename, data) { if (this.#isWriting) { awaitsetTimeout(250) returnthis.writeFile(filename, data) } this.#isWriting= true const result = awaitwriteFile(filename, data, { flag: 'a' }) this.#isWriting= false return result } } This implementation introduces a lock mechanism. If a write operation is already in progress,subsequentoperationswaitbefore retrying. This ensures thatwritesoccur sequentially rather than concurrently. With coordination in place, execution becomes predictable. Without it, asynchronous code may behave inconsistently under real conditions. Callback Hell: When Async Structure Breaks Readability Asynchronous issues are not limited to executionorder. They also affect howcodeis structured. Deeply nested callbacks create code that is difficult to read andmaintain. An example of this structure is shown below: stepOne((err,resultOne) => { stepTwo(resultOne, (err,resultTwo) => { stepThree(resultTwo, (err,resultThree) => { console.log(resultThree); }); }); }); Although the code executes correctly, the nested structure makes it harder to follow the flow of data and control. Error handling is repeated at each level, increasing complexity. As the number of steps increases, the difficulty ofmaintainingand debugging the code also increases. The Event Loop and Hidden Blocking Node.js relies on non-blocking operations tomaintainperformance. The event loop processes tasks and delegatesworkwhen possible, allowing other operations to continue executing. However, not all operationsare non-blocking. Some APIs perform blocking I/O, which pauses execution of the entire program until completion. This prevents the event loop from handling other tasks. For example, cryptographic operations can block the main thread when executed synchronously. An asynchronous alternative allows work to be delegated externally: generateKeyPair("rsa", {modulusLength:1024 }, () => {}) The asynchronous version allows other operations to continue, while the synchronous version blocks execution. Under production load, blocking operations can significantly reduce system responsiveness. Async Does Not Always Mean Non-Blocking A common misconception is that wrapping a function in asynchronous code makes it non-blocking. This is not always true. If the underlying operation is blocking, it will still block execution. In such cases, performance improvements come from reducing how often the operation runs rather than changing how it is invoked. For example, caching results avoids repeated expensive computations: letcachedSignature= null; if (!cachedSignature) { cachedSignature=signData(...) } This approach improves throughput by reducing execution frequency rather than altering execution style. Async Behavior Under Load Many asynchronous issues only become visible under load. In controlled development environments, operations often execute in predictable sequences, and resource contention is minimal. As a result, code that appears stable during testing can behave differently when multiple operations are triggered at the same time. Race conditions become moreapparentwhen concurrent requests attempt to access ormodifythe same resource. What may appear as an occasional inconsistency during development can become a frequent issue when the same code is executed repeatedly under higher traffic. The lack of coordination between asynchronous operations leads to unpredictable results, making these issues harder to reproduce and debug. Blocking operations also have a more pronounced impactunderload. When a synchronous task runs on the main thread, it prevents the event loop from processing other incoming requests. In low-traffic scenarios, this delay may not be noticeable. Under production conditions, where many requests arrive simultaneously, blocking behavior can cause cascading delays, reducing overall responsiveness. Repeated execution of expensive operations further amplifies the problem. When the same computation is performed for every request without caching or reuse, system resources are consumed unnecessarily. This reduces throughput and increases response times, especially when multiple requests trigger the same operation concurrently. Anotherimportant factoris timing variability. Asynchronous execution depends on system scheduling, resource availability, and workload distribution. Under load, these factors fluctuate more significantly, increasing the likelihood of inconsistent outcomes. Code that relies on implicit ordering or timing assumptions becomes less reliable as concurrency increases. These issues highlight that asynchronous behavior is not only about writing non-blocking code, but also about understanding how that code behaves when multiple operations interact simultaneously. Without coordination, control over execution order, and careful management of shared resources, asynchronous systems can produce inconsistent results under real-world conditions. Final words Asynchronous programming enables Node.js to handle multiple operations efficiently, but it also introduces complexity in execution order, resource access, and performance. Many failures are caused by incorrect assumptions about how asynchronous code behaves. These issues oftenremainhidden during development and only surface under production conditions. Understanding how asynchronous code interacts with the event loop, shared resources, and system constraints is essential for building reliable applications. This Week in the News 🟦TypeScript 6.0 is really about TypeScript 7.0: TypeScript 6.0 quietly sets the stage for a bigger shift. This release moves the ecosystem closer to a Go-powered native compiler planned for TypeScript 7.0, with clear signals that performance and build speed are about to take a serious leap. It focuses less on surface-level features and more on groundwork that could reshape how large codebases compile and scale. 📦 pnpm 11 Beta just changed how dependencies are stored: pnpm 11 Beta offers a glimpse into where package management is heading. The shift to a SQLite-powered store improves lookup speed and reliability, while a broader config overhaul simplifies how projects define and share settings. Stricter build security is now enabled by default, reflecting a growing focus on supply chain safety. This release feels less like an incremental update and more like a rethink of how dependencies are stored and secured. 🤖Next.js just made AI apps feel native: Next.js 16.2 leans deeper into AI-native development. The update reshapes how developers connect model output to real interfaces, tightening the loop between prompting and product. The direction is becoming clear. Next.js is evolving into a foundation for AI-powered applications, not just a frontend framework. ⚡Why Claude dropped SSR for a Vite-powered setup: Anthropic’steam shared how they made Claude and its desktop apps meaningfully faster by moving away from SSR to a static setup using Vite andTanStackRouter. The shift highlights a growing pattern where speed and responsiveness win over traditional rendering models, especially for AI-heavy interfaces that demand instant feedback. 🧩 Storybook MCP brings AI into your UI workflow:Storybook’s latest update introduces an MCP server that lets coding agents understand your components at a deeper level. Instead of guessing structure, AI can now access metadata, generate stories, write tests, and even help fix bugs with more context. It signals a shift where component libraries are no longer just for developers, but also for the tools assisting them. Beyond the Headlines 🧠Fix your Next.js errors without exposing your code: Debugging production errors in Next.js often means staring at unreadable stack traces. This guide walks through setting up source maps withSentryso errors point back to your actual code, not minified chunks. The key detail is balance. You get full visibility in Sentry while keeping source maps out of the browser, which protects your code and improves debugging at the same time. 📊 Most open source projects are already accepting AI code:Phil Eaton surveyed 112 major source-available projects to understand how they handle AI-assisted contributions. The results show a clear trend. Most projects already allow or have accepted AI-generated code, with only a handful enforcing outright bans. The takeaway is not just policy, but reality. AI is already part of how modern open source evolves, and governance is still catching up ⚛️The React quirks you hate are actually fundamental: Some ofReact’smost disliked patterns, like deferred state updates and dependency arrays, are not accidental complexity. This piecearguesthey reflect deeper constraints of asynchronous UI systems. Once you look past the friction, they reveal problems every framework eventuallyhas tosolve, even the ones trying to replace React. 🧩Small programming tricks shape how your code scales: Tiny habits compound. This piece explores how small implementation choices, often dismissed as style or preference, quietly influence readability, maintainability, and long-term velocity. It is a reminder that good engineering is rarely about big rewrites. It is built through consistent, low-level decisions that add up over time. Tool of the Week ✍️ Draw it once, animate it instantly Stroke turns rough sketches into production-ready animations. You draw directly in the browser, and it generates Motion-based code you can drop into a React or Next.js component. Under the hood, it converts your strokes into SVG paths and animates them without manual setup, making it ideal for signatures, logos, or playful UI details. That’s all for this week. Have any ideas you want to see in the next article? Hit Reply! Cheers! Editor-in-chief, Kinnari Chohan 👋 Advertise with us Interested in sponsoring this newsletter and reaching a highly engaged audience of tech professionals? Simply reply to this email, and our team will get in touch with the next steps. SUBSCRIBE FOR MORE AND SHARE IT WITH A FRIEND! *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}.image_block img+div{display:none}sub,sup{font-size:75%;line-height:0}#converted-body .list_block ol,#converted-body .list_block ul,.body [class~=x_list_block] ol,.body [class~=x_list_block] ul,u+.body .list_block ol,u+.body .list_block ul{padding-left:20px} @media (max-width: 100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width: 100%;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}} @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }
Read more
  • 0
  • 0

Kinnari Chohan
23 Mar 2026
14 min read
Save for later

WebDevPro #131: Building Reliable Applications with Cursor: A Spec-Driven Workflow for Incremental Development

Kinnari Chohan
23 Mar 2026
14 min read
Crafting the Web: Tips, Tools, and Trends for DevelopersAdvertise with Us|Sign Up to the Newsletter @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }WebDevPro #131Building Reliable Applications with Cursor: A Spec-Driven Workflow for Incremental DevelopmentCrafting the Web: Tips, Tools, and Trends for DevelopersMost Spring Boot projects stop at REST APIsReal systems require service discovery, API gateways, centralized configuration, and built-in resilience. In this live, hands-on workshop, you’ll build a working microservices system end-to-end, define service boundaries, wire up discovery, configure a gateway, and handle failures properly.🎟 Register now and get 40% off with code SAVE40📢 Important: WebDevPro is Moving to SubstackWe’ll be moving WebDevPro to Substack soon.Once the transition is complete, all future issues will come from packtwebdevpro@substack.com.To make sure the newsletter continues reaching your inbox, please add this address to your contacts or whitelist it in your mail client. No other action is needed. You’ll keep receiving WebDevPro on the same weekly schedule.Substack will also give you more control over your subscription preferences if you decide to adjust them later.Welcome to this week’s issue of WebDevPro!If you’ve been experimenting with tools like Cursor, you’ve probably had that moment where the output looks promising but not quite right. You tweak the prompt, try again, and end up chasing the result instead of building something stable.What tends to work far better is not bigger prompts or more detailed instructions, but a shift toward a spec-driven, incremental workflow. One that mirrors how experienced developers already build systems, but adapts it to work effectively with AI-assisted coding environments.In this article, we want to walk through a more reliable way to approach this. By the end, you’ll have a practical workflow for structuring your projects, breaking down features, and working with AI coding tools in a way that actually scales beyond small experiments.Why direct prompting breaks down in real projectsIt is tempting to treat Cursor as a high-powered code generator. Describe the app, mention the tech stack, and expect a complete implementation.That approach holds up for small, disposable prototypes. It starts to fracture as soon as the project has multiple features, shared state, or evolving requirements.There are a few reasons for this.First, ambiguity compounds quickly. Even a well-written prompt leaves room for interpretation. The model fills in those gaps based on patterns, not your intent.Second, outputs are inherently variable. The same prompt can produce slightly different structures, naming conventions, or flows. This makes it difficult to build predictably.Third, you give up control of the system’s shape. Instead of designing the architecture, you are reacting to whatever the model generates.The result is a loop of correction rather than a process of construction.A more reliable approach: constrain, decompose, iterateA more effective pattern is to treat the tool as a collaborator that works best within clearly defined boundaries.The workflow is straightforward:Define what the system should doBreak it into discrete units of workImplement those units incrementallyRefine through feedbackThis is familiar territory. The difference is that your artifacts, like specifications and task lists, are now also guiding the model.Start with a specification that removes guessworkBefore opening Cursor, it helps to define how the application should behave in concrete terms. Consider a simple example: a math practice application for children.At a glance, it sounds trivial. In practice, it involves a number of decisions:What kinds of operations are supportedHow questions are presentedHow users interact with answersHow incorrect attempts are trackedWhat metrics are surfaced in a statistics viewWriting this down in a structured format forces clarity. It also creates a shared reference point for every subsequent step.One useful approach is to draft the specification in Markdown using a general-purpose language model. The key is not the initial output, but the interaction that follows. Asking the model to clarify uncertainties before generating the spec often leads to better results.Once you have a draft, it is worth reviewing line by line. Fill in gaps, remove contradictions, and add constraints that matter to you. Visual expectations, state transitions, and edge cases are all worth capturing early.The specification becomes less of a document and more of a contract. It defines intent in a way that both you and the tool can consistently refer back to.Translate the specification into a phased TODO listWith a clear specification in place, the next step is to decompose it into manageable units of work.Instead of thinking in terms of “build the app,” the focus shifts to “implement the next smallest meaningful piece.” A structured TODO list works well for this. Features are grouped into phases, and each phase contains tasks that can be completed independently.For example: Phase 1: Project setupInitialize React applicationConfigure styling systemCreate base routing and placeholder pages Phase 2: Core interactionGenerate math questionsRender question cardsImplement answer selection logicThis breakdown serves two purposes: It reduces cognitive load. You are no longer juggling the entire system in your head. It also gives Cursor a much clearer target. Instead of interpreting a broad request, it can focus on a well-defined task.Build incrementally and keep the scope tightWith the TODO list in place, you can begin implementing features in Cursor. The important shift here is how you frame your prompts. Rather than asking for a full implementation, you anchor the request to specific artifacts and a limited scope:Use the specification and TODO list to complete Phase 1. Mark completed tasks.This keeps the model grounded. It knows where to look for context and what success looks like. In the case of the math practice app, the first phase might result in:A scaffolded React projectBasic routingPlaceholder pages such as Home, Practice, Stats, and SettingsAt this stage, the application is simple, but functional. That is intentional. Each phase establishes a stable base for the next.Iterate deliberately instead of restartingOnce the initial output is in place, the next step is refinement.It is common for the first version of the UI or structure to feel underdeveloped. The instinct might be to rewrite the prompt or regenerate the feature from scratch. A more effective approach is to iterate on top of what already exists.For example:Improve the homepage layout. Add clearer navigation and better spacing.This kind of prompt is focused and contextual. It builds on existing work rather than replacing it.Over time, this creates a steady progression:Initial structureIncremental improvementsTargeted fixesEach step is small, but collectively they move the system toward a more polished state.Designing for variability instead of fighting itOne characteristic of working with language models is that outputs are not perfectly consistent. Even with the same inputs, you may see variations in structure, naming, or implementation details. Rather than trying to eliminate this variability, it helps to account for it in your workflow.Specifications anchor intent.TODO lists constrain scope.Incremental development limits the blast radius of changes.Together, these reduce the impact of variation. You retain control over the system, even as the underlying outputs shift slightly.A closer look at a single featureTo make this more concrete, consider the first phase of the math practice application. The goal is to establish the basic application shell. You begin with two inputs:A specification that defines pages and navigationA TODO list that outlines setup tasksThe prompt is simple and scoped:Complete Phase 1 using the provided specification and TODO list.Cursor initializes the project, sets up dependencies, and creates the necessary files. You run the application locally and see a basic interface with navigation between pages. At this point, the system is functional but minimal. You then refine:Improve the visual structure of the homepage and navigation.The tool updates components, adjusts layout, and introduces better spacing. You review the changes, keep what works, and continue. This pattern repeats for each feature. The system grows in layers, each one built on a stable foundation.Patterns that hold up in larger projectsA few patterns consistently make this approach effective. Clear specifications reduce the need for corrective prompts later. They shift effort from fixing outputs to shaping intent early.Breaking work into smaller tasks improves reliability. Each unit is easier for the model to interpret and implement correctly.Iteration keeps progress steady. Instead of waiting for a perfect result, you move forward through a series of improvements.Documentation becomes an active part of the process. Specifications and TODO lists evolve alongside the code, keeping everything aligned.Most importantly, you remain in control of the system’s design. The tool accelerates execution, but the direction still comes from you. But this workflow is not automatic. It depends on a few habits.If the specification is vague, the outputs will reflect that.If tasks are too large, the results become inconsistent again.If changes are accepted without review, small issues compound over time.The tool amplifies the process you bring to it. Structure leads to leverage. Lack of structure leads to friction.Closing thoughtsWorking with Cursor is less about mastering prompts and more about shaping a reliable development process. Once you move away from one-shot generation and toward a spec-driven, incremental approach, the experience changes. The tool becomes more predictable. The codebase becomes easier to reason about. Progress feels steadier.In many ways, this is not a new way of building software. It is a return to fundamentals, adapted for a new interface. The difference is that when the structure is right, the speed at which you can move begins to compound.If you want to go deeper into this way of working, especially with hands-on examples and a full project walkthrough, this approach is explored in detail in Vibe Coding with Cursor. It’s a practical guide to building real applications using these patterns, and a useful next step if you’re looking to move beyond experimentation into something more structured.This Week in the News🤖 OpenAI to acquire Astral: OpenAI is acquiring Astral and its widely used open-source Python developer tools, including uv, Ruff, and ty. The move strengthens OpenAI’s push into developer tooling, bringing core workflows like dependency management, linting, and type checking closer to its ecosystem. The goal is to accelerate Codex beyond code generation, positioning it to operate across the full development lifecycle, where it can work directly within the tools developers already use.🗓️ Why JavaScript still struggles with dates: The JavaScript new Date(someString) constructor is notoriously over-eager, stemming from legacy C++ parsers in engines like V8 and SpiderMonkey that aggressively "guess" date components to maintain backwards compatibility. This leads to absurd "hallucinations" where the engine prioritizes finding a year at any cost. Beyond these quirks, the parser's inconsistency is a frequent source of production bugs; for instance, new Date("2024-03-25") is treated as UTC, while adding a time element like T00:00 shifts it to Local Time, often causing "off-by-one-day" errors for users in different time zones. To avoid silent data corruption, such as new Date(null) defaulting to the 1970 Unix Epoch, developers should abandon these loose string constructors in favor of strict ISO 8601 formatting or the modern, predictable Temporal API.▲ Next.js 16.2 updates:Next.js 16.2 introduces a set of refinements focused on improving performance, stability, and the overall developer experience. The release builds on recent changes to the app router and server components, with updates that make common workflows more predictable and easier to manage. The direction remains consistent, with Vercel continuing to smooth out rough edges while aligning Next.js more closely with modern React patterns and production needs.⚡Vite 8 and Void, the fill-stack pivot: The Vite ecosystem is expanding beyond build tooling. Alongside the release of Vite 8, which brings updates focused on faster builds and a more streamlined developer experience, the VoidZero team has introduced Void, a Vite-native deployment platform built on Cloudflare Workers. Void aims to turn Vite apps into full-stack applications by bundling capabilities like databases, KV storage, object storage, and AI inference directly into the workflow. Together, these updates signal a shift toward a more integrated Vite stack that spans both development and deployment.⚖️ No AI code in Node.js core? A new petition sparks debate:A long-running discussion around AI-assisted development in OpenJS Foundation projects has taken a new turn. A Node.js contributor has started a petition urging the TSC to restrict AI-generated code from being merged into Node’s core, following concerns raised alongside a large 19K LOC PR. The debate touches on deeper questions around code quality, maintainability, and ownership as AI becomes a common part of development workflows.🧩 How TanStack approaches modern developer tooling:TanStack shares a set of ecosystem updates alongside a detailed talk that breaks down the thinking behind its tooling approach. The focus stays on building framework-agnostic, composable primitives that can scale across different application architectures without locking developers into a specific stack. The talk offers a deeper look into how these decisions play out in practice, from managing server state to structuring UI logic, and why flexibility and interoperability remain central to TanStack’s design philosophy.Beyond the Headlines📉 The hidden cost of “comprehension debt”:Addy Osmani introduces the idea of comprehension debt: the growing gap between how quickly code can be produced and how easily it can be understood. As tools (especially AI) accelerate code generation, the burden shifts to developers who need to read, debug, and maintain that code. It’s a useful lens for thinking about long-term code quality. Fast output is valuable, but only if teams can still reason about what they’ve built.🧠 React to Svelte migration with AI agents: Strawberry shares how it rewrote 130K lines of React code to Svelte in just two weeks using coding agents, cutting UI complexity and doubling performance in the process. The write-up goes deeper into why the team moved away from React’s rendering model toward Svelte’s compiler-first approach, and how AI-assisted workflows made a full rewrite practical at a scale that would usually take months. It’s a strong signal of how framework decisions, performance constraints, and AI-driven development are starting to converge in real-world systems.🎥 Rethinking how we build modern web apps: This talk explores how modern web development is evolving, touching on architecture, tooling, and the trade-offs developers face as applications grow in complexity. It’s a useful watch if you’re thinking about how today’s patterns scale in real-world systems.🚄 Understanding how JIT affects runtime performance: This post explores how Just-In-Time (JIT) compilation behaves in practice, looking at how execution performance evolves over time. It offers a deeper look at runtime optimizations and how they impact real-world performance characteristics.Tool of the Week🔍 Understand code changes with structural diffsTraditional diffs compare text line by line, which can make complex changes hard to follow. Difftastic takes a different approach by comparing the syntax tree of your code, helping you see what actually changed at a structural level.It supports multiple languages and highlights meaningful differences, making it especially useful when reviewing refactors or large code updates.That’s all for this week. Have any ideas you want to see in the next article? Hit Reply!Cheers!Editor-in-chief,Kinnari Chohan👋 Advertise with usInterested in sponsoring this newsletter and reaching a highly engaged audience of tech professionals? Simply reply to this email, and our team will get in touch with the next steps.SUBSCRIBE FOR MORE AND SHARE IT WITH A FRIEND!*{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}.image_block img+div{display:none}sub,sup{font-size:75%;line-height:0}#converted-body .list_block ol,#converted-body .list_block ul,.body [class~=x_list_block] ol,.body [class~=x_list_block] ul,u+.body .list_block ol,u+.body .list_block ul{padding-left:20px} @media (max-width: 100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width: 100%;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}} @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }
Read more
  • 0
  • 0

Kinnari Chohan
16 Mar 2026
13 min read
Save for later

WebDevPro #130: Rethinking State Management in Modern React

Kinnari Chohan
16 Mar 2026
13 min read
Crafting the Web: Tips, Tools, and Trends for Developers Advertise with Us|Sign Up to the Newsletter @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } WebDevPro #130 Rethinking State Management in Modern React Crafting the Web: Tips, Tools, and Trends for Developers Most Spring Boot projects stop at REST APIs Real systems require service discovery, API gateways, centralized configuration, and built-in resilience. In this live, hands-on workshop, you’ll build a working microservices system end-to-end, define service boundaries, wire up discovery, configure a gateway, and handle failures properly. 🎟 Register now and get 40% off with code SAVE40 📢 Important: WebDevPro is Moving to Substack We’ll be moving WebDevPro to Substack soon.Once the transition is complete, all future issues will come from packtwebdevpro@substack.com. To make sure the newsletter continues reaching your inbox, please add this address to your contacts or whitelist it in your mail client. No other action is needed. You’ll keep receiving WebDevPro on the same weekly schedule. Substack will also give you more control over your subscription preferences if you decide to adjust them later. Welcome to this week’s issue of WebDevPro! If you’ve worked with React for a while, you’ve probably run into the same recurring question: how should we manage state in this application? State management is one of those topics that almost every React team ends up debating sooner or later. Should everything live in context? Do we need a global store? Would something like Zustand make things simpler? Those conversations usually focus on tools. But in many cases the real problem shows up earlier than that. In practice, React applications rarely become difficult to maintain because the wrong library was chosen. They become difficult to maintain because different kinds of state get handled in the same way. A form input, an API response, a UI filter, and a shared user object often end up managed with identical patterns. That is where complexity starts to creep in. In this article, we will look at the main categories of state that appear in React applications and how each category is best managed. By the end, you will have a clearer way to decide where state belongs in a React app, whether that means local component state, shared state, server data tools, or even the URL itself. Not all state belongs in the same place State in React applications usually falls into a few clear categories. Local state lives inside a single component. A dropdown menu, a modal visibility toggle, or a small UI interaction typically belongs here. Hooks such as useState or useReducer work well because the logic remains isolated. Server state represents data fetched from APIs or databases. This includes user profiles, product listings, or analytics data. The key difference is that the source of truth lives outside the application. Form state includes field values, validation errors, and submission status. Forms have their own lifecycle and benefit from specialized tools such as React Hook Form or React’s form hooks. URL state stores small pieces of UI state in route or search parameters. Tabs, filters, pagination, and search queries often belong here. Shared state exists when multiple components need access to the same data. Authentication details, application settings, or user preferences are common examples. Understanding these categories clarifies an important point. React state is not a single problem. It is a set of related problems that require different solutions. The easiest state to manage is the state you never store One of the most common sources of complexity in React applications comes from duplicated state. Consider a list of items where the interface displays only active entries. A common pattern stores both the original list and a filtered version of it. const [items, setItems] = useState([...]) const [filteredItems, setFilteredItems] = useState([]) useEffect(() => { setFilteredItems(items.filter(item => item.active)) }, [items]) This introduces synchronization logic that React must maintain. A simpler approach derives the filtered list directly from the original state. const [items, setItems] = useState([...]) const filteredItems = items.filter(item => item.active) The application now maintains a single source of truth. React calculates the derived value when needed. Derived state reduces bugs and simplifies reasoning about data flow. Many React components become easier to maintain once unnecessary state variables disappear. Shared state is where React applications become complicated Local state remains predictable because it stays within a component. Shared state changes that dynamic because multiple components read and update the same values. Developers often encounter this situation when user data, permissions, or global settings must appear across different sections of the interface. The simplest approach begins with prop drilling. A parent component holds the state and passes it through props to child components. This technique often receives criticism, yet it works well for a small number of adjacent components. It also keeps data flow explicit and easy to trace. Problems appear when the component tree becomes deeper. Intermediate components may receive props they do not use, simply to pass them further down the hierarchy. At this stage, many teams introduce React context. Context allows components to access shared values without passing them through each level of the tree. A provider component stores the shared state and exposes it to any descendant component. This pattern simplifies access but introduces a different trade-off. When context values change, every consumer beneath the provider may re-render. For small applications this rarely matters. In larger interfaces the rendering behavior becomes harder to control. Libraries such as Zustand approach shared state from a different angle. Zustand creates a centralized store and allows components to subscribe only to the pieces of state they require. const userName = useUserStore(state => state.userName) Components update only when the subscribed value changes. This selective subscription reduces unnecessary renders and keeps the shared state logic compact. Context and Zustand both solve shared state challenges, but they operate at different scales. Context works well for moderate application state. Zustand becomes attractive once shared state spreads across larger parts of the interface. Server state follows a different lifecycle Data fetched from external services introduces a different category of state entirely. Server data involves caching, background refetching, loading indicators, and stale data management. Handling these concerns with useState and useEffect often leads to repetitive code and fragile synchronization. Libraries such as TanStack Query address this layer directly. They treat server data as cached resources rather than ordinary component state. Components request data using a shared query key. const { data } = useQuery({ queryKey: ['user', userId], queryFn: fetchUser }) TanStack Query stores the response in a client cache. Other components requesting the same query receive the cached result rather than triggering another network request. This model separates server data concerns from UI logic. React components remain focused on rendering rather than managing asynchronous data lifecycles. Some state belongs in the URL Another overlooked location for state is the browser address bar. Interfaces often contain UI state that describes how a page is being viewed. Active tabs, filter selections, search queries, and pagination indexes all fall into this category. Storing this information in URL search parameters creates several benefits. The state becomes shareable through links. Refreshing the page preserves the interface configuration. Browser navigation also restores previous UI states naturally. Framework hooks such as useSearchParams and routing utilities make this pattern straightforward. const params = useSearchParams() const activeTab = params.get('tab') Many teams duplicate this information inside React state and attempt to synchronize it with the URL. Removing that duplication simplifies the architecture. The address bar can act as a reliable source of truth for small pieces of UI state. Choosing the right home for state Modern React offers many tools for managing state, but the real skill lies in recognizing where each type of state belongs. Local UI interactions usually remain simplest when handled with component state. Values that can be computed from existing data should be derived instead of stored. Data fetched from APIs benefits from tools designed for server state, such as TanStack Query, which handle caching and refetching automatically. UI state that affects navigation, such as active tabs or filters, often works best when stored in URL parameters. Shared client state can start with straightforward prop passing and evolve into solutions such as context or Zustand when multiple components need coordinated access. The key takeaway is not which library to choose. It is learning to recognize the nature of the state problem in front of you. Once you can distinguish between local, shared, server, form, and URL state, the architecture decisions become much clearer. Instead of forcing every problem into the same pattern, each piece of state can live in the place where it is easiest to manage. That shift in thinking leads to React applications that are easier to reason about, easier to scale, and far less prone to the state management issues that often slow teams down. Build real skills in LLMs and agentic AI with this 20+ course Packt bundle, featuring titles like Learn Python Programming, 4E and The LLM Engineer’s Handbook. Learn how to design and deploy intelligent systems while supporting World Central Kitchen. 👉 Grab the LLM & Agentic AI Career Accelerator bundle This Week in the News 🧠 TypeScript 6.0 RC prepares the ecosystem for the Go-powered compiler:TypeScript 6.0 RC has landed and it marks an important transition for the language. This release functions largely as a stepping stone toward TypeScript 7.0, which will introduce a new native compiler written in Go. The RC itself contains only a few small changes compared to the beta, but the bigger shift lies in the groundwork being laid for the next generation of the toolchain. Required updates to tsconfig.json help projects align with upcoming architecture changes, giving teams time to prepare before the performance gains of the Go-powered compiler arrive later this year. ⚛️ SolidJS 2.0 beta introduces first-class async and a redesigned reactive core: SolidJS has entered the 2.0 beta phase after several years of experimental work on its next-generation reactive system. The release introduces major architectural changes including a rewritten signals implementation, deterministic batching, and first-class async support built directly into the framework’s primitives. New patterns such as action and optimistic state helpers aim to make server mutations and UI updates easier to manage, while updates to control flow and rendering bring several breaking changes developers will notice quickly. The beta sets the stage for broader ecosystem updates ahead of the stable 2.0 release. ☁️ Astro 6 introduces a Rust compiler and deeper Cloudflare alignment:Astro 6 arrives as the framework’s first major release since its acquisition by Cloudflare in January, and the platform direction is already becoming clearer. The update introduces an experimental Rust compiler that will eventually replace the original Go-based .astro compiler, promising faster builds and a modernized compilation pipeline. Development workflows also improve through Vite’s new Environment API, which allows developers to run the exact production runtime during development. Astro also introduces a new Fonts API that simplifies custom font handling across projects. 🌊 Cloudflare pushes for a simpler modern JavaScript Streams API:Cloudflare engineers are questioning the design of the Web Streams API, arguing that it reflects an earlier era of JavaScript before patterns like async iteration became common. The result is an abstraction that often feels overly complex, with specialized readers, locking mechanics, and extra boilerplate. Cloudflare proposes a simpler model built on modern JavaScript primitives that could improve both ergonomics and performance. Early benchmarks suggest potential speedups of up to 120× across runtimes such as Node.js, Deno, and Workers. In a related talk, James Snell explains how an async-iterator-driven approach could make streaming code easier to reason about for developers working with edge platforms and large data pipelines. 🎥 Watch the talk: https://www.youtube.com/watch?v=abbeIUOCzmw Beyond the Headlines 🤖 Literate programming may finally make sense in the AI agent era: Donald Knuth’s idea of literate programming asked developers to write code as a narrative meant for humans, with the compiler following along. For decades the concept remained mostly academic. This piece revisits the idea through the lens of AI-assisted development, where agents read, analyze, and generate code alongside developers. Clear explanations, structured reasoning, and intent-rich programs suddenly become far more valuable. The argument is simple but compelling: the rise of coding agents may finally make literate programming practical. 🐘 Just use Postgres and delay the infrastructure sprawl:Many modern stacks quickly grow into a collection of specialized tools: queues, search services, analytics systems, and caches. This article argues that much of that complexity arrives too early. PostgreSQL already includes powerful capabilities such as JSON support, full-text search, background jobs, and extensions that cover a surprising range of workloads. For many products, a single Postgres database can carry far more responsibility than teams assume. The takeaway is pragmatic: lean on Postgres longer and introduce new infrastructure only when the workload genuinely demands it. ⚡Building a real-time collaborative to-do app with Jazz and Vue: Real-time collaboration often brings complex backend logic, synchronization challenges, and WebSocket plumbing. This tutorial shows a simpler path by building a collaborative to-do app using Jazz and Vue. The stack manages shared state and synchronization automatically, allowing the interface to update instantly as multiple users interact with the same data. The walkthrough focuses on how collaborative state flows through the application and how the UI reflects updates in real time. It offers a practical introduction to modern tooling for building collaborative applications. ⚛️ Why React needed Fiber and what problem it actually solved:React Fiber is one of the biggest architectural changes in React’s history, yet its purpose is often misunderstood. This deep dive explains why the original reconciliation algorithm struggled with large or complex updates. Fiber introduced a scheduling model that allows React to pause, resume, and prioritize rendering work instead of processing everything in one blocking pass. That change laid the foundation for features such as concurrent rendering and smoother user experiences under heavy workloads. Tool of the Week 🛠️ VMPrint deterministic PDF generation without headless browsers Print-to-PDF pipelines often rely on headless Chrome, bringing along browser quirks and inconsistent rendering. VMPrint takes a different approach with a pure TypeScript typesetting engine that bypasses the DOM entirely and computes layout through typographic math. The result is deterministic output across browsers, Node.js, and edge runtimes like Cloudflare Workers. Given identical input, VMPrint guarantees identical layout down to the sub-point position of every glyph. For teams building document generation pipelines or publishing systems, it offers a precise and reproducible alternative to browser-based rendering. That’s all for this week. Have any ideas you want to see in the next article? Hit Reply! Cheers! Editor-in-chief, Kinnari Chohan SUBSCRIBE FOR MORE AND SHARE IT WITH A FRIEND! *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}.image_block img+div{display:none}sub,sup{font-size:75%;line-height:0}#converted-body .list_block ol,#converted-body .list_block ul,.body [class~=x_list_block] ol,.body [class~=x_list_block] ul,u+.body .list_block ol,u+.body .list_block ul{padding-left:20px} @media (max-width: 100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width: 100%;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}} @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }
Read more
  • 0
  • 0
Kinnari Chohan
09 Mar 2026
12 min read
Save for later

WebDevPro #129: Why Single Page Applications Changed How We Build Web Apps

Kinnari Chohan
09 Mar 2026
12 min read
Crafting the Web: Tips, Tools, and Trends for DevelopersAdvertise with Us|Sign Up to the Newsletter @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }WebDevPro #129Why Single Page Applications Changed How We Build Web AppsCrafting the Web: Tips, Tools, and Trends for DevelopersMost Spring Boot projects stop at REST APIsReal systems require service discovery, API gateways, centralized configuration, and built-in resilience. In this live, hands-on workshop, you’ll build a working microservices system end-to-end, define service boundaries, wire up discovery, configure a gateway, and handle failures properly.🎟 Register now and get 40% off with code SAVE40Welcome to this week’s issue of WebDevPro!Take a moment to think about the web apps you use every day. A dashboard updates without refreshing the page. A chat window receives messages in real time. A project board moves tasks across columns instantly.Modern web applications behave very differently from traditional websites. Interfaces update instantly, dashboards refresh without reloading the page, and navigation feels closer to using installed software than browsing documents. This experience is powered by an architectural shift called the Single Page Application, or SPA.By the end of this article, you will understand three things that shape modern frontend systems. First, what actually defines a Single Page Application beyond the buzzword. Second, why the SPA model improves responsiveness and interactivity in web applications. And third, how React enables this architecture through its rendering model and state-driven design.This is not a tutorial about setting up React or writing components. Instead, the goal is to understand the architectural thinking behind SPAs and how React fits into that model.The architectural shift behind modern web appsEarly websites were built around a page-based model. Each interaction triggered a request to the server, which generated a new HTML document and returned it to the browser. This worked well when the web primarily delivered content.Modern web applications demand a different experience. Consider tools such as analytics dashboards, project management systems, or collaborative editors. Users interact continuously with the interface. Data updates frequently, UI elements move around the screen, and navigation happens rapidly.In a traditional page-driven architecture, each of those interactions would require a server request and a full page refresh. Even small updates would rebuild the entire interface.Single Page Applications address this limitation by shifting the responsibility for rendering the interface into the browser itself.Instead of repeatedly requesting new pages, the browser loads the application once and then updates the interface dynamically as data changes. This change might sound simple, but it transforms the browser from a document viewer into a runtime environment for applications.What actually defines a Single Page ApplicationThe term “Single Page Application” can be misleading. It does not mean an application literally has only one page in the user experience. Instead, it describes how the application is delivered and rendered.A SPA typically loads a single HTML document that acts as a container for the application. Once the application initializes, JavaScript takes responsibility for rendering and updating the interface.From that point onward, most user interactions modify the current interface rather than loading new documents.Several architectural patterns define this approach.Client-side renderingIn a traditional website, the server generates HTML for each page request. In a SPA, the browser renders most of the interface.The server still plays an important role. It provides data through APIs and delivers the initial application bundle. However, the frontend application determines how that data appears on the screen.This allows the interface to update instantly when new data arrives.Persistent application runtimeBecause the application remains loaded in the browser, its logic persists across user interactions.Instead of rebuilding the interface from scratch, the system modifies the existing UI based on changes in application state.This persistence allows applications to maintain context and update small pieces of the interface without resetting the entire page.Virtual navigationUsers still see URLs change when navigating through a SPA, but the navigation logic is handled inside the application rather than by the server.The application interprets the URL and determines which components to display. From the user's perspective, the experience feels like normal navigation. Internally, the browser remains on the same underlying document.Together, these characteristics create the foundation of SPA architecture.Why the SPA model improves responsivenessThe key advantage of SPAs is not simply that they avoid page reloads. The deeper benefit is that they reduce unnecessary work.In a page-based system, the browser discards the entire interface whenever a new page loads. The server reconstructs the page, sends it back, and the browser renders it again.In a SPA, the application updates only the parts of the interface that change.Once the application code is loaded, the browser already contains everything needed to update the UI. When data changes, the system redraws only the relevant components rather than rebuilding the entire page.This approach becomes particularly valuable in highly interactive environments.Imagine a data dashboard where metrics update continuously while users filter results, adjust settings, and move between views. Rebuilding the entire interface for each interaction would create unnecessary delays.SPAs allow these updates to happen immediately because the rendering logic runs locally in the browser.However, this architecture introduces new complexity. The browser must now manage application state, UI rendering, and navigation. Without structured tools, this quickly becomes difficult to maintain.This challenge is where frameworks such as React play an important role.How React enables SPA architectureReact does not directly implement the SPA model. Instead, it provides a system that makes managing dynamic interfaces significantly easier.At its core, React helps developers answer a difficult question: how should the interface update when application data changes?Instead of manipulating the browser DOM directly, React introduces a model where the UI is described as a function of application state.This approach rests on two key ideas: the Virtual DOM and state-driven rendering.The Virtual DOM and efficient UI updatesEvery webpage contains a structure known as the Document Object Model, or DOM. The DOM represents the hierarchy of elements that make up the interface.Updating the DOM directly can become expensive when applications contain large numbers of elements or frequent updates.React addresses this problem through the Virtual DOM.The Virtual DOM is an in-memory representation of the interface maintained by React. When application state changes, React creates a new representation of the interface and compares it with the previous version.This comparison process determines exactly what has changed between the two states.React then updates only those elements in the real DOM that need to change. This process is often referred to as reconciliation.By reducing the number of direct DOM operations, React helps maintain performance even when applications grow large and complex.State as the driver of the interfaceThe second important idea in React is that the interface should be driven by state.React applications are built from components, each representing a piece of the user interface. Components maintain state that describes the data they display.When that state changes, React automatically updates the component's output and reconciles the differences in the DOM.A simplified example illustrates the concept.import { useState } from "react";function Counter() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)}> Count is {count} </button> );}In this example, the interface is directly derived from the value of count. When the state changes, React recalculates the component output and updates the interface accordingly.Developers do not manually manipulate DOM elements. Instead, they describe how the interface should look given a particular state.This declarative model simplifies reasoning about complex interfaces. It also aligns naturally with the needs of Single Page Applications, where the interface must update frequently in response to data changes.Why React became central to modern frontend developmentReact's design aligns well with the requirements of SPA architecture.Component-based design allows large interfaces to be broken into smaller, manageable pieces. Each component encapsulates its logic, state, and rendering behavior.The Virtual DOM helps maintain performance by minimizing unnecessary DOM updates. Meanwhile, React's state-driven model ensures that the interface stays synchronized with application data.Together, these ideas provide a structured approach to managing dynamic user interfaces.This combination of performance and developer ergonomics helped React become one of the most widely used frameworks for building modern web applications.Final wordsSingle Page Applications represent a shift in how the web is used. Instead of treating the browser as a document viewer, modern applications treat it as an execution environment for complex software.This architectural shift allows web applications to behave more like native software, delivering responsive interfaces and fluid user experiences.Understanding SPAs means understanding three core ideas. First, the browser now handles much of the interface rendering that used to happen on the server. Second, applications persist in the browser and update the interface dynamically rather than rebuilding pages. And third, frameworks such as React provide the abstractions needed to manage the resulting complexity.Once you view the web through this lens, many modern frontend patterns begin to make sense. Component-based architectures, state-driven rendering, and client-side routing all exist to support the same goal: building web applications that behave like real software rather than collections of pages.These concepts are explored further in Full-Stack React, TypeScript, and Node (2nd Edition), currently available in early access for PacktPub subscribers.Preorder now!This Week in the News📊 State of React Native results are out: The latest State of React Native survey is live, offering a snapshot of how developers are using the framework today. It covers adoption trends, satisfaction, tooling, and common pain points across the ecosystem. Worth a look if React Native is part of your stack or roadmap.🤖 Cloudflare rebuilds Next.js as AI reshapes the commercial open source model: Cloudflare surprised the developer community this week by claiming that a single developer rewrote Next.js in just one week using AI tools, spending about $1,100 in tokens. The experiment highlights how quickly large codebases can now be recreated with AI-assisted development, raising new questions about the speed and economics of building complex frameworks.📈 Next.js 16 upgrade triggers unexpected request spikes: Some developers upgrading from Next.js 15 to 16 report higher server request volume and increased response latency. The change appears to put more strain on backend infrastructure, which could mean higher compute usage and increased hosting costs in production. A useful read if you’ve ever been surprised by a framework behaving differently outside local dev.🔐 When a GitHub Issue Becomes a Supply Chain Attack: A security write-up explains a vulnerability chain called Clinejection that exploited an AI GitHub issue bot. The attack combined prompt injection with CI cache poisoning to publish a malicious package. It’s a good reminder that AI agents in developer workflows introduce a new kind of supply chain risk.💬 Vercel introduces the AI Chat SDK: Vercel has released a new Chat SDK aimed at simplifying how developers build conversational AI interfaces. The SDK provides primitives for streaming responses, handling message state, and integrating with multiple AI providers. If you’re building AI features into web apps, it removes a lot of the plumbing typically required for chat-style interactions.Beyond the Headlines🔄 Migrating from TypeScript 5.x to 6.0: This GitHub gist explores some extreme TypeScript type patterns that push the compiler surprisingly far. It’s a fascinating look at how conditional types, inference, and recursion turn TypeScript into something close to a compile-time programming language.🤖 AI Is Writing More Software, but Who Checks the Output?: Lean creator Leo de Moura explores what happens if AI systems eventually generate the majority of production code. The essay looks beyond productivity gains and asks deeper questions about verification, correctness, and how we maintain trust in software when humans are no longer writing most of it. A thoughtful perspective on where AI-assisted development could lead.🎞 Web Performance with Image Sprites: Sprite animations are an old web technique that still works surprisingly well today. Josh Comeau walks through how sprite sheets function, how to implement them with modern CSS, and when they’re preferable to other animation approaches. A great refresher for anyone building playful UI interactions or performance-friendly animations.🧠 Patterns for building agentic systems: Simon Willison breaks down emerging patterns in “agentic engineering” workflows. The article looks at how developers are structuring AI agents that can reason, call tools, and coordinate tasks across systems. If you’re experimenting with agent-style architectures, this piece highlights the design patterns that are starting to emerge.Tool of the Week🧩Build Custom Rich Text Editors with YooptaIf you're building apps that need rich text editing, finding a flexible editor can be tricky. Many solutions are either too rigid or difficult to extend.Yoopta Editor is a modern, open source rich text editor framework built for React. It uses a block-based architecture, making it easier to customize editing experiences, add plugins, and control how content is structured.For developers building CMS tools, collaborative editors, or AI-assisted writing interfaces, Yoopta offers a flexible foundation without forcing a fixed editing model.That’s all for this week. Have any ideas you want to see in the next article? Hit Reply!Cheers!Editor-in-chief,Kinnari ChohanSUBSCRIBE FOR MORE AND SHARE IT WITH A FRIEND!*{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}.image_block img+div{display:none}sub,sup{font-size:75%;line-height:0}#converted-body .list_block ol,#converted-body .list_block ul,.body [class~=x_list_block] ol,.body [class~=x_list_block] ul,u+.body .list_block ol,u+.body .list_block ul{padding-left:20px} @media (max-width: 100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width: 100%;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}} @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }
Read more
  • 0
  • 0

Kinnari Chohan
02 Mar 2026
15 min read
Save for later

WebDevPro #128: TypeScript Under Pressure in Evolving Full Stack Systems

Kinnari Chohan
02 Mar 2026
15 min read
Crafting the Web: Tips, Tools, and Trends for Developers Advertise with Us|Sign Up to the Newsletter @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } WebDevPro #128 TypeScript Under Pressure in Evolving Full Stack Systems Crafting the Web: Tips, Tools, and Trends for Developers Most Spring Boot projects stop at REST APIs Real systems require service discovery, API gateways, centralized configuration, and built-in resilience. In this live, hands-on workshop, you’ll build a working microservices system end-to-end, define service boundaries, wire up discovery, configure a gateway, and handle failures properly. 🎟 Register now and get 40% off with code SAVE40 Welcome to this week’s issue of WebDevPro. This issue looks at a common transition in modern engineering: taking a fast proof of concept and turning it into something that can survive real change. Consider a small internal AI support chatbot. The architecture is simple. A React frontend collects messages and sends them to an Express POST /api/chat endpoint. The server returns a short reply. The goal here is speed, not durability. To move quickly, the frontend stores messages as any[]. The backend reads req.body.messages from untyped JSON and returns a response like: { "reply": "You said: hello" } There is no formal contract between frontend and backend. The agreement lives in shared understanding: reply is a string, and messages is an array of objects with role and content. When both sides are written close together, this feels stable. Then the requirements change. The reply must now include structured data: the assistant’s message, references, and follow-up questions. The backend evolves and begins returning an object under reply instead of a string. The frontend does not change. The build passes. The browser fails at runtime because React attempts to render an object as if it were a string. Nothing in the type system flagged the mismatch because nothing at the boundary defined what the response was supposed to be. This is where TypeScript either acts as a safety mechanism under change or becomes a thin layer of annotations. What follows is a practical look at how that same chatbot evolves into a production-safe system by making contracts explicit and boundaries deliberate. Phase One: Turning Assumptions into a Shared Contract The chatbot did not break because the change was complex. It broke because the change was invisible to the type system. On the frontend, state was typed as any[]. The API response was treated as whatever res.json() returned. On the backend, req.body was used without an explicit shape. From TypeScript’s perspective, there was nothing concrete to compare, so the change in response structure did not register as a problem. The fix was not adding defensive conditionals in the UI. It was introducing a shared domain contract and making both sides depend on it. Instead of allowing the frontend and backend to “just agree,” the system defines explicit types for: ChatMessage ChatRequest ChatReply ChatResponse These types live in a shared module that both the frontend and backend import. The frontend state becomes ChatMessage[]. The API call returns Promise<ChatResponse>. The backend handler constructs a value that must satisfy ChatResponse. At that point, the contract stops being an assumption and becomes code. If someone changes the shape of reply again, the compiler will force every dependent piece of code to reconcile with that change. The mismatch that previously appeared in the browser now appears during build. In practical terms, this shifts the feedback loop. Instead of discovering drift during manual testing or after deployment, the team discovers it the moment they try to compile. That difference seems small, but over time it fundamentally changes how safely a system can evolve. Phase Two: Making the Network Boundary Honest Introducing shared types stabilizes collaboration between frontend and backend. It does not solve everything. There is still one place where untyped data enters the system: the network boundary. On the backend, req.body arrives as raw JSON. On the frontend, res.json() returns data that TypeScript cannot inspect at runtime. Even with shared contracts, the system is still trusting external input. In the chatbot’s case, this matters more as the system grows. Deployments may not always be perfectly synchronized. A proxy or middleware layer might alter payloads. A partial rollout could temporarily mix versions of frontend and backend. Shared types alone cannot guard against that. The practical adjustment is simple but important: treat boundary data as unknown until it is validated. Instead of assuming that req.body matches ChatRequest, the backend checks that it actually does. Instead of assuming that res.json() returns a valid ChatResponse, the frontend verifies the shape before proceeding. These checks do not need to be elaborate. Lightweight type guards are enough to confirm that required properties exist and have the correct types. Once the data passes that validation step, the rest of the system can rely on strong typing with confidence. This introduces a clear separation of responsibilities: Outside the boundary, data is untrusted. Inside the boundary, data is guaranteed to match the domain contract. In day to day development, this prevents subtle corruption. Rather than allowing malformed data to move deeper into the UI or business logic, the system fails immediately at the edge. Errors become explicit and local instead of diffused and harder to trace. The chatbot remains small, but its architecture becomes more deliberate. The boundary is no longer a blind spot; it becomes a controlled entry point. Phase Three: Containing Vendor Volatility As the chatbot matures, another requirement arrives: support more than one LLM provider. This is a common inflection point. Early on, it is convenient to wire the backend directly to a single SDK. The response from the provider flows straight through the server and into the UI. It feels efficient. It also quietly couples your entire stack to a vendor’s response shape. If that vendor changes its format, or if you decide to introduce a second provider, the ripple effects can reach the frontend quickly. What began as a backend integration detail becomes a full-stack coordination problem. In the chatbot’s evolution, this risk is handled differently. Instead of exposing raw provider responses, the system defines a provider interface that returns a domain-level ChatReply. Each provider implementation adapts its own SDK response into that shared shape. The rest of the application does not know or care which provider generated the reply. It only understands the domain contract. This decision seems architectural, but it has very practical consequences. Switching providers or introducing a second one no longer forces a redesign of shared types. The volatility is contained. The surface area of change is smaller. TypeScript reinforces this separation. The compiler ensures that every provider implementation produces a valid ChatReply. The backend handler depends on the interface, not on vendor-specific JSON. In a growing system, this is what stability looks like. The parts that are likely to change are isolated behind clear contracts. The parts that need to remain steady, especially the frontend, are shielded from that churn. Phase Four: Making States Explicit as Features Expand Feature growth rarely stops at integration. The chatbot evolves again. Sometimes a response includes citations. Sometimes it does not. A quick solution would be to add optional fields. Over time, optional fields accumulate. The type becomes flexible, but also ambiguous. It becomes unclear which combinations are valid and which are accidental. Instead, the chatbot models these variations explicitly using a discriminated union. A response is either a plain answer or an answer with citations, and the kind field identifies which one it is. On the frontend, rendering logic switches on that kind. The exhaustive check ensures that every variant is handled. This design choice has a subtle but important effect. When a new response variant is introduced later, the compiler highlights every place that must adapt. Nothing slips through unnoticed. In everyday development, this reduces the risk of partial updates. The type system becomes a guide for refactoring, not just a static annotation layer. The chatbot still feels small. The difference is that its possible states are no longer implied. They are declared. Phase Five: Removing Quiet Type Erosion As the chatbot grows, new endpoints appear. Health checks. Admin actions. Maybe analytics or feedback capture. The API surface expands gradually. This is usually where small shortcuts start accumulating. A common one looks harmless: const data = (await res.json()) as ChatResponse; The cast makes the compiler quiet. It also shifts responsibility back to the developer. At the exact point where the system is most exposed to incorrect data, you are asserting that everything is fine. In a small codebase, this feels manageable. In a growing one, these assertions multiply. Over time, they erode the safety you thought TypeScript was providing. In the chatbot’s evolution, this problem is addressed structurally rather than procedurally. Instead of scattering casts, the system defines a mapping between route literals and their request and response types. The endpoint determines the shape. The generic client enforces it. Now the type of /api/chat is tied directly to ChatRequest and ChatResponse. You cannot accidentally call the wrong endpoint and pretend it returns something else. The compiler resolves the relationship based on the route itself. This removes a category of silent drift. It also makes the API surface self-documenting. When someone adds a new endpoint, they define its contract in one place, and the rest of the system aligns automatically. It is a small shift in structure, but it prevents gradual type erosion. What This Evolution Actually Achieved The chatbot still answers questions. The UI still renders messages. The architecture is not radically different from the initial proof of concept. What changed is where mistakes surface. At first, mismatches appeared in the browser. After shared contracts, they appeared during compilation. After boundary validation, they appeared at the edge of the system. After isolating providers, vendor changes stopped leaking across layers. After modeling variants explicitly, feature growth became safer. After tightening endpoint typing, unsafe assumptions stopped spreading quietly. None of these changes required advanced language features. They required clarity about boundaries and discipline about contracts. In a system that evolves under real-world pressure, assumptions are the most fragile dependency. TypeScript is most valuable when it turns those assumptions into enforceable structure. The chatbot did not become more sophisticated for its own sake. It became more predictable under change. For a deeper exploration, pre-order Clean Code with TypeScript. The book takes a detailed look at the evolving chatbot system and walks through the architectural decisions, trade-offs, and production considerations behind it. If you’re a PacktPub subscriber, you can access the Early Access version right away. This Week in the News ☁️ Cloudflare rebuilds Next.js on Vite: Cloudflare unveiled vinext, an experimental reimplementation of the Next.js API surface built on Vite, reportedly put together in a week using AI coding agents and the official Next.js test suite as a spec. Instead of adapting Next’s build output for non-Vercel platforms, Cloudflare rebuilt the framework layer itself to make deployment on its infrastructure more natural. vinext already supports routing, SSR, React Server Components, server actions, middleware, and both routing systems, passing about 94 percent of the Next 16 test suite. Cloudflare claims faster builds and smaller client bundles in early benchmarks, while calling the results directional. It is still early, but this is a bold signal in the ongoing platform versus framework conversation. 🅰️ Angular Skills for coding agents: Angular developers can now equip their coding agents with “Angular Skills,” a set of curated defaults and patterns designed to guide agents toward modern Angular best practices. The project packages conventions, structure, and tooling preferences so AI-assisted workflows generate code that feels aligned with today’s Angular ecosystem. Do agent skills materially improve output quality? That remains to be seen. At the very least, they formalize standards and make human intent more explicit, which may be half the battle in AI-assisted development. 🟢Node.js 24.14.0 LTS and 25.7.0 Current released: Node shipped both an LTS and a Current release this week, and this one is more than routine version churn. The LTS update tightens up async_hooks, improves fs.watch reliability, adds proxy configuration support, and begins exposing early ESM embedder API enhancements. If you’re running backend services, SSR stacks, or dev tooling that leans on Node internals, these changes are practical, not cosmetic. Meanwhile, 25.7.0 Current gives a preview of where the runtime is heading. Framework maintainers and teams with native modules should treat this as a signal to test early and avoid CI surprises later. 📈 OpenClaw briefly overtakes Python projects on GitHub: A retro game reimplementation project saw a spike in GitHub activity that temporarily outranked major Python repositories. While largely a trend metric, this serves as a reminder that GitHub stars and ranking spikes do not always reflect lasting ecosystem impact. For developers, this is a useful gut check. Popularity metrics can highlight energy, but they don’t always indicate production relevance or long-term impact. 🤖 Claude Code turns one as distillation dispute surfaces:Anthropic marked the first anniversary of Claude Code while raising concerns about competitors using Claude outputs to train rival models through large-scale distillation. The discussion moves beyond company rivalry and into deeper questions around model output ownership, competitive boundaries, and how AI tooling companies protect their work. For developers building AI-powered products, this matters. The ecosystem is still defining what is acceptable reuse, what is extraction, and where legal lines will be drawn. Tooling decisions today are increasingly shaped by these policy and governance shifts 🏛️ React moves to the Linux Foundation:React and React Native are now governed by a newly formed React Foundation under the Linux Foundation. This shifts stewardship from a single corporate sponsor to neutral, open governance. For teams betting their front end architecture on React, this reduces long-term platform risk. Governance stability may not feel urgent in daily development, but it quietly shapes the future of roadmaps, community trust, and ecosystem continuity. Beyond the Headlines 🦀 Ladybird adopts Rust for new components: The Ladybird browser project is moving new development to Rust, citing safety and maintainability. This isn’t just about language preference. It reflects the industry’s steady migration toward memory-safe systems programming in infrastructure-level code. The browser space has historically been C and C++. Rust’s continued expansion here signals that safety is becoming a baseline expectation, not a luxury. 🤖 The five stages of AI agents: A useful framework is emerging around how AI agents evolve: from basic task automation to more autonomous, goal-oriented systems. For developers experimenting with AI workflows, this gives structure to what can otherwise feel like hype. The takeaway is simple. Not every workflow needs autonomy. Understanding where your system sits on that spectrum prevents overengineering and keeps expectations grounded. 🧠 Mitchell Hashimoto on AI adoption: Mitchell Hashimoto, the co-founder of HashiCorp shared a candid look at integrating AI tools into his workflow. Not evangelism. Not backlash. Just a practical breakdown of where AI speeds things up and where it still falls short. That grounded middle space is where most developers are operating right now. AI is useful. It is not magic. And thoughtful integration beats blanket adoption. Refactoring production systems is risky, especially when API changes and hidden coupling are involved. In this Deep Engineering session, learn how to safely evolve real-world codebases using ast-grep and Claude Code, with practical guardrails you can apply immediately. Use code WDP40 to get 40% off your seat. Tool of the Week 🛡️ Zod Makes Runtime Validation Feel Native to TypeScript If you’ve ever trusted req.body a little too much or written as SomeType just to quiet the compiler, you already know the pain: TypeScript disappears at runtime. The type system keeps you safe inside your codebase, but JSON at the network boundary remains unchecked. Zod solves that gap cleanly. It’s a TypeScript-first schema validation library that lets you define runtime validation and static types in one place. You describe the shape once, validate incoming data against it, and automatically infer the TypeScript type. No duplication. No drift between interface and validator. The real value shows up under change. When your API evolves or a provider shifts its response format, Zod forces the mismatch to surface immediately at the boundary instead of leaking deeper into your system. It turns assumptions into enforced contracts. Good architecture is often about making edges explicit. Zod gives those edges structure. That’s all for this week. Have any ideas you want to see in the next article? Hit Reply! Cheers! Editor-in-chief, Kinnari Chohan SUBSCRIBE FOR MORE AND SHARE IT WITH A FRIEND! *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}.image_block img+div{display:none}sub,sup{font-size:75%;line-height:0}#converted-body .list_block ol,#converted-body .list_block ul,.body [class~=x_list_block] ol,.body [class~=x_list_block] ul,u+.body .list_block ol,u+.body .list_block ul{padding-left:20px} @media (max-width: 100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width: 100%;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}} @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} } @media only screen and (max-width: 100%;} #pad-desktop {display: none !important;} }
Read more
  • 0
  • 0
Modal Close icon
Modal Close icon