<?xml version="1.0" encoding="UTF-8"?><rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>logrocket-Dev</title><link>https://blog.logrocket.com/</link><atom:link href="http://rss.144-124-237-35.sslip.io/logrocket/dev" rel="self" type="application/rss+xml"></atom:link><description>logrocket-Dev - Powered by AtomRSS</description><generator>AtomRSS</generator><webMaster>contact@atomgroup.dev (AtomRSS)</webMaster><language>en</language><lastBuildDate>Mon, 19 Oct 2026 14:58:50 GMT</lastBuildDate><ttl>5</ttl><item><title>Astryx vs. shadcn/ui: Testing Meta’s AI-friendly React design system with Claude Code</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/astryx-vs-shadcn-meta-ai-react-claude/#why-agents-trip-over-design-systems&quot;&gt;Why agents trip over design systems&lt;/a&gt;
        &lt;ul data-toc-name=&quot;why-agents-trip-over-design-systems&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/astryx-vs-shadcn-meta-ai-react-claude/#what-you-actually-get-with-astryx&quot;&gt;What you actually get with Astryx&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-you-actually-get-with-astryx&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/astryx-vs-shadcn-meta-ai-react-claude/#test-one-dashboard&quot;&gt;Test one: the dashboard&lt;/a&gt;
        &lt;ul data-toc-name=&quot;test-one-dashboard&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/astryx-vs-shadcn-meta-ai-react-claude/#test-two-swapping-theme&quot;&gt;Test two: swapping the theme&lt;/a&gt;
        &lt;ul data-toc-name=&quot;test-two-swapping-theme&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/astryx-vs-shadcn-meta-ai-react-claude/#test-three-settings-modal&quot;&gt;Test three: the settings modal&lt;/a&gt;
        &lt;ul data-toc-name=&quot;test-three-settings-modal&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/astryx-vs-shadcn-meta-ai-react-claude/#the-numbers&quot;&gt;The numbers&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-numbers&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/astryx-vs-shadcn-meta-ai-react-claude/#the-catches&quot;&gt;The catches&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-catches&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/astryx-vs-shadcn-meta-ai-react-claude/#so-is-it-worth-it&quot;&gt;So, is it worth it?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;so-is-it-worth-it&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/astryx-vs-shadcn-meta-ai-react-claude/#key-takeaways&quot;&gt;Key takeaways&lt;/a&gt;
        &lt;ul data-toc-name=&quot;key-takeaways&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;If you’ve pointed an AI agent at a component library lately, you’ve probably watched this happen. You ask Claude Code to build a dashboard; it picks a component that was renamed two versions ago, passes a prop the library dropped, and hands you a file that looks right but won’t compile.&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;1536&quot; height=&quot;1024&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/featured-image-2.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;Astryx vs. shadcn/ui: Testing Meta&#39;s AI-friendly React design system with Claude Code&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/featured-image-2.png 1536w, https://blog.logrocket.com/wp-content/uploads/2026/08/featured-image-2-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/08/featured-image-2-1024x683.png 1024w, https://blog.logrocket.com/wp-content/uploads/2026/08/featured-image-2-768x512.png 768w&quot; sizes=&quot;(max-width: 1536px) 100vw, 1536px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;The agent couldn’t see the library’s actual API, so it filled in the gaps with whatever it remembered from training. What it remembered was out of date.&lt;/p&gt;
&lt;p&gt;This isn’t a new problem, and the ecosystem has been chipping away at it. shadcn/ui shipped an MCP server. Chakra UI and &lt;a href=&quot;https://blog.logrocket.com/storybook-mcp-component-libraries/&quot;&gt;Storybook&lt;/a&gt; did too. The idea behind all of them is the same: give the agent a machine-readable description of the components so it can query the real API instead of guessing.&lt;/p&gt;
&lt;p&gt;Astryx is Meta’s take on that idea. Meta open-sourced it on 28 June 2026 under the MIT license after eight years of internal use. It’s built on React and StyleX, and it ships with a CLI and an MCP server that give the agent a manifest of every component, prop, and default.&lt;/p&gt;
&lt;p&gt;I wanted to know whether that actually changes how an agent behaves. So I connected Claude Code to Astryx, built three real UIs with it, then built the same three against shadcn/ui without a manifest. Here’s what held up and what didn’t.&lt;/p&gt;
&lt;h2 id=&quot;why-agents-trip-over-design-systems&quot;&gt;Why agents trip over design systems&lt;/h2&gt;
&lt;p&gt;I started with shadcn/ui, partly because it’s what many React developers reach for and partly because it recently shipped a change that illustrates the problem. shadcn/ui v4 moved off Radix and onto base-ui. That one change invalidates a lot of what an agent may have learned from older shadcn code, with no obvious way for the agent to know the underlying model changed.&lt;/p&gt;
&lt;p&gt;So it wrote what it remembered. It put &lt;code&gt;asChild&lt;/code&gt; on &lt;code&gt;DropdownMenuTrigger&lt;/code&gt;, the classic Radix composition pattern. In v4, that prop is gone, and the build failed.&lt;/p&gt;
&lt;p&gt;To sort it out, I opened &lt;code&gt;input.tsx&lt;/code&gt; and &lt;code&gt;button.tsx&lt;/code&gt; to check the new base-ui patterns, then leaned on what I already knew about shadcn for the rest. It worked, but something important was missing: there was no manifest to ask and no CLI to query. The agent’s only sources were its own memory and whatever I could infer from the source.&lt;/p&gt;
&lt;p&gt;There was a second, smaller issue in my first run, a TypeScript complaint on an &lt;code&gt;onChange&lt;/code&gt; handler, but I couldn’t reliably reproduce it afterward. It seems to depend on how strict your &lt;code&gt;tsconfig&lt;/code&gt; is, so I’m not going to lean on it.&lt;/p&gt;
&lt;p&gt;Setup friction also piled up before a single component rendered. &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/&quot;&gt;Tailwind v4&lt;/a&gt; needed a manual &lt;code&gt;vite.config.ts&lt;/code&gt; change. &lt;code&gt;shadcn init&lt;/code&gt; dropped files into a literal &lt;code&gt;@/&lt;/code&gt; folder that I had to move into &lt;code&gt;src/&lt;/code&gt; by hand. TypeScript 6.0 wanted an &lt;code&gt;ignoreDeprecations&lt;/code&gt; flag for a deprecated &lt;code&gt;baseUrl&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;None of that is Astryx’s doing, but it is part of the practical cost of the comparison.&lt;/p&gt;
&lt;p&gt;shadcn isn’t standing still. It now ships its own MCP server, and Chakra UI and Storybook have added similar tooling.&lt;/p&gt;
&lt;p&gt;All of these systems, Astryx included, started as tools for humans. Astryx wasn’t designed for agents eight years ago. What’s different is that its CLI, manifest, MCP server, and &lt;code&gt;AGENTS.md&lt;/code&gt; file arrived together as part of the public release rather than being added later.&lt;/p&gt;

&lt;p&gt;Whether that packaging changes how an agent behaves is the question. So let’s test it.&lt;/p&gt;
&lt;h2 id=&quot;what-you-actually-get-with-astryx&quot;&gt;What you actually get with Astryx&lt;/h2&gt;
&lt;p&gt;Astryx is a component library plus the tooling around it. You import precompiled CSS and typed React components. There’s no build plugin or separate styling library you need to adopt.&lt;/p&gt;
&lt;p&gt;The component count depends on where you look. The GitHub repo lists 90-plus components, Meta’s docs say 150-plus, and when I ran &lt;code&gt;npx astryx init&lt;/code&gt;, the generated &lt;code&gt;AGENTS.md&lt;/code&gt; listed 153. Meta says Astryx has powered more than 13,000 of its internal apps over eight years, though that’s a company claim rather than something I independently verified.&lt;/p&gt;
&lt;p&gt;For an agent, three pieces matter. The CLI, available as &lt;code&gt;astryx&lt;/code&gt; or &lt;code&gt;xds&lt;/code&gt;, prints a component’s full documentation on demand. The &lt;a href=&quot;https://blog.logrocket.com/introducing-the-logrocket-mcp/&quot;&gt;MCP server&lt;/a&gt; exposes the same surface over the protocol used by tools such as Claude Code, Cursor, and Copilot. Then there’s the manifest, which provides a machine-readable description of every component and prop.&lt;/p&gt;
&lt;p&gt;The agent doesn’t have to scrape a docs site. It can query the system directly.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/static/img/pixel.gif&quot; alt=&quot;Pixel placeholder image&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Building on StyleX doesn’t require you to use StyleX yourself. Astryx authors its styles with StyleX, but that implementation detail is largely invisible to consumers. You can override components through &lt;code&gt;className&lt;/code&gt; using Tailwind, CSS Modules, or plain CSS. You’ll need React 19 or later.&lt;/p&gt;
&lt;h2 id=&quot;test-one-dashboard&quot;&gt;Test one: the dashboard&lt;/h2&gt;
&lt;p&gt;For the first test, I asked for an analytics dashboard with sidebar navigation, a top bar and user menu, four stat cards, a chart placeholder, and a data table.&lt;/p&gt;
&lt;p&gt;The agent didn’t guess a single component name. It ran &lt;code&gt;npx astryx component --list&lt;/code&gt; to inspect the inventory, then queried each component it planned to use: &lt;code&gt;AppShell&lt;/code&gt;, &lt;code&gt;SideNav&lt;/code&gt;, &lt;code&gt;Card&lt;/code&gt;, &lt;code&gt;DropdownMenu&lt;/code&gt;, &lt;code&gt;Table&lt;/code&gt;, &lt;code&gt;Grid&lt;/code&gt;, and about a dozen more.&lt;/p&gt;
&lt;p&gt;Eighteen lookups in total. Each returned real props and, sometimes, guidance I wouldn’t have thought to ask for. The &lt;code&gt;Card&lt;/code&gt; docs explicitly warn that cards aren’t the default layout tool. The &lt;code&gt;AppShell&lt;/code&gt; docs pointed me to &lt;code&gt;height=&quot;fill&quot;&lt;/code&gt; for dashboards and &lt;code&gt;contentPadding={0}&lt;/code&gt; for tables.&lt;/p&gt;

&lt;p&gt;Then the agent wrote the file, and it still got four things wrong.&lt;/p&gt;
&lt;p&gt;It guessed &lt;code&gt;variant=&quot;chromeless&quot;&lt;/code&gt; on a button when the real value was &lt;code&gt;ghost&lt;/code&gt;. It put &lt;code&gt;startIcon&lt;/code&gt; on the dropdown trigger, assuming the standard button prop applied, even though the trigger accepts a narrower set and expects &lt;code&gt;icon&lt;/code&gt;. It forgot that &lt;code&gt;SideNavSection&lt;/code&gt; requires a &lt;code&gt;title&lt;/code&gt;. Finally, it imported &lt;code&gt;TableRow&lt;/code&gt; and &lt;code&gt;TableCell&lt;/code&gt; out of habit before realizing Astryx’s &lt;code&gt;Table&lt;/code&gt; is data-driven and doesn’t use them.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;// Guessed a variant name. It doesn&#39;t exist.
variant: &#39;chromeless&#39;,

// Corrected to a real one.
variant: &#39;ghost&#39;,

// Assumed the standard button prop applied to the dropdown trigger.
startIcon: &amp;lt;Avatar name=&quot;Marvel Ken&quot; size=&quot;sm&quot; /&amp;gt;,

// The trigger accepts a narrower set. The real prop is icon.
icon: &amp;lt;Avatar name=&quot;Marvel Ken&quot; size=&quot;sm&quot; /&amp;gt;,
&lt;/pre&gt;
&lt;p&gt;Every one of those errors was a wrong prop name, and TypeScript caught every one at build time before anything rendered. The agent never invented a component that didn’t exist because it had already pulled the real component list.&lt;/p&gt;
&lt;p&gt;After those four fixes, the build passed with no runtime errors. The manifest didn’t make the agent perfect. It made its mistakes cheap.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Table&lt;/code&gt; component is where an agent working from memory would be most likely to faceplant. Astryx doesn’t build tables from &lt;code&gt;TableRow&lt;/code&gt; and &lt;code&gt;TableCell&lt;/code&gt; children. It accepts &lt;code&gt;data&lt;/code&gt; and a column definition, with width helpers such as &lt;code&gt;proportional()&lt;/code&gt; and &lt;code&gt;pixel()&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;// Astryx: data-driven, no row or cell children.
&amp;lt;Table&amp;lt;TransactionRow&amp;gt;
  data={transactions}
  columns={[
    {key: &#39;date&#39;, header: &#39;Date&#39;, width: proportional(1)},
    {key: &#39;customer&#39;, header: &#39;Customer&#39;, width: proportional(1)},
    {key: &#39;amount&#39;, header: &#39;Amount&#39;, width: pixel(100)},
    {
      key: &#39;status&#39;,
      header: &#39;Status&#39;,
      width: pixel(100),
      renderCell: (row) =&amp;gt; (
        &amp;lt;Badge
          variant={statusVariant(row.status as string)}
          label={row.status as string}
        /&amp;gt;
      ),
    },
  ]}
  density=&quot;compact&quot;
  hasHover
/&amp;gt;
&lt;/pre&gt;
&lt;p&gt;For comparison, here’s the same table the agent built against shadcn, using the children-based structure it initially started to reach for with Astryx before the manifest corrected it.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;// shadcn: children-based composition.
&amp;lt;Table&amp;gt;
  &amp;lt;TableHeader&amp;gt;
    &amp;lt;TableRow&amp;gt;
      &amp;lt;TableHead&amp;gt;Date&amp;lt;/TableHead&amp;gt;
      &amp;lt;TableHead&amp;gt;Customer&amp;lt;/TableHead&amp;gt;
      &amp;lt;TableHead&amp;gt;Amount&amp;lt;/TableHead&amp;gt;
      &amp;lt;TableHead&amp;gt;Status&amp;lt;/TableHead&amp;gt;
    &amp;lt;/TableRow&amp;gt;
  &amp;lt;/TableHeader&amp;gt;
  &amp;lt;TableBody&amp;gt;
    {transactions.map((tx, i) =&amp;gt; (
      &amp;lt;TableRow key={i}&amp;gt;
        &amp;lt;TableCell className=&quot;text-xs&quot;&amp;gt;{tx.date}&amp;lt;/TableCell&amp;gt;
        &amp;lt;TableCell className=&quot;text-xs&quot;&amp;gt;{tx.customer}&amp;lt;/TableCell&amp;gt;
        &amp;lt;TableCell className=&quot;text-xs&quot;&amp;gt;{tx.amount}&amp;lt;/TableCell&amp;gt;
        &amp;lt;TableCell&amp;gt;
          &amp;lt;Badge variant={statusVariant(tx.status)}&amp;gt;{tx.status}&amp;lt;/Badge&amp;gt;
        &amp;lt;/TableCell&amp;gt;
      &amp;lt;/TableRow&amp;gt;
    ))}
  &amp;lt;/TableBody&amp;gt;
&amp;lt;/Table&amp;gt;
&lt;/pre&gt;
&lt;p&gt;An agent trained on &lt;a href=&quot;https://blog.logrocket.com/generating-the-feature-with-an-ai-coding-assistant/&quot;&gt;children-based component libraries&lt;/a&gt; has good reason to reach for the second pattern. The one reading Astryx’s manifest wrote the first.&lt;/p&gt;
&lt;h2 id=&quot;test-two-swapping-theme&quot;&gt;Test two: swapping the theme&lt;/h2&gt;
&lt;p&gt;This test shows the difference between a theme system that can cascade through the whole component library and one whose styling is distributed across tokens and component classes. It’s also where Astryx looked strongest.&lt;/p&gt;
&lt;p&gt;I originally wanted to swap both libraries to the same brutalist theme to keep the comparison fair. I couldn’t. Astryx’s brutalist theme package, &lt;code&gt;@astryxdesign/theme-brutalist&lt;/code&gt;, is listed on GitHub but wasn’t published to npm when I tested it.&lt;/p&gt;
&lt;p&gt;I used Astryx’s gothic theme instead and hand-approximated a brutalist look on the shadcn side. The resulting UIs have different aesthetics, so the useful comparison here is the mechanism: how much work each theme swap required and how completely it propagated through the UI.&lt;/p&gt;
&lt;p&gt;With Astryx, switching from neutral to gothic meant changing two files: the CSS import and the theme provider prop. Four lines changed, with no component files touched.&lt;/p&gt;
&lt;pre class=&quot;language-diff hljs&quot;&gt;--- a/src/index.css
+++ b/src/index.css
-@import &#39;@astryxdesign/theme-neutral/theme.css&#39;;
+@import &#39;@astryxdesign/theme-gothic/theme.css&#39;;

--- a/src/main.tsx
+++ b/src/main.tsx
-import {neutralTheme} from &#39;@astryxdesign/theme-neutral/built&#39;;
+import {gothicTheme} from &#39;@astryxdesign/theme-gothic/built&#39;;

-    &amp;lt;Theme theme={neutralTheme}&amp;gt;
+    &amp;lt;Theme theme={gothicTheme}&amp;gt;
&lt;/pre&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/static/img/pixel.gif&quot; alt=&quot;The dashboard after the theme swap, rendered in the gothic theme with a dark background, bordered cards, and pill-shaped status badges&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;The build worked immediately because Astryx layers its theme tokens through a CSS cascade that reaches every component. Change the theme, and the UI restyles with it.&lt;/p&gt;
&lt;p&gt;shadcn doesn’t have an equivalent theme package to swap, so I changed the token values directly with a handful of &lt;code&gt;sed&lt;/code&gt; commands, about six edits.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/blog-image-8.png&quot; alt=&quot;A diff of the shadcn index.css showing token values being rewritten, including chart and sidebar color tokens as oklch values&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;It got me partway.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/blog-image-6.png&quot; alt=&quot;The shadcn dashboard after the brutalist token edits, a near-black sidebar, flattened corners, and high-contrast sharp-edged badges&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;The sidebar went near-black and most corners flattened, but the theme didn’t propagate completely. The reason is structural. shadcn splits styling between CSS variables and Tailwind classes hardcoded in its component templates.&lt;/p&gt;
&lt;p&gt;Setting &lt;code&gt;--radius: 0&lt;/code&gt; flattens token-driven corners, but a component with a hardcoded &lt;code&gt;rounded-lg&lt;/code&gt; class ignores that token. Typography has the same limitation: components can hardcode classes such as &lt;code&gt;text-sm&lt;/code&gt; and &lt;code&gt;font-medium&lt;/code&gt;, so a broader typographic change requires editing component source.&lt;/p&gt;
&lt;p&gt;A complete theme swap therefore crosses into the component files. Astryx’s required four changed lines and touched none.&lt;/p&gt;
&lt;h2 id=&quot;test-three-settings-modal&quot;&gt;Test three: the settings modal&lt;/h2&gt;
&lt;p&gt;The last task was a settings modal with a form, three toggles, and a save button.&lt;/p&gt;
&lt;p&gt;Both libraries had every component I needed. shadcn provided &lt;code&gt;Dialog&lt;/code&gt;, &lt;code&gt;Input&lt;/code&gt;, &lt;code&gt;Switch&lt;/code&gt;, &lt;code&gt;Label&lt;/code&gt;, and &lt;code&gt;Separator&lt;/code&gt;, and the agent composed them without anything missing. The difference was how much layout scaffolding it had to create around those components.&lt;/p&gt;
&lt;p&gt;With shadcn, the modal needed extra wrapper &lt;code&gt;div&lt;/code&gt;s with flex and gap utilities to hold the layout together. With Astryx, the agent used &lt;code&gt;VStack&lt;/code&gt;, &lt;code&gt;HStack&lt;/code&gt;, and &lt;code&gt;FormLayout&lt;/code&gt;, primitives that already carry spacing behavior. More of the layout came from the design system itself.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Switch&lt;/code&gt; makes the difference especially clear. Astryx uses &lt;code&gt;value&lt;/code&gt; and &lt;code&gt;onChange&lt;/code&gt; instead of the &lt;code&gt;checked&lt;/code&gt; and &lt;code&gt;onCheckedChange&lt;/code&gt; pattern you might expect from other libraries. It also provides &lt;code&gt;description&lt;/code&gt; and &lt;code&gt;labelSpacing&lt;/code&gt; props for exactly the labeled-toggle pattern the task required.&lt;/p&gt;
&lt;p&gt;The agent knew that because it queried the component.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;// Astryx: label, description, and spacing are part of the component.
&amp;lt;Switch
  label=&quot;Email Notifications&quot;
  description=&quot;Receive product updates and account alerts&quot;
  value={emailNotifications}
  onChange={setEmailNotifications}
  labelSpacing=&quot;spread&quot;
/&amp;gt;
&lt;/pre&gt;
&lt;p&gt;The same toggle takes more scaffolding with shadcn because the label and description layout aren’t part of the component itself.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;// shadcn: you assemble the label and description with wrapper divs.
&amp;lt;div className=&quot;flex items-center justify-between&quot;&amp;gt;
  &amp;lt;div className=&quot;flex flex-col gap-0.5&quot;&amp;gt;
    &amp;lt;Label htmlFor=&quot;emailNotif&quot;&amp;gt;Email Notifications&amp;lt;/Label&amp;gt;
    &amp;lt;span className=&quot;text-xs text-muted-foreground&quot;&amp;gt;
      Receive product updates and account alerts
    &amp;lt;/span&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;Switch
    id=&quot;emailNotif&quot;
    checked={emailNotifications}
    onCheckedChange={setEmailNotifications}
  /&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/pre&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/blog-image-7.png&quot; alt=&quot;Astryx settings modal on the gothic theme, dark background, with a profile form, three labeled notification toggles, a danger-zone delete button, and cancel and save actions&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/blog-image-7.png&quot; alt=&quot;shadcn settings modal on a light background, with the same profile form, notification toggles, delete button, and footer actions&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;The two modals are shown on different themes, gothic for Astryx and the brutalist-inspired tokens for shadcn, because that’s where each build ended up after the previous test. The useful comparison is the composition rather than the color.&lt;/p&gt;
&lt;h2 id=&quot;the-numbers&quot;&gt;The numbers&lt;/h2&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;Here are the counts from the two runs, taken directly from the agent’s log rather than from a scoring system I imposed afterward.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Astryx (MCP connected)&lt;/th&gt;
&lt;th&gt;shadcn/ui (no manifest)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Component lookups before writing&lt;/td&gt;
&lt;td&gt;18, via the CLI&lt;/td&gt;
&lt;td&gt;0, read source and used prior knowledge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixes needed&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1 confirmed (&lt;code&gt;asChild&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What the fixes were&lt;/td&gt;
&lt;td&gt;Wrong prop names, caught at build&lt;/td&gt;
&lt;td&gt;Outdated Radix &lt;code&gt;asChild&lt;/code&gt; pattern on base-ui&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Components used&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theme swap&lt;/td&gt;
&lt;td&gt;4 lines, one theme import&lt;/td&gt;
&lt;td&gt;A few &lt;code&gt;sed&lt;/code&gt; edits to token values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theme swap result&lt;/td&gt;
&lt;td&gt;Complete, no component files touched&lt;/td&gt;
&lt;td&gt;Partial, radius and type stayed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup friction&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Tailwind plugin, install hang, misplaced files, &lt;code&gt;tsconfig&lt;/code&gt; flag&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;By raw count, Astryx looks worse: four fixes compared with shadcn’s one. The count hides the important difference, which is what kind of fixes they were.&lt;/p&gt;&lt;hr&gt;
&lt;iframe id=&quot;podrocket-plug&quot; src=&quot;https://open.spotify.com/embed/playlist/4j70ZVQNj5Lk3gtyCG5VWk?utm_source=generator&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot; allow=&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; loading=&quot;lazy&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;/iframe&gt;
&lt;hr&gt;
&lt;style&gt;
#podrocket-plug {
  border-radius: 12px;
  width: 75%;
  height: 352px;
  margin: 1rem auto;
  display: block;
}

&lt;/style&gt;
&lt;p&gt;Astryx’s four errors were incorrect prop names that TypeScript rejected within seconds. The confirmed shadcn error came from an outdated mental model of the library: &lt;code&gt;asChild&lt;/code&gt; belonged to the Radix-based version the agent knew, while v4 had moved on to base-ui.&lt;/p&gt;
&lt;p&gt;The manifest didn’t stop the agent from making mistakes. It changed the cost of those mistakes.&lt;/p&gt;
&lt;h2 id=&quot;the-catches&quot;&gt;The catches&lt;/h2&gt;
&lt;p&gt;Astryx is still a young public project, and a fair verdict needs to account for that.&lt;/p&gt;
&lt;p&gt;It was in Beta at v0.0.14 when I tested it. The public project is only weeks old, and its APIs can still shift before v1.0. Eight years of internal use across a reported 13,000 Meta apps is evidence of a mature internal system. It doesn’t tell us how stable the newly public tooling will be.&lt;/p&gt;
&lt;p&gt;The third-party ecosystem is also almost bare. I found one package on npm, &lt;code&gt;@atomic-testing/component-driver-astryx&lt;/code&gt;, and little else. There isn’t yet a large body of community patterns or Stack Overflow answers to fall back on when you hit an edge case.&lt;/p&gt;
&lt;p&gt;The brutalist theme package advertised in the repo, &lt;code&gt;@astryxdesign/theme-brutalist&lt;/code&gt;, wasn’t available on npm when I tested it, which is why my theme swap used gothic instead. It’s a small issue, but exactly the kind of rough edge you’d expect from a Beta release.&lt;/p&gt;
&lt;p&gt;The component count is inconsistent too: 90-plus in the repo, 150-plus in Meta’s docs, and 153 in the manifest generated by my CLI. Some components appear to exist internally but haven’t been extracted publicly yet.&lt;/p&gt;
&lt;p&gt;The StyleX concern is smaller than it might initially sound. You don’t need to adopt StyleX to use Astryx because its CSS is precompiled, and you can override components through &lt;code&gt;className&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The more consequential question is whether you want to build on a library whose public life is still measured in weeks. If you’re evaluating your &lt;a href=&quot;https://blog.logrocket.com/product-management/how-to-use-technical-debt-register/&quot;&gt;technical debt&lt;/a&gt; exposure before committing to a new dependency, that’s worth factoring in.&lt;/p&gt;
&lt;h2 id=&quot;so-is-it-worth-it&quot;&gt;So, is it worth it?&lt;/h2&gt;
&lt;p&gt;The important result from these tests is that Astryx didn’t stop the agent from making mistakes. It kept making them. What changed was the kind of mistake it made and how expensive each mistake was to diagnose.&lt;/p&gt;
&lt;p&gt;With Astryx, the errors were incorrect props that TypeScript caught immediately. With shadcn, the confirmed error came from a mental model the agent had no way to know was outdated. The theme test tells a similar story: four changed lines produced a complete Astryx restyle, while the shadcn version required manual token edits and still only partially changed the UI.&lt;/p&gt;
&lt;p&gt;Whether you should adopt Astryx today is a separate question from whether its approach makes sense. Giving an agent a real contract for the design system instead of relying on whatever API it remembers from training is clearly useful, and Astryx isn’t alone in moving that way. shadcn, Chakra UI, Storybook, and others are pursuing similar approaches.&lt;/p&gt;
&lt;p&gt;Astryx’s implementation is unusually complete for something this new, but the public project is still young. For a greenfield internal tool where you control the stack and want an &lt;a href=&quot;https://blog.logrocket.com/comparing-ai-agent-sandbox-platforms-e2b-modal-daytona-and-more/&quot;&gt;agent doing substantial UI composition&lt;/a&gt;, it’s worth a serious look. More broadly, it offers a useful preview of what AI-friendly design systems are likely to be expected to provide.&lt;/p&gt;

&lt;h2 id=&quot;key-takeaways&quot;&gt;Key takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The agent connected to Astryx’s manifest never invented a component that didn’t exist because it queried the real component list before writing&lt;/li&gt;
&lt;li&gt;Astryx needed more fixes than shadcn, four to one, but every Astryx fix was a wrong prop name caught at build time, while shadcn’s confirmed error came from an &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/&quot;&gt;outdated API assumption&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The theme swap exposed a structural difference: four changed lines produced a complete Astryx restyle, while shadcn required manual token edits and still produced only a partial theme change&lt;/li&gt;
&lt;li&gt;You don’t need to adopt StyleX to use Astryx; the larger concern is its Beta status and still-small public ecosystem&lt;/li&gt;
&lt;/ul&gt;


&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/ai/&quot;&gt;#ai&lt;/a&gt;
        &lt;/li&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/react/&quot;&gt;#react&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/astryx-vs-shadcn-meta-ai-react-claude/</link><guid isPermaLink="false">https://blog.logrocket.com/astryx-vs-shadcn-meta-ai-react-claude/</guid><pubDate>Invalid Date</pubDate><author>Chizaram Ken</author></item><item><title>10 best AI agent skills for senior engineers in 2026</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-ai-agent-skills-engineers/#top-10-ai-agent-skills&quot;&gt;What are the best AI agent skills for engineers?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;top-10-ai-agent-skills&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-ai-agent-skills-engineers/#superpowers&quot;&gt;Superpowers&lt;/a&gt;
        &lt;ul data-toc-name=&quot;superpowers&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-ai-agent-skills-engineers/#deslop&quot;&gt;Deslop&lt;/a&gt;
        &lt;ul data-toc-name=&quot;deslop&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-ai-agent-skills-engineers/#caveman&quot;&gt;Caveman&lt;/a&gt;
        &lt;ul data-toc-name=&quot;caveman&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-ai-agent-skills-engineers/#performance-optimization&quot;&gt;Performance optimization&lt;/a&gt;
        &lt;ul data-toc-name=&quot;performance-optimization&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-ai-agent-skills-engineers/#improve&quot;&gt;Improve&lt;/a&gt;
        &lt;ul data-toc-name=&quot;improve&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-ai-agent-skills-engineers/#test-driven-development-tdd&quot;&gt;Test-driven development (TDD)&lt;/a&gt;
        &lt;ul data-toc-name=&quot;test-driven-development-tdd&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-ai-agent-skills-engineers/#context7&quot;&gt;Context7&lt;/a&gt;
        &lt;ul data-toc-name=&quot;context7&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-ai-agent-skills-engineers/#react-best-practices&quot;&gt;React best practices&lt;/a&gt;
        &lt;ul data-toc-name=&quot;react-best-practices&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-ai-agent-skills-engineers/#nextjs-best-practices&quot;&gt;Next.js best practices&lt;/a&gt;
        &lt;ul data-toc-name=&quot;nextjs-best-practices&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-ai-agent-skills-engineers/#incremental-implementation&quot;&gt;Incremental implementation&lt;/a&gt;
        &lt;ul data-toc-name=&quot;incremental-implementation&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-ai-agent-skills-engineers/#skill-combinations-that-i-use&quot;&gt;How do you chain AI agent skills together?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;skill-combinations-that-i-use&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-ai-agent-skills-engineers/#large-feature-build&quot;&gt;Which skills work best for large feature builds?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;large-feature-build&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-ai-agent-skills-engineers/#performance-regression-and-refactoring&quot;&gt;Which skills work best for performance refactoring?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;performance-regression-and-refactoring&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-ai-agent-skills-engineers/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;When using AI coding agents, a frustrating pattern usually emerges: the agent ignores key codebase rules, churns out verbose boilerplate, or clutters the context window with repetitive prompts.&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;895&quot; height=&quot;597&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/10-best-AI-agent-skills-for-senior-engineers-in-2026.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/10-best-AI-agent-skills-for-senior-engineers-in-2026.png 895w, https://blog.logrocket.com/wp-content/uploads/2026/08/10-best-AI-agent-skills-for-senior-engineers-in-2026-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/08/10-best-AI-agent-skills-for-senior-engineers-in-2026-768x512.png 768w&quot; sizes=&quot;(max-width: 895px) 100vw, 895px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;Manually pasting guidelines into every fresh session or copying system instructions over and over is tedious and burns through tokens unnecessarily.&lt;/p&gt;
&lt;p&gt;Skill files solve this at the root.&lt;/p&gt;
&lt;p&gt;A skill is a structured prompt file loaded into your agent’s context once, altering how it behaves across every task where that capability is relevant.&lt;/p&gt;
&lt;p&gt;This article explores ten AI agent skills that deliver the highest practical value for a senior engineer’s day-to-day workflow. For each one, we will break down what it does, how to install it, and when you are better off skipping it.&lt;/p&gt;
&lt;h2 id=&quot;top-10-ai-agent-skills&quot;&gt;What are the best AI agent skills for engineers?&lt;/h2&gt;
&lt;p&gt;For quick reference, we are considering the following agent skills:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/obra/superpowers&quot;&gt;Superpowers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://explainx.ai/skills/cursor/plugins/deslop&quot;&gt;Deslop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/juliusbrussee/caveman&quot;&gt;Caveman&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://explainx.ai/skills/supercent-io/skills-template/performance-optimization&quot;&gt;Performance optimization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/shadcn/improve&quot;&gt;Improve&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mfranzon/tdd&quot;&gt;Test-driven development (TDD)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/netresearch/context7-skill&quot;&gt;Context7&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://agenticskills.io/skills/react-best-practices&quot;&gt;React best practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://agenticskills.io/skills/next-best-practices&quot;&gt;Next.js best practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://explainx.ai/skills/explainx/incremental-implementation/incremental-implementation&quot;&gt;Incremental implementation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;superpowers&quot;&gt;Superpowers&lt;/h3&gt;
&lt;p&gt;Superpowers is an agentic skill pack designed to enforce a complete, end-to-end software engineering workflow on your AI agent. Rather than letting the model leap straight into writing code, it routes execution through structured, non-negotiable quality gates:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Upfront brainstorming&lt;/strong&gt;: Uncovers hidden assumptions and resolves ambiguities before a single line of code is written&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Granular implementation planning&lt;/strong&gt;: Generates an isolated, step-by-step roadmap broken into small, independently executable tasks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sub-agent context isolation&lt;/strong&gt;: Spawns dedicated sub-agents to complete each micro-task in its own fresh context window, eliminating context drift across components&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strict test-driven development (TDD)&lt;/strong&gt;: Enforces a strict red-green-refactor cycle within each sub-agent before changes are accepted&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated code review&lt;/strong&gt;: Triggers a structured review pass against completed changes to verify both spec compliance and code quality&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clean handoff&lt;/strong&gt;: Prepares an isolated git feature branch and delivers a clean Pull Request ready for your final human sign-off&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;superpowers-setup&quot;&gt;How do you install the Superpowers skill?&lt;/h4&gt;
&lt;p&gt;Install the Superpowers skill by adding the skill file to your agent’s skills directory. In Claude Code, that looks like:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;/plugin install superpowers@claude-plugins-official&lt;/pre&gt;
&lt;p&gt;Once it is set up, you do not explicitly need to call Superpowers in your coding agent. Instead, Superpowers scans its installed skill registry dynamically, selecting and invoking the exact tools needed for your specific task at any given moment.&lt;/p&gt;
&lt;h4 id=&quot;when-not-to-use-superpowers&quot;&gt;When should you skip the Superpowers skill?&lt;/h4&gt;
&lt;p&gt;Skip Superpowers for single-function changes, quick bug fixes, or small, isolated refactors. The upfront brainstorming and planning phases consume both time and tokens; that investment only pays off when applied to moderate or large-scale tasks.&lt;/p&gt;
&lt;h3 id=&quot;deslop&quot;&gt;Deslop&lt;/h3&gt;
&lt;p&gt;Deslop checks the diff against your main branch and removes AI-generated artifacts introduced in the current branch. It targets problems that agents introduce consistently, but that are easy to miss in review.&lt;/p&gt;
&lt;h4 id=&quot;deslop-setup&quot;&gt;How do you install the Deslop skill?&lt;/h4&gt;
&lt;p&gt;Install Deslop using the &lt;code&gt;npx skills add&lt;/code&gt; command below:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npx skills add https://github.com/cursor/plugins --skill deslop&lt;/pre&gt;
&lt;p&gt;After installation, you can go ahead and use the deslop prompt in your codebase like so:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;Run deslop on this codebase&lt;/pre&gt;
&lt;h4 id=&quot;when-not-to-use-deslop&quot;&gt;When should you skip Deslop?&lt;/h4&gt;
&lt;p&gt;Deslop compares against local style. On a project with no established conventions, the comparison has no baseline. Use it on codebases where existing files have a recognizable pattern. On new projects, skip Deslop until the codebase has enough examples to define a style.&lt;/p&gt;
&lt;h3 id=&quot;caveman&quot;&gt;Caveman&lt;/h3&gt;
&lt;p&gt;Caveman compresses an AI agent’s output to communicate with precision while maintaining technical accuracy.&lt;/p&gt;
&lt;p&gt;An AI agent output with Caveman looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;authenticate: check email in DB → validate bcrypt hash → JWT on pass, error on fail&lt;/pre&gt;
&lt;p&gt;Without Caveman, it looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;The `authenticate` function is responsible for verifying user credentials against
the database. It first checks whether the provided email address exists in the
users table, then validates the password hash using bcrypt. If either check fails,
it returns an appropriate error response. If both checks pass, it generates a JWT
token and returns it to the caller.&lt;/pre&gt;
&lt;p&gt;Caveman supports the following levels of compression:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Light&lt;/strong&gt;: This removes filler sentences and language, keeps full prose structure&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full&lt;/strong&gt;: This compresses to structured notation with arrows and short-form syntax&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ultra&lt;/strong&gt;: This is a single-line compression for status updates and confirmations&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;caveman-setup&quot;&gt;How do you install the Caveman skill?&lt;/h4&gt;
&lt;p&gt;Install Caveman with the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npx skills add https://github.com/juliusbrussee/caveman --skill caveman&lt;/pre&gt;
&lt;p&gt;After installation, you can go ahead and prompt the AI like so:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;Use caveman skill (full mode) to explain the current error.&lt;/pre&gt;
&lt;p&gt;Or set it as a session default by invoking it at the start of a session:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;Use caveman for all responses in this session.&lt;/pre&gt;
&lt;h3 id=&quot;performance-optimization&quot;&gt;Performance optimization&lt;/h3&gt;
&lt;p&gt;Performance optimization skill, as the name suggests, handles performance. It takes care of the following performance issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Frontend&lt;/strong&gt;: &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;, lazy loading, code splitting, image optimization, and bundle analysis with webpack-bundle-analyzer&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backend&lt;/strong&gt;: N+1 query fixes, database indexing, Redis caching, and API response compression&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measurement&lt;/strong&gt;: Lighthouse for page-level scores and Web Vitals for Core Web Vitals tracking; profiling comes before any optimization step&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;performance-optimization-setup&quot;&gt;How do you install the performance-optimization skill?&lt;/h4&gt;
&lt;p&gt;You can install the skill with the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npx skills add https://github.com/supercent-io/skills-template --skill performance-optimization&lt;/pre&gt;
&lt;p&gt;You can then go ahead and run it when you run into performance issues like below:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;Use performance-optimization skill to diagnose the slow initial load on
the dashboard page and suggest fixes in order of expected impact.&lt;/pre&gt;
&lt;h4 id=&quot;when-not-to-use-performance-optimization&quot;&gt;When should you skip performance optimization?&lt;/h4&gt;
&lt;p&gt;The skill optimizes existing code. Without a measurement baseline, the optimization recommendations have nothing to improve against.&lt;/p&gt;

&lt;h3 id=&quot;improve&quot;&gt;Improve&lt;/h3&gt;
&lt;p&gt;Improve skill addresses a cost problem in agent-assisted development. High capability models produce better plans and catch more edge cases, but running them for every implementation is expensive. Cheaper models are cost-effective but cannot reliably navigate complex multi-file changes on their own.&lt;/p&gt;
&lt;p&gt;The most capable model reads the code, identifies the problem, and writes a detailed step-by-step implementation plan. A cheaper model then reads that plan and implements each step without loading the full codebase into its context window.&lt;/p&gt;
&lt;h4 id=&quot;improve-setup&quot;&gt;How do you install the Improve skill?&lt;/h4&gt;
&lt;p&gt;Install this skill with the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npx skills add shadcn/improve&lt;/pre&gt;
&lt;p&gt;Invoke it by describing the task for the senior model to analyze:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;Use improve skill to analyze the subscription billing flow and generate
an implementation plan for adding proration when a user upgrades mid-cycle.&lt;/pre&gt;
&lt;h4 id=&quot;when-not-to-use-improve&quot;&gt;When should you skip the Improve skill?&lt;/h4&gt;
&lt;p&gt;The Improve skill adds an analysis phase before any code is written. For straightforward changes, the planning overhead is not justified. Reserve Improve for tasks where the implementation plan itself is the hard part.&lt;/p&gt;
&lt;h3 id=&quot;test-driven-development-tdd&quot;&gt;Test-driven development (TDD)&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://blog.logrocket.com/tdd-with-react-test-renderer/&quot;&gt;TDD&lt;/a&gt; skill enforces a test-driven cycle at the agent level. It enforces the following test cycle:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Red&lt;/strong&gt;: write a test that specifies the desired behaviour. The test must fail before any implementation exists. The agent confirms the failure before proceeding&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Green&lt;/strong&gt;: write the minimum implementation code to make the test pass. Not the best implementation; the minimal one&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refactor&lt;/strong&gt;: clean up the implementation while keeping the test green&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The skill blocks the agent from writing implementation code until step one is complete and the test failure is confirmed.&lt;/p&gt;
&lt;h4 id=&quot;tdd-setup&quot;&gt;How do you install the TDD skill?&lt;/h4&gt;
&lt;p&gt;The TDD skill is included in Superpowers but runs as a standalone skill for focused use cases. You can use it separately like the example below:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;Use TDD skill to add input validation to the createUser function.
It should reject empty strings, emails without @ symbols, and
passwords shorter than 8 characters.&lt;/pre&gt;
&lt;p&gt;The agent will first write the test file, show you the failing test output, and then write the implementation.&lt;/p&gt;
&lt;h4 id=&quot;when-not-to-use-tdd&quot;&gt;When should you skip TDD for AI agents?&lt;/h4&gt;
&lt;p&gt;TDD adds a test-writing step to every implementation task. For throwaway scripts, one-off data migrations, or prototype code you know you will delete, the overhead is not justified. Use the skill for code that will be maintained and changed over time.&lt;/p&gt;
&lt;h3 id=&quot;context7&quot;&gt;Context7&lt;/h3&gt;
&lt;p&gt;Context7 is an agent skill that fetches current, version-specific documentation for any library as needed. When the agent is about to write code that uses an external library, Context7 intercepts the request, fetches the relevant documentation for the installed version of that library, and injects it into the agent’s context window.&lt;/p&gt;
&lt;p&gt;The documentation injection happens automatically; you do not have to tell the agent which version of which library to look up. Context7 also has an MCP server that you can learn about in our earlier &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/&quot;&gt;blog post&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;context7-setup&quot;&gt;How do you install the Context7 skill?&lt;/h4&gt;
&lt;p&gt;Install the Context7 skill with:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npx skills add https://github.com/netresearch/context7-skill --skill context7&lt;/pre&gt;
&lt;p&gt;After installation, the skill activates automatically when the agent detects library usage in its output.&lt;/p&gt;
&lt;h4 id=&quot;when-not-to-use-context7&quot;&gt;When should you skip Context7?&lt;/h4&gt;
&lt;p&gt;Context7 relies on the library’s documentation being available and up to date. For niche or internal libraries, the documentation source may not exist or may not be indexed. In those cases, you need to supply the relevant documentation manually through a context file or an inline prompt.&lt;/p&gt;
&lt;h3 id=&quot;react-best-practices&quot;&gt;React best practices&lt;/h3&gt;
&lt;p&gt;This skill enforces the following in your &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/&quot;&gt;React&lt;/a&gt; codebase:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;State colocation&lt;/strong&gt;: Keep state in the component that owns it, not in a parent that passes it down&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composition over prop drilling&lt;/strong&gt;: When a value needs to pass more than two levels, use context or component composition&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exhaustive useEffect dependency arrays&lt;/strong&gt;: No missing dependencies, no suppression comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Selective memoization&lt;/strong&gt;: Apply &lt;code&gt;useMemo&lt;/code&gt; and &lt;code&gt;useCallback&lt;/code&gt; only where profiling shows a measurable re-render cost&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data-fetching separation&lt;/strong&gt;: Fetching logic lives outside presentation components&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;react-best-practices-setup&quot;&gt;How do you install the React best-practices skill?&lt;/h4&gt;
&lt;p&gt;Go ahead and install the skill with the command below:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npx skills add vercel-labs/agent-skills&lt;/pre&gt;
&lt;p&gt;Invoke it like so:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;Use react-best-practices skill to build a filterable product list
with search input, category filters, and paginated results.&lt;/pre&gt;
&lt;h3 id=&quot;nextjs-best-practices&quot;&gt;Next.js best practices&lt;/h3&gt;
&lt;p&gt;Next.js best practices skill from Vercel Labs enforces Next.js best practices in your Next.js codebase. It enforces the following in Next.js:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Correct RSC boundaries and detection of invalid async client component patterns&lt;/li&gt;
&lt;li&gt;Async &lt;code&gt;params&lt;/code&gt; and &lt;code&gt;searchParams&lt;/code&gt; usage for Next.js 15+&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&#39;use client&#39;&lt;/code&gt; and &lt;code&gt;&#39;use server&#39;&lt;/code&gt; directive placement&lt;/li&gt;
&lt;li&gt;Data fetching patterns that avoid waterfalls&lt;/li&gt;
&lt;li&gt;Error boundary conventions using &lt;code&gt;error.tsx&lt;/code&gt;, &lt;code&gt;not-found.tsx&lt;/code&gt;, and &lt;code&gt;global-error.tsx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Image and font optimization with &lt;code&gt;next/image&lt;/code&gt; and &lt;code&gt;next/font&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Runtime selection between Node.js and Edge runtimes&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;nextjs-best-practices-setup&quot;&gt;How do you install the Next.js best-practices skill?&lt;/h4&gt;
&lt;p&gt;Install the skill with:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npx skills add vercel-labs/next-best-practices&lt;/pre&gt;
&lt;p&gt;Use it like below:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;Use next-best-practices skill to build a product listing page with
server-side data fetching, dynamic route params, and optimized images.&lt;/pre&gt;
&lt;h4 id=&quot;when-not-to-use-nextjs-best-practices&quot;&gt;When should you skip the Next.js best-practices skill?&lt;/h4&gt;
&lt;p&gt;The skill targets &lt;a href=&quot;https://blog.logrocket.com/next-js-16-whats-new/&quot;&gt;Next.js 15+&lt;/a&gt; patterns. On projects running older versions of Next.js, there could be issues.&lt;/p&gt;
&lt;h3 id=&quot;incremental-implementation&quot;&gt;Incremental implementation&lt;/h3&gt;
&lt;p&gt;The incremental-implementation skill constrains the agent to implement the smallest complete piece of functionality, run tests, verify the build, commit, before moving to the next task. Each completed task leaves the codebase in a working state.&lt;/p&gt;
&lt;h4 id=&quot;incremental-implementation-setup&quot;&gt;How do you install the incremental-implementation skill?&lt;/h4&gt;
&lt;p&gt;Install the incremental implementation skill with the command below:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npx skills add https://github.com/addyosmani/agent-skills --skill incremental-implementation&lt;/pre&gt;
&lt;p&gt;Go ahead and invoke it like so:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;Use incremental-implementation skill to add user profile editing. Start
with just the API endpoint — do not touch the UI yet.&lt;/pre&gt;
&lt;h4 id=&quot;when-not-to-use-incremental-implementation&quot;&gt;When should you skip incremental implementation?&lt;/h4&gt;
&lt;p&gt;If the change is already minimal, the increment cycle adds overhead without adding safety. Use it for changes that span more than one file or that involve multiple layers.&lt;/p&gt;
&lt;h2 id=&quot;skill-combinations-that-i-use&quot;&gt;How do you chain AI agent skills together?&lt;/h2&gt;
&lt;p&gt;In my workflow, I chain complementary skills together to build a robust development pipeline. These are some of the ways I chain these agent skills.&lt;/p&gt;
&lt;h3 id=&quot;large-feature-build&quot;&gt;Which skills work best for large feature builds?&lt;/h3&gt;
&lt;p&gt;I use this combination when building features that span multiple components or cross architectural boundaries.&lt;/p&gt;
&lt;p&gt;For a large feature build, I chain these skills:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Superpowers&lt;/strong&gt;: I initiate the feature workflow with Superpowers to handle brainstorming, clarify requirements, and design the initial architecture. Superpowers isolates implementation steps into sub-agents to avoid context window pollution&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context7&lt;/strong&gt;: I equip the agent with Context7 to automatically pull version-specific documentation for any new dependencies I introduce&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;React/Next.js best practices&lt;/strong&gt;: I apply the React or Next.js Best Practices skill to keep my component hierarchy clean, enforce correct RSC boundaries, and avoid common rendering bottlenecks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Incremental implementation&lt;/strong&gt;: I enforce Incremental Implementation to guide the sub-agents through minimal, verifiable commits&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deslop&lt;/strong&gt;: Finally, I run Deslop on the finished diff to strip away any temporary AI-generated comments, verbose explanations, or unnecessary console logs before opening the pull request&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;performance-regression-and-refactoring&quot;&gt;Which skills work best for performance refactoring?&lt;/h3&gt;
&lt;p&gt;I use this combination when investigating performance bottlenecks or refactoring code.&lt;/p&gt;
&lt;p&gt;My refactoring chain runs as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Performance optimization&lt;/strong&gt;: I profile the application using Lighthouse or Web Vitals to locate the exact bottleneck&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TDD&lt;/strong&gt;: I write a failing test that reproduces the performance issue or captures the existing contract requirements. This locks the expected behavior in place&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Incremental implementation&lt;/strong&gt;: I apply my optimizations in small, isolated steps rather than a single massive refactor. I run the tests after each change to verify correctness&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deslop&lt;/strong&gt;: I run Deslop to clean up the code&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;AI coding agents are powerful, but they are only as good as the guardrails and context we provide. By integrating skills into our daily workflow, we shift from manually babysitting prompts to establishing a declarative development environment.&lt;/p&gt;


&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/ai/&quot;&gt;#ai&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/top-ai-agent-skills-engineers/</link><guid isPermaLink="false">https://blog.logrocket.com/top-ai-agent-skills-engineers/</guid><pubDate>Invalid Date</pubDate><author>Emmanuel John</author></item><item><title>Why Google is probably going to win the AI race</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/why-google-wins-ai-race/#who-has-most-data&quot;&gt;Who has the most data?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;who-has-most-data&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/why-google-wins-ai-race/#enter-antigravity&quot;&gt;Enter Antigravity&lt;/a&gt;
        &lt;ul data-toc-name=&quot;enter-antigravity&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/why-google-wins-ai-race/#cruising-agentic-web&quot;&gt;Cruising the agentic web&lt;/a&gt;
        &lt;ul data-toc-name=&quot;cruising-agentic-web&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/why-google-wins-ai-race/#working-through-information&quot;&gt;Working through the information&lt;/a&gt;
        &lt;ul data-toc-name=&quot;working-through-information&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/why-google-wins-ai-race/#tale-two-ai-ides&quot;&gt;A tale of two AI IDEs&lt;/a&gt;
        &lt;ul data-toc-name=&quot;tale-two-ai-ides&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/why-google-wins-ai-race/#what-about-model&quot;&gt;But what about the model?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-about-model&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/why-google-wins-ai-race/#final-piece-monetization&quot;&gt;The final piece: monetization&lt;/a&gt;
        &lt;ul data-toc-name=&quot;final-piece-monetization&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;For a long time, perhaps too long, if the antitrust lawsuits are anything to go by, &lt;a href=&quot;https://knowledge.wharton.upenn.edu/article/why-google-dominates-the-search-engine-market/&quot;&gt;Google has dominated the search engine space&lt;/a&gt;. Recently, &lt;a href=&quot;https://www.cnn.com/2026/02/10/business/google-one-hundred-year-bond&quot;&gt;Alphabet issued a hundred-year bond to fund its AI ambitions&lt;/a&gt;, so the idea of Google still being around in a hundred years is apparently not that far-fetched.&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;1536&quot; height=&quot;1024&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/featured-image-1.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;Why Google is probably going to win the AI race&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/featured-image-1.png 1536w, https://blog.logrocket.com/wp-content/uploads/2026/08/featured-image-1-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/08/featured-image-1-1024x683.png 1024w, https://blog.logrocket.com/wp-content/uploads/2026/08/featured-image-1-768x512.png 768w&quot; sizes=&quot;(max-width: 1536px) 100vw, 1536px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;At the end of 2022, OpenAI released ChatGPT to the public. The results were incredible. The idea that you could simply ask something in natural language and receive a direct answer felt like a game changer. The days of searching Google and manually flipping through results suddenly seemed a little dated. You could just &lt;em&gt;ask&lt;/em&gt; your question, and the answer appeared. The flow-on effects for other industries, especially software development, were immense, and they are still being felt around the world.&lt;/p&gt;
&lt;p&gt;For a brief moment, it seemed like what Google had set out to do had been rendered obsolete by ChatGPT. People could ask a specific question and get a specific answer. For many queries, this made Google’s approach, and by extension Bing’s and every other search engine’s, feel redundant.&lt;/p&gt;
&lt;p&gt;Competition in this space was a good thing. Competition forces incumbents to move faster. It can even destabilize companies that have held a category for so long that their dominance starts to feel inevitable.&lt;/p&gt;
&lt;p&gt;In the long term, though, people usually coalesce around one tool. Nobody tells you to &lt;em&gt;Bing&lt;/em&gt; something. They tell you to Google it.&lt;/p&gt;
&lt;p&gt;The same is often true of social networks. Facebook still owns a huge part of the social media game, while Instagram dominates a major share of image and video sharing. The only way to establish market dominance against platforms like that is to release something people actually want, then spend an absurd amount of money promoting it. For example, &lt;a href=&quot;https://www.digitalinformationworld.com/2022/10/bytedance-spent-192-billion-on.html#:~:text=ByteDance%20spent%20$19.2%20billion%20on,for%20TikTok%20is%20fairly%20visible.&quot;&gt;ByteDance reportedly spent nearly $20 billion promoting TikTok in 2021&lt;/a&gt;. TikTok became massive. Instagram is still here.&lt;/p&gt;
&lt;p&gt;The current AI field feels reminiscent of the early search engine era, when people bounced between AltaVista, Yahoo, and Google. To get the best results, you had to try a few engines, until one became clearly and obviously better.&lt;/p&gt;
&lt;p&gt;OpenAI, Anthropic, Google, Meta, and others are all in this race. But the question is not just who has the best chatbot today. It is who has the most data, who has the best tools, who has the strongest distribution, and who can subsidize the whole thing for the longest.&lt;/p&gt;
&lt;h2 id=&quot;who-has-most-data&quot;&gt;Who has the most data?&lt;/h2&gt;
&lt;p&gt;AI, and by extension large language models, are essentially probabilistic systems that generate one token at a time based on learned probability distributions. When things go well, they produce the right answer or outcome. But because they are probabilistic, their answers can sound plausible while being wildly wrong. That is why AI systems sometimes tell people to &lt;a href=&quot;https://www.unsw.edu.au/newsroom/news/2024/05/eat-a-rock-a-day-put-glue-on-your-pizza-how-googles-ai-is-losing-touch-with-reality&quot;&gt;put glue on pizza or eat rocks&lt;/a&gt;. The model has seen enough text to produce a likely-sounding answer, but it has not necessarily classified that answer as nonsense.&lt;/p&gt;
&lt;p&gt;To train the best possible model, you would ideally compile every useful word ever written by a human into a dataset. But only a subset of that data is freely available, and purchasing every piece of high-quality content ever written would cost more money than anyone has.&lt;/p&gt;
&lt;p&gt;So how do companies get the data they need?&lt;/p&gt;
&lt;p&gt;There are a few options, but one of the big ones is web scraping. Plenty of websites contain high-quality human-written information. Less than a year after ChatGPT was released, &lt;a href=&quot;https://www.wired.com/story/stack-overflow-will-charge-ai-giants-for-training-data/&quot;&gt;Stack Overflow announced that it would begin charging AI companies for access to its data&lt;/a&gt;. By that point, though, the horse had largely bolted. Huge amounts of data had already been scraped from sites like Stack Overflow, Reddit, and Twitter.&lt;/p&gt;
&lt;p&gt;We also know that AI needs real, human-written data. &lt;a href=&quot;https://www.wsj.com/opinion/ai-cant-teach-ai-new-tricks-model-collapse-need-for-human-prose-a6d0648f&quot;&gt;Training AI on too much AI-generated material can lead to model collapse&lt;/a&gt;. At the same time, AI companies need enormous datasets to keep improving model quality.&lt;/p&gt;
&lt;p&gt;So who has the most data?&lt;/p&gt;
&lt;p&gt;Surprising nobody, it is probably Google. Google has been crawling and indexing the web for decades to power the most popular search engine in the world. It already has infrastructure built around discovering, categorizing, ranking, and storing web content. Why would it not also find ways to use that knowledge to improve its own AI systems?&lt;/p&gt;

&lt;p&gt;But the open season on data is ending. Publishers have gotten smarter. Websites have firmed up their positions on AI training. When scrapers do not play nicely, they can get &lt;a href=&quot;https://blog.cloudflare.com/ai-labyrinth/&quot;&gt;sent down an endless AI-fueled rabbit hole&lt;/a&gt;. More attention is being paid to how AI models are trained, and publishers are increasingly willing to sue providers for unlicensed use of their content.&lt;/p&gt;
&lt;p&gt;So how do companies like Google keep training and improving their models when more of the web is being locked down?&lt;/p&gt;
&lt;h2 id=&quot;enter-antigravity&quot;&gt;Enter Antigravity&lt;/h2&gt;
&lt;p&gt;In late November, Google announced Antigravity, a VS Code fork with integrated agentic coding abilities. While companies like OpenAI integrated into existing environments through VS Code extensions, Google chose to ship its own IDE with the AI tooling baked in.&lt;/p&gt;
&lt;p&gt;Google shipped Antigravity with its own Gemini models, but also with Anthropic models like Claude Sonnet and Opus. Spinning up multiple agentic coding sessions was easy. Google’s generous access to its own models, plus access to Opus 4.5, made Antigravity an instant hit.&lt;/p&gt;
&lt;p&gt;But unexpectedly, this IDE had another trick up its sleeve. It would not just test web apps to see whether they worked. When I was in the middle of a particularly tricky feature, Antigravity could not figure out what to do. So it opened Chrome, and what happened next kind of blew my mind.&lt;/p&gt;
&lt;h2 id=&quot;cruising-agentic-web&quot;&gt;Cruising the agentic web&lt;/h2&gt;
&lt;p&gt;The LLM could not quite figure out how to use the library I had chosen. So it opened GitHub and navigated to the repository. Then it retrieved the DOM for the browser, which gave it the layout of the web page and the content on it. It kept moving through each page, occasionally asking to run JavaScript to grep for a specific thing.&lt;/p&gt;
&lt;p&gt;Eventually, it began leafing through the tests for the library. From those tests, it found up-to-the-minute information on how the function worked and was able to use it correctly in the app.&lt;/p&gt;
&lt;p&gt;That is genuinely interesting because AI is trained on fixed sets of information, and every training set has an end date. The idea that an agent can open a browser, retrieve a website in real time, and use that information to solve a problem changes the dynamic.&lt;/p&gt;

&lt;p&gt;It also has implications for Google’s broader AI strategy.&lt;/p&gt;
&lt;h2 id=&quot;working-through-information&quot;&gt;Working through the information&lt;/h2&gt;
&lt;p&gt;We know that not all websites allow scraping. Some explicitly prohibit AI companies from using their data. Sometimes scrapers ignore those preferences anyway, which is why companies like Cloudflare are building ways to confuse them.&lt;/p&gt;
&lt;p&gt;So let’s play out a mental exercise.&lt;/p&gt;
&lt;p&gt;Website A has the information we need, but it does not allow scraping or AI training. In theory, that data cannot be added to an LLM training set, and the information stays out of reach.&lt;/p&gt;
&lt;p&gt;At the same time, we know that &lt;a href=&quot;https://blog.logrocket.com/ai-dev-tool-power-rankings/&quot;&gt;AI IDEs like Antigravity can search Google and browse websites&lt;/a&gt; to find information. A browser on your local computer can interact with websites like a normal user, but under the control of an AI agent. If something like a CAPTCHA or Cloudflare challenge appears, the user can complete it, and the agent can go back to work.&lt;/p&gt;
&lt;p&gt;We also know that AI products often log conversations and interaction data, and that providers may use some of that data, depending on settings and policies, to improve or tune their systems. At scale, this kind of analysis would almost certainly be automated.&lt;/p&gt;
&lt;p&gt;So imagine that same website, the one that never allowed scraping. It had the answer to our problem. The AI agent navigated to the site. The user solved the bot challenge. The agent parsed the page contents to decide where to look. Some version of that session may then become telemetry, training feedback, or product improvement data.&lt;/p&gt;
&lt;p&gt;Technically, did Google scrape the website to get the data? Not in the old-fashioned sense. But a Google-controlled tool may have accessed the site’s content through a user’s browser, summarized it, and sent portions of that interaction back to Google.&lt;/p&gt;
&lt;p&gt;Does that mean that even websites that expect AI companies to license their data, such as Stack Overflow, could still end up indirectly contributing to AI systems through agentic browsing sessions?&lt;/p&gt;
&lt;p&gt;Uh, did Google just turn &lt;em&gt;us&lt;/em&gt; into web scrapers, or at least into accessories to the process?&lt;/p&gt;
&lt;p&gt;It is hard to say. But it is also hard to believe that a company racing to build better models would ignore valuable interaction data simply because it came from a user’s browser under agent control.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/blog-image-5.png&quot; alt=&quot;Antigravity agent browsing session showing the AI navigating GitHub to retrieve library documentation&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;So Google has decades of access to human-written web information from running the world’s biggest search engine. It also appears to have a path into content that may be harder for traditional crawlers to access. That answers part of the data question.&lt;/p&gt;
&lt;p&gt;But what about the tooling?&lt;/p&gt;
&lt;h2 id=&quot;tale-two-ai-ides&quot;&gt;A tale of two AI IDEs&lt;/h2&gt;
&lt;p&gt;Annoyingly, when AI companies decide to make AI development tools, many of them seem to do the same thing: fork VS Code and jam their AI features on top.&lt;/p&gt;
&lt;p&gt;I am not a billion-dollar company, but let me tell you, forking VS Code just so you can integrate your AI tools into it is usually the wrong thing to do.&lt;/p&gt;
&lt;p&gt;Why? For starters, the moment you fork VS Code, &lt;a href=&quot;https://medium.com/javascript-in-plain-english/how-google-antigravity-gets-punished-by-microsofts-shenanigans-302d1d240224&quot;&gt;you lose access to arguably its biggest asset: the VS Code Extension Marketplace&lt;/a&gt;. You wind up using something like Open VSX, which is good and useful, but not as well-stocked as the proper marketplace. It has also been compromised before, &lt;a href=&quot;https://www.wiz.io/blog/supply-chain-risk-in-vscode-extension-marketplaces&quot;&gt;introducing a supply chain risk through the extension ecosystem&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Then there is duplication. VS Code is a fairly large app for a text editor. If a user has VS Code installed and then installs a second VS Code-like app with some AI code-assist features sprinkled through it, the setup quickly becomes unwieldy.&lt;/p&gt;
&lt;p&gt;Unfortunately, this is what Google did with its first attempt at an AI coding tool. Antigravity arrived, and, surprise surprise, it was a VS Code fork. The case for doing this was not always clear. Antigravity users effectively had an inferior copy of VS Code on their computers with Gemini features added. The AI tooling would sometimes break mid-task, wasting credits and time. Reddit filled up with people complaining about it.&lt;/p&gt;
&lt;p&gt;OpenAI’s approach was different. It did not fork VS Code. It wrote an extension for it. Users could keep the same VS Code instance with all their existing plugins. It was less resource-heavy. It broke less. OpenAI did not have to keep merging upstream VS Code changes into a fork over time.&lt;/p&gt;
&lt;p&gt;Then OpenAI made the Codex app, which was standalone from VS Code altogether. Basically, it is a chat app with a frontend.&lt;/p&gt;
&lt;p&gt;And in the time it has taken me to write this article, Google has re-released Antigravity. Let’s look for any hints of inspiration from what OpenAI did.&lt;/p&gt;
&lt;p&gt;For starters, here is Codex:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/blog-image-4.png&quot; alt=&quot;OpenAI Codex desktop app interface showing a chat-based frontend for agentic coding&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;And here is the new Google Antigravity:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/blog-image-3.png&quot; alt=&quot;Google Antigravity app interface with a sidebar and chat-based layout similar to Codex&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;How brave and unique. The sidebar is an entirely different color.&lt;/p&gt;
&lt;p&gt;But seriously: the AI industry is coalescing around the thing that works. Google has not abandoned its VS Code fork altogether, as suggested by the “Install IDE” button in the top right of Antigravity. But OpenAI was right about the Codex desktop app design, and Google has more or less lifted that pattern wholesale.&lt;/p&gt;
&lt;p&gt;Better still, many of the earlier errors around agents dropping out mid-conversation seem to be resolved in this version of Antigravity. That was by far its biggest problem.&lt;/p&gt;
&lt;h2 id=&quot;what-about-model&quot;&gt;But what about the model?&lt;/h2&gt;
&lt;p&gt;Google has access to more of the web than almost any other company because of its search engine. And, as we have covered, Antigravity can browse websites and use live content to help solve coding problems.&lt;/p&gt;
&lt;p&gt;The missing piece is the model.&lt;/p&gt;
&lt;p&gt;From my own experience, Google’s models are still not that crash-hot. Gemini Flash, even on high thinking, is frequently the model that gives me code that is needlessly obtuse or does not compile. When I ask Gemini to make an app and then ask Opus or Grok to review it, the latter models often find multiple bugs that need fixing. &lt;a href=&quot;https://blog.logrocket.com/generating-the-feature-with-an-ai-coding-assistant/&quot;&gt;Shipping AI-generated code without careful review can introduce subtle bugs&lt;/a&gt; that are easy to miss.&lt;/p&gt;
&lt;p&gt;Speaking of Grok, Grok 4.5 was recently released to the public after xAI/SpaceXAI merged its efforts with Cursor and trained the model. I have been using it a lot lately, and while it is not quite as good as Opus 4.6 or 4.8, it is definitely in the same ballpark.&lt;/p&gt;
&lt;p&gt;Grok is designed to be a top-tier frontier coding model. Gemini Flash, by contrast, is supposed to be a faster, cheaper model. The only non-Flash model from Google currently available is Gemini 3.1 Pro, which was released in February 2026, making it roughly 80 years old in AI years. Still, in my experience, it is not great as a coding companion. It frequently makes mistakes and runs quite slowly.&lt;/p&gt;
&lt;p&gt;Google does not have a publicly available Opus-level model yet. But it certainly has the compute available to train a gigantic new model if it chooses to do so. Given the hype around AI, that is almost certainly what it is working toward.&lt;/p&gt;
&lt;p&gt;The point is that Google did not ship Gemini Flash and call it a day. Its models are being worked on constantly. Eventually, it may produce something that feels truly Fable- or Sol-class.&lt;/p&gt;
&lt;h2 id=&quot;final-piece-monetization&quot;&gt;The final piece: monetization&lt;/h2&gt;
&lt;p&gt;Every other major AI company, from OpenAI to SpaceXAI and Anthropic, relies heavily on people paying subscriptions for AI services. They also have income from other sources, including government contracts and commercial customers. That income can be substantial.&lt;/p&gt;
&lt;p&gt;But people change providers. Governments come and go. If everyone stopped paying for Anthropic tomorrow, and if it lost its commercial contracts, where would the money come from? Investors will only last so long.&lt;/p&gt;
&lt;p&gt;These companies lack one thing that still prints money for Google: AdSense.&lt;/p&gt;
&lt;p&gt;AdSense pulls in a lazy $8 billion each quarter in revenue. That matters because, at this stage, &lt;a href=&quot;https://blog.logrocket.com/ai-dev-tool-rankings/&quot;&gt;AI companies have to develop models and then sell access at a loss&lt;/a&gt; to get people onboard. For example, I subscribed to Grok for three months for the price of one. Elon is going to lose money on that, guaranteed. The hope is that, after three months, I stay subscribed and pay the increased rate.&lt;/p&gt;
&lt;p&gt;With huge revenue and enormous compute, Google can afford to develop a true frontier model and sell access at a loss long enough for a substantial number of users to migrate. Whether individual users subscribe immediately or not, AdSense can help subsidize the cost for a long time. Without that kind of cash machine, competitors will have a much harder time eating the cost of training a model and subsidizing its use at Google’s scale.&lt;/p&gt;
&lt;p&gt;Then, possibly years down the track, after some bigger players have run out of money or the will to continue, Google can stop subsidizing its own AI and charge closer to what it actually costs, without worrying that someone else can keep outperforming it at a loss.&lt;/p&gt;
&lt;p&gt;Maybe that sounds a little dire. But think about how Google became the number one search engine in the world. That position was not handed to it. Google fought for it with billions of dollars in deals to make Google the default search engine, with the development of Chrome, and with payments to Apple to keep Google as the default search engine on iOS, to name just a few examples.&lt;/p&gt;
&lt;p&gt;We tend to trivialize the complexity of those deals and the scale of that strategy. You might drive around to a few different gas stations to find cheaper gas, but you probably do not bounce between search engines looking for results. You Google it, and if it is not there, you move on.&lt;/p&gt;
&lt;p&gt;Google has more of the web to train on than almost any other player. It has an OpenAI-level coding interface, or at least something moving in that direction. It has the AdSense money machine. The last missing piece is a truly killer model.&lt;/p&gt;
&lt;p&gt;If you Google something and it does not show up, you probably assume the answer is not worth finding. You do not go to Bing and try again. It is hard to imagine the same thing not happening with AI if Google can produce a model good enough to make that behavior stick.&lt;/p&gt;
&lt;p&gt;So, yes: Google won the search engine race, and it is not hard to imagine it winning the AI race, too. &lt;a href=&quot;https://blog.logrocket.com/why-are-ai-companies-buying-teams-behind-dev-tools/&quot;&gt;The broader consolidation happening across the AI industry&lt;/a&gt; only reinforces how high the stakes are for every player involved.&lt;/p&gt;
&lt;p&gt;In the meantime, we can all use some subsidized AI coding. That part, at least, is kind of neat.&lt;/p&gt;


&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/why-google-wins-ai-race/</link><guid isPermaLink="false">https://blog.logrocket.com/why-google-wins-ai-race/</guid><pubDate>Invalid Date</pubDate><author>Lewis Cianci</author></item><item><title>Best React scheduler component libraries</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/best-react-scheduler-component-libraries/#react-scheduler-library-choose&quot;&gt;Which React scheduler library should you choose?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;react-scheduler-library-choose&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/best-react-scheduler-component-libraries/#react-scheduler-component-library&quot;&gt;Why use a React scheduler component library?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;react-scheduler-component-library&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/best-react-scheduler-component-libraries/#kendoreact-scheduler&quot;&gt;KendoReact Scheduler&lt;/a&gt;
        &lt;ul data-toc-name=&quot;kendoreact-scheduler&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/best-react-scheduler-component-libraries/#syncfusion-react-scheduler&quot;&gt;Syncfusion React Scheduler&lt;/a&gt;
        &lt;ul data-toc-name=&quot;syncfusion-react-scheduler&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/best-react-scheduler-component-libraries/#dhtmlx-scheduler&quot;&gt;DHTMLX Scheduler&lt;/a&gt;
        &lt;ul data-toc-name=&quot;dhtmlx-scheduler&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/best-react-scheduler-component-libraries/#bryntum&quot;&gt;Bryntum&lt;/a&gt;
        &lt;ul data-toc-name=&quot;bryntum&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/best-react-scheduler-component-libraries/#react-big-calendar&quot;&gt;react-big-calendar&lt;/a&gt;
        &lt;ul data-toc-name=&quot;react-big-calendar&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/best-react-scheduler-component-libraries/#fullcalendar&quot;&gt;FullCalendar&lt;/a&gt;
        &lt;ul data-toc-name=&quot;fullcalendar&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/best-react-scheduler-component-libraries/#schedule-x&quot;&gt;Schedule-X&lt;/a&gt;
        &lt;ul data-toc-name=&quot;schedule-x&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/best-react-scheduler-component-libraries/#mui-x-scheduler&quot;&gt;MUI X Scheduler&lt;/a&gt;
        &lt;ul data-toc-name=&quot;mui-x-scheduler&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/best-react-scheduler-component-libraries/#daypilot-react-scheduler&quot;&gt;DayPilot React Scheduler&lt;/a&gt;
        &lt;ul data-toc-name=&quot;daypilot-react-scheduler&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/best-react-scheduler-component-libraries/#calendarkit&quot;&gt;CalendarKit&lt;/a&gt;
        &lt;ul data-toc-name=&quot;calendarkit&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/best-react-scheduler-component-libraries/#planby&quot;&gt;Planby&lt;/a&gt;
        &lt;ul data-toc-name=&quot;planby&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/best-react-scheduler-component-libraries/#react-scheduler-library-comparison-table&quot;&gt;React scheduler library comparison table&lt;/a&gt;
        &lt;ul data-toc-name=&quot;react-scheduler-library-comparison-table&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Editor’s note:&lt;/strong&gt; This article was updated by &lt;a href=&quot;https://blog.logrocket.com/author/isaacjunior/&quot;&gt;Isaac Okoro&lt;/a&gt; on 6 August 2026 to refresh library details, pricing/licensing notes, React support, and guidance on choosing the right React scheduler component library.&lt;/em&gt;&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;895&quot; height=&quot;597&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/05/Best-React-scheduler-component-libraries.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;Best React Scheduler Component Libraries&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2023/05/Best-React-scheduler-component-libraries.png 895w, https://blog.logrocket.com/wp-content/uploads/2023/05/Best-React-scheduler-component-libraries-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2023/05/Best-React-scheduler-component-libraries-768x512.png 768w&quot; sizes=&quot;(max-width: 895px) 100vw, 895px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;React remains a default choice for building stateful, dynamic web applications. But as an app becomes more complex, scheduling features can quickly become difficult to build and maintain from scratch.&lt;/p&gt;
&lt;p&gt;A basic calendar is one thing. A production scheduler usually needs multiple views, recurring events, drag-and-drop editing, resource planning, timezone handling, localization, accessibility support, and strong performance with large event datasets. A React scheduler component library gives you these patterns as pre-built components, so you can focus on the application logic around them.&lt;/p&gt;
&lt;p&gt;In this article, we’ll compare some of the best React scheduler component libraries based on features, ease of use, performance, pricing, and ideal use cases. To get the most out of this article, you should have a basic understanding of how to &lt;a href=&quot;https://react.dev/learn&quot;&gt;set up and run a React project&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;react-scheduler-library-choose&quot;&gt;Which React scheduler library should you choose?&lt;/h2&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;Here’s a quick starting point if you already know the type of scheduling experience you need:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Recommended options&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple event calendar, free&lt;/td&gt;
&lt;td&gt;FullCalendar core or react-big-calendar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Booking or appointment flow for a smaller app&lt;/td&gt;
&lt;td&gt;CalendarKit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise scheduling as part of a broader component suite&lt;/td&gt;
&lt;td&gt;KendoReact Scheduler or Syncfusion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource planning at scale with a one-time license&lt;/td&gt;
&lt;td&gt;DHTMLX Scheduler for React&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource or shift planning with a free tier to start&lt;/td&gt;
&lt;td&gt;DayPilot React Scheduler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource-heavy planning with strong grid performance&lt;/td&gt;
&lt;td&gt;Bryntum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduler for an existing Material UI app&lt;/td&gt;
&lt;td&gt;MUI X Scheduler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lightweight, modern FullCalendar alternative&lt;/td&gt;
&lt;td&gt;Schedule-X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TV guide, streaming schedule, or general timeline UI&lt;/td&gt;
&lt;td&gt;Planby&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;If you need a free, general-purpose calendar, start with FullCalendar or react-big-calendar. If you need a full enterprise scheduler, evaluate KendoReact, Syncfusion, DHTMLX, Bryntum, or DayPilot. If your app already uses Material UI, MUI X Scheduler is worth watching closely. For more specialized flows, CalendarKit, Schedule-X, and Planby may be better fits.&lt;/p&gt;
&lt;h2 id=&quot;react-scheduler-component-library&quot;&gt;Why use a React scheduler component library?&lt;/h2&gt;
&lt;p&gt;React scheduler component libraries help developers add scheduling features without rebuilding common calendar interactions from the ground up. They are especially useful when your app needs more than a basic date picker.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Time savings&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Scheduler libraries provide pre-built components, views, and editing flows for common scheduling patterns, including day, week, month, timeline, and agenda views. This can reduce development time and let your team focus on domain-specific logic rather than calendar infrastructure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Many scheduler libraries are designed to handle large event datasets through virtualization, lazy loading, or optimized rendering. This matters for applications that need to show many events, long date ranges, or resource-heavy timelines.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;User experience&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Schedulers usually include familiar interactions such as drag-and-drop editing, event resizing, recurring events, keyboard navigation, and responsive layouts. These patterns are difficult to implement well from scratch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Customization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most React scheduler libraries support theming, localization, custom event rendering, and configurable date formats. This helps you adapt the scheduler to your product instead of forcing your users into a generic calendar UI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cross-platform support&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Some scheduler libraries are React-only, while others support multiple frameworks such as Angular, Vue, Blazor, Svelte, or vanilla JavaScript. If your organization works across multiple frontend stacks, cross-framework support may be an important selection factor.&lt;/p&gt;
&lt;h2 id=&quot;kendoreact-scheduler&quot;&gt;KendoReact Scheduler&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.telerik.com/kendo-react-ui/scheduler&quot;&gt;KendoReact Scheduler&lt;/a&gt; is a feature-rich React scheduler component from Progress Telerik. It provides out-of-the-box support for timezone conversion, globalization, resource visualization, event grouping, recurring events, and editing:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/05/KendoReact-Scheduler-example.png&quot; alt=&quot;KendoReact Scheduler Example&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;KendoReact Scheduler brings Outlook-style scheduling functionality into a single React component. It is best suited for enterprise teams that already use KendoReact or need a broad commercial component suite.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;KendoReact Scheduler includes day, week, work week, month, timeline, and agenda views. It also supports multiple event selection, resource visualization, time zone conversion, grouping, recurring events, and editing. Localization and globalization support make it suitable for applications with international users.&lt;/p&gt;
&lt;p&gt;The component includes a built-in recurrence editor, so users can configure daily, weekly, monthly, or annual recurring events. It also supports data binding from arrays of data items.&lt;/p&gt;

&lt;p&gt;KendoReact is a paid suite. A license key is required for trial and paid usage, and pricing is suite-based. Check the Telerik pricing page before budgeting because licensing details can change.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ease of use&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;KendoReact provides strong documentation, samples, demos, tutorials, and virtual classroom resources. The scheduler has a large API surface, with classes such as &lt;code&gt;AgendaView&lt;/code&gt;, &lt;code&gt;DayView&lt;/code&gt;, and &lt;code&gt;MonthView&lt;/code&gt;. The interface can also be styled with &lt;a href=&quot;https://www.telerik.com/themebuilder&quot;&gt;Progress ThemeBuilder&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;KendoReact Scheduler is built specifically for React and supports efficient data binding, so the scheduler can update when the data source changes without reloading the entire component. Accessibility support includes WAI-ARIA attributes, keyboard navigation, and RTL mode.&lt;/p&gt;
&lt;h2 id=&quot;syncfusion-react-scheduler&quot;&gt;Syncfusion React Scheduler&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.syncfusion.com/&quot;&gt;Syncfusion&lt;/a&gt; offers a feature-rich &lt;a href=&quot;https://www.syncfusion.com/react-components/react-scheduler&quot;&gt;React scheduler component&lt;/a&gt; as part of its broader UI component suite:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/05/Syncfusion-React-Scheduler-example.png&quot; alt=&quot;Syncfusion React Scheduler Example&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;Syncfusion is a good fit for teams that want an enterprise scheduler along with a larger set of UI components across React and other frameworks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Syncfusion Scheduler supports day, week, work week, month, agenda, year, and timeline views. It also includes recurring events, globalization, localization, and data binding from sources such as JSON and RESTful services.&lt;/p&gt;
&lt;p&gt;Syncfusion supports responsive layouts across desktop and mobile devices. It also has component suites for Angular, Vue, Blazor, and other platforms.&lt;/p&gt;
&lt;p&gt;Syncfusion is a paid product, but it offers a free community license for qualifying individuals and small businesses, along with a free trial.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ease of use&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Syncfusion provides detailed documentation, samples, and demos. Its API is straightforward for common scheduling tasks, and the broader Syncfusion ecosystem can be useful if your team already uses the vendor’s components elsewhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Syncfusion uses techniques such as virtual scrolling and on-demand loading to reduce rendering cost for larger schedules. It is suitable for apps that need multiple views, external data binding, and enterprise-level customization.&lt;/p&gt;
&lt;h2 id=&quot;dhtmlx-scheduler&quot;&gt;DHTMLX Scheduler&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://dhtmlx.com/docs/products/dhtmlxScheduler/&quot;&gt;DHTMLX Scheduler&lt;/a&gt; is a mature JavaScript calendar scheduler with an official React wrapper. It supports drag-and-drop scheduling, recurring events, customizable views, and other features for building scheduling applications:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/05/DHTMLX-Scheduler.png&quot; alt=&quot;DHTMLX Scheduler Example&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;DHTMLX Scheduler is a paid tool with a free trial. It is useful for teams that prefer a one-time commercial license instead of a subscription-based component suite.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DHTMLX Scheduler includes day, week, month, and year views, along with drag-and-drop event scheduling and recurrence support. It also supports globalization and localization, making it easier to customize date formats and language settings.&lt;/p&gt;
&lt;p&gt;DHTMLX works across modern browsers and integrates with frameworks and platforms such as React, Angular, Vue, Node.js, jQuery, PHP, Laravel, Ruby on Rails, and Salesforce.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ease of use&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DHTMLX Scheduler has detailed documentation and a large API with methods, properties, and events for configuring behavior and appearance. That flexibility is useful for complex requirements, though it can also mean a steeper learning curve than simpler React-only calendars.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DHTMLX Scheduler is designed for modern browsers and includes accessibility support such as WAI-ARIA attributes, keyboard navigation, high-contrast themes, and RTL mode.&lt;/p&gt;
&lt;h2 id=&quot;bryntum&quot;&gt;Bryntum&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://bryntum.com/products/react-scheduler/&quot;&gt;Bryntum Scheduler&lt;/a&gt; is a commercial scheduling component for React with a large feature set and a performance-oriented rendering model:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/05/Bryntum-React-Scheduler.png&quot; alt=&quot;Bryntum React Scheduler Example&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;Bryntum is best suited for complex scheduling, resource planning, and timeline-heavy applications.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Bryntum includes drag-and-drop scheduling, event resizing, exporting to PDF, PNG, and Excel, JSX rendering in events and cells, sortable entries, and built-in themes. It is a paid product, and pricing depends on the license type and usage model.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ease of use&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Bryntum provides documentation, tutorials, and demos for React. The API is broad, so it is most useful when your app needs advanced scheduling behavior rather than a simple event calendar.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Bryntum focuses heavily on performance through virtual rendering, optimized DOM updates, and efficient data handling. This makes it a strong option for demanding scheduling interfaces with many events or resources.&lt;/p&gt;
&lt;h2 id=&quot;react-big-calendar&quot;&gt;react-big-calendar&lt;/h2&gt;
&lt;p&gt;For teams looking for a free React scheduler component library, &lt;a href=&quot;https://github.com/jquense/react-big-calendar&quot;&gt;react-big-calendar&lt;/a&gt; is a popular open source option. Instead of a traditional table-based layout, &lt;a href=&quot;https://blog.logrocket.com/how-to-build-a-basic-flexbox-layout-a-tutorial-with-examples/&quot;&gt;it uses flexbox&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/05/react-big-calendar-example.png&quot; alt=&quot;React Big Calendar Example&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;react-big-calendar includes day, week, month, and agenda views. It supports drag-and-drop scheduling, event resizing, customizable tooltips, localization, and theming.&lt;/p&gt;
&lt;p&gt;The project also has community support, including a &lt;a href=&quot;https://github.com/jquense/react-big-calendar?tab=readme-ov-file#join-the-community&quot;&gt;Slack community&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ease of use&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;react-big-calendar is straightforward to set up and has documentation and demos for common calendar use cases. It is a good fit for teams that need a free, React-specific event calendar and do not need enterprise resource-planning features.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;react-big-calendar can work well for everyday calendar interfaces, but teams building very large resource schedules should test it against their expected event volume before committing.&lt;/p&gt;
&lt;h2 id=&quot;fullcalendar&quot;&gt;FullCalendar&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://fullcalendar.io/docs&quot;&gt;FullCalendar&lt;/a&gt; is one of the most widely used calendar libraries in the JavaScript ecosystem. It has an official React connector and a broad feature set, including customizable and draggable events. You can get started with &lt;a href=&quot;https://fullcalendar.io/pricing&quot;&gt;FullCalendar’s core version&lt;/a&gt; for free:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/05/FullCalendar-example.png&quot; alt=&quot;FullCalendar Example&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;FullCalendar provides day, week, month, list, and agenda-style views. It accepts React JSX for rendering nested content and integrates with React, Vue, Angular, vanilla JavaScript, and TypeScript. It also supports theming, Bootstrap 5 compatibility, and integrations with iCal and Google Calendar.&lt;/p&gt;
&lt;p&gt;FullCalendar’s core package is MIT-licensed. Premium plugins add features such as resource timelines and advanced views.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ease of use&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;FullCalendar has extensive documentation, examples, and a large community. This makes it one of the safer picks for teams that want a general-purpose event calendar with plenty of reference material.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;FullCalendar is optimized for common event-calendar use cases and can fetch event data on demand. If you need resource timelines or large scheduling grids, review its Premium plugins and test against your data size.&lt;/p&gt;
&lt;h2 id=&quot;schedule-x&quot;&gt;Schedule-X&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://schedule-x.dev/&quot;&gt;Schedule-X&lt;/a&gt; is a modern calendar and date picker library for web applications. It offers material-style calendar components and works well as a lightweight alternative to larger scheduler suites:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/05/Schedule-X-example.png&quot; alt=&quot;Schedule-X Scheduler Example&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Schedule-X supports multiple calendar views, including day, week, and month. It also includes time pickers, dark mode, recurring events, and accessibility-focused UI patterns.&lt;/p&gt;
&lt;p&gt;As of Schedule-X v4, the library draws a clearer line between the free and Premium tiers. Drag-and-drop and event resizing, which were previously listed as free features in older versions of this article, now live behind Schedule-X Premium, alongside the Time Grid Resource View. Premium is sold per project based on team size, with yearly subscription and lifetime license options.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ease of use&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Schedule-X has a clean API and approachable documentation. It is a good fit for teams that want a modern, lightweight calendar without adopting a larger enterprise component suite.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Schedule-X is designed to be lightweight and responsive, making it suitable for many everyday scheduling use cases. As with any scheduler, test it with your expected event volume if your app needs dense calendars or long timelines.&lt;/p&gt;
&lt;h2 id=&quot;mui-x-scheduler&quot;&gt;MUI X Scheduler&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://mui.com/x/react-scheduler/&quot;&gt;MUI X Scheduler&lt;/a&gt; is a newer option from the Material UI team. It is worth including because Material UI is widely used in production React codebases, though the Scheduler is still early-stage compared with more mature libraries.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_14EEB5DE0B8786310ED6739C00D30E91670E67D70017B0A21B01C76C9054C970_1784291426658_Screenshot+2026-07-17+at+13.30.15.png&quot; alt=&quot;MUI X Scheduler Example&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The Community tier covers multiple views, resource support, drag and drop, resizing, and theming. The Premium tier adds features such as recurring events, exception dates, DST-aware recurrence, lazy loading through data sources, and resource timeline capabilities.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ease of use&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If your app already uses Material UI, MUI X Scheduler should be relatively low-friction. It uses the same design system, theming conventions, and component patterns as the rest of the MUI ecosystem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MUI X Scheduler builds on the virtualization patterns used across other MUI X components. The Community tier is suitable for many everyday scheduling interfaces, while teams working with very large event datasets or long date ranges should evaluate the Premium data-source and timeline features.&lt;/p&gt;
&lt;p&gt;Because the scheduler is still early-stage, expect some API changes before it reaches the same level of maturity as older scheduler libraries.&lt;/p&gt;
&lt;h2 id=&quot;daypilot-react-scheduler&quot;&gt;DayPilot React Scheduler&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.daypilot.org/react/&quot;&gt;DayPilot React Scheduler&lt;/a&gt; provides calendar and scheduler components for React, including a free Lite version and a paid Pro version.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DayPilot Lite for React covers basic scheduler use cases, including horizontal timelines, drag-and-drop support, and customizable appearance. DayPilot Pro adds more advanced scheduling features, such as progressive rendering, larger timeline support, event editing, image and PDF export, RTL support, and additional UI controls.&lt;/p&gt;
&lt;p&gt;DayPilot is a good fit for resource scheduling, shift planning, and timeline-style interfaces where a horizontal scheduler is more useful than a traditional month calendar.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ease of use&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DayPilot provides React-specific documentation and examples. It is not as broadly discussed as FullCalendar or react-big-calendar, but the Lite version gives developers a practical way to test the scheduler model before moving to a paid plan.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DayPilot Pro is designed for larger scheduling datasets through features such as progressive rendering. If performance across many resources or long timelines is central to your project, test the Lite and Pro versions with realistic data before choosing a license.&lt;/p&gt;
&lt;h2 id=&quot;calendarkit&quot;&gt;CalendarKit&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.calendarkit.io/&quot;&gt;CalendarKit&lt;/a&gt; is a newer, lightweight, TypeScript-first calendar aimed at booking and appointment flows.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_14EEB5DE0B8786310ED6739C00D30E91670E67D70017B0A21B01C76C9054C970_1784292885155_image.png&quot; alt=&quot;CalendarKit Scheduler Example&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The basic tier ships as &lt;code&gt;@calendarkit/react&lt;/code&gt;, with a &lt;code&gt;BasicScheduler&lt;/code&gt; component that can render events from an array of event objects. It includes TypeScript support, dark mode, and a simple drop-in scheduler experience.&lt;/p&gt;&lt;hr&gt;
&lt;iframe id=&quot;podrocket-plug&quot; src=&quot;https://open.spotify.com/embed/playlist/4j70ZVQNj5Lk3gtyCG5VWk?utm_source=generator&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot; allow=&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; loading=&quot;lazy&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;/iframe&gt;
&lt;hr&gt;
&lt;style&gt;
#podrocket-plug {
  border-radius: 12px;
  width: 75%;
  height: 352px;
  margin: 1rem auto;
  display: block;
}

&lt;/style&gt;
&lt;p&gt;CalendarKit also supports deeper customization through props such as &lt;code&gt;renderEventForm&lt;/code&gt;, which lets you replace the default event creation and editing modal with your own UI.&lt;/p&gt;
&lt;p&gt;The Pro tier adds more production-oriented features, including recurring events and full timezone handling. CalendarKit currently targets React and Next.js; support for Vue, Angular, Svelte, and Solid is planned under a future Universal license but is not available yet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ease of use&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;CalendarKit is quick to get running and is especially appealing for booking or appointment flows. It is less suitable if you need a cross-framework scheduler today.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;CalendarKit provides pre-built timeline, day, and week interfaces, which can reduce the amount of custom layout, scrolling, and date-math work your team needs to build. As with the other newer options, test it with realistic event density before using it for a large production scheduler.&lt;/p&gt;
&lt;h2 id=&quot;planby&quot;&gt;Planby&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://planby.app/&quot;&gt;Planby&lt;/a&gt; started as an Electronic Program Guide, or TV/streaming schedule, library and has since repositioned as a more general timeline scheduler. Its channels-and-programs model can map well to resources, bookings, shift planning, or conference agendas.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_14EEB5DE0B8786310ED6739C00D30E91670E67D70017B0A21B01C76C9054C970_1784293888184_Screenshot+2026-07-17+at+14.11.22.png&quot; alt=&quot;Planby Timeline Scheduler Example&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Planby’s core offering uses custom virtual rendering for large datasets and supports timezone-aware scheduling. Its commercial tier adds drag and drop, resizing, advanced grid modes, and priority support.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ease of use&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Planby is built in React and TypeScript, with documentation and live examples. Its API is more specialized than the general-purpose calendars on this list, so it is best for timeline-style interfaces rather than ordinary event calendars.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Planby is designed around virtualized rendering, drawing only the events and channels currently visible on the screen. This makes it a stronger fit for timeline-heavy applications than for simple calendar widgets.&lt;/p&gt;
&lt;h2 id=&quot;react-scheduler-library-comparison-table&quot;&gt;React scheduler library comparison table&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;License/pricing&lt;/th&gt;
&lt;th&gt;React support&lt;/th&gt;
&lt;th&gt;Best use case&lt;/th&gt;
&lt;th&gt;Other framework support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;KendoReact Scheduler&lt;/td&gt;
&lt;td&gt;Paid suite; trial available&lt;/td&gt;
&lt;td&gt;Native React, frequent releases&lt;/td&gt;
&lt;td&gt;Enterprise teams already on KendoReact&lt;/td&gt;
&lt;td&gt;Angular, Vue, and jQuery through separate Kendo UI suites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Syncfusion React Scheduler&lt;/td&gt;
&lt;td&gt;Paid suite; free community license for qualifying small teams&lt;/td&gt;
&lt;td&gt;Actively maintained React support&lt;/td&gt;
&lt;td&gt;Multi-framework enterprise suites&lt;/td&gt;
&lt;td&gt;Angular, Vue, Blazor, and more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DHTMLX Scheduler for React&lt;/td&gt;
&lt;td&gt;Paid, one-time license; trial available&lt;/td&gt;
&lt;td&gt;Official React wrapper&lt;/td&gt;
&lt;td&gt;Enterprise scheduling with one-time licensing&lt;/td&gt;
&lt;td&gt;Angular, Vue, Node.js, jQuery, PHP, Laravel, Ruby on Rails, and Salesforce&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bryntum Scheduler&lt;/td&gt;
&lt;td&gt;Paid; pricing varies by license&lt;/td&gt;
&lt;td&gt;Official React wrapper&lt;/td&gt;
&lt;td&gt;Complex resource planning&lt;/td&gt;
&lt;td&gt;Angular, Vue, Ionic, Salesforce, and vanilla JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;react-big-calendar&lt;/td&gt;
&lt;td&gt;Free, MIT&lt;/td&gt;
&lt;td&gt;React-focused&lt;/td&gt;
&lt;td&gt;Budget event calendars&lt;/td&gt;
&lt;td&gt;None, React only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FullCalendar&lt;/td&gt;
&lt;td&gt;MIT core; paid Premium plugins&lt;/td&gt;
&lt;td&gt;Official React connector&lt;/td&gt;
&lt;td&gt;General-purpose event calendars&lt;/td&gt;
&lt;td&gt;Vue, Angular, vanilla JavaScript, and web component support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schedule-X&lt;/td&gt;
&lt;td&gt;Free core; Premium paid&lt;/td&gt;
&lt;td&gt;React support plus cross-framework packages&lt;/td&gt;
&lt;td&gt;Lightweight FullCalendar alternative&lt;/td&gt;
&lt;td&gt;Angular, Vue, Svelte, and Preact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MUI X Scheduler&lt;/td&gt;
&lt;td&gt;Community and Premium tiers&lt;/td&gt;
&lt;td&gt;React, aligned with MUI X&lt;/td&gt;
&lt;td&gt;Material UI apps&lt;/td&gt;
&lt;td&gt;None, React only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DayPilot React Scheduler&lt;/td&gt;
&lt;td&gt;Lite free; Pro paid&lt;/td&gt;
&lt;td&gt;Documented React integration&lt;/td&gt;
&lt;td&gt;Resource and shift planning&lt;/td&gt;
&lt;td&gt;Angular, Vue, jQuery, AngularJS, plus server-side editions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CalendarKit&lt;/td&gt;
&lt;td&gt;Free basic; Pro paid&lt;/td&gt;
&lt;td&gt;React 18+ and Next.js&lt;/td&gt;
&lt;td&gt;Booking flows and MVPs&lt;/td&gt;
&lt;td&gt;None yet; additional frameworks planned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Planby&lt;/td&gt;
&lt;td&gt;Free core with commercial tier&lt;/td&gt;
&lt;td&gt;React and TypeScript&lt;/td&gt;
&lt;td&gt;EPG, TV guides, and general timelines&lt;/td&gt;
&lt;td&gt;None, React only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;Pricing above reflects publicly listed information as of this update. Check each vendor’s site before budgeting, since pricing and feature tiers can change.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;React scheduler component libraries save development time by providing calendar views, recurring event logic, editing interactions, localization, accessibility support, and performance optimizations that are difficult to rebuild from scratch.&lt;/p&gt;
&lt;p&gt;The best option depends on what you’re building. FullCalendar and react-big-calendar are strong starting points for free, general-purpose event calendars. KendoReact, Syncfusion, DHTMLX, Bryntum, and DayPilot are better fits for enterprise scheduling and resource planning. MUI X Scheduler is a natural option for teams already invested in Material UI, while Schedule-X, CalendarKit, and Planby are useful for more specialized calendar and timeline interfaces.&lt;/p&gt;
&lt;p&gt;Before choosing a scheduler, test it with your real data shape, expected event volume, accessibility requirements, and licensing constraints. A scheduler can look simple in a demo, but production requirements such as recurring events, timezone handling, resource views, and drag-and-drop editing often determine whether the library is actually a good fit.&lt;/p&gt;



&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/2026/&quot;&gt;#2026&lt;/a&gt;
        &lt;/li&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/react/&quot;&gt;#react&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/best-react-scheduler-component-libraries/</link><guid isPermaLink="false">https://blog.logrocket.com/best-react-scheduler-component-libraries/</guid><pubDate>Invalid Date</pubDate><author>Frank Joseph</author></item><item><title>How to build a real time voice AI agent in the browser</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#what-we-are-building&quot;&gt;What does this voice AI agent&#39;s tech stack include?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-we-are-building&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#how-the-stack-fits-together&quot;&gt;How does Transformers.js run speech-to-text and an LLM in the same browser tab?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-the-stack-fits-together&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#the-end-to-end-flow&quot;&gt;The end-to-end flow&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-end-to-end-flow&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#speech-to-text-with-whisper&quot;&gt;How do you run Whisper speech-to-text in the browser?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;speech-to-text-with-whisper&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#loading-the-model&quot;&gt;Loading the model&lt;/a&gt;
        &lt;ul data-toc-name=&quot;loading-the-model&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#selecting-a-model-size&quot;&gt;Selecting a model size&lt;/a&gt;
        &lt;ul data-toc-name=&quot;selecting-a-model-size&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#chunking-mic-audio&quot;&gt;Chunking mic audio&lt;/a&gt;
        &lt;ul data-toc-name=&quot;chunking-mic-audio&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#streaming-interim-transcripts-without-freezing-the-ui&quot;&gt;Streaming interim transcripts without freezing the UI&lt;/a&gt;
        &lt;ul data-toc-name=&quot;streaming-interim-transcripts-without-freezing-the-ui&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#running-the-local-llm&quot;&gt;How do you run a local LLM in the browser for voice responses?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;running-the-local-llm&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#loading-the-model-2&quot;&gt;Loading the model&lt;/a&gt;
        &lt;ul data-toc-name=&quot;loading-the-model-2&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#building-the-prompt&quot;&gt;Building the prompt&lt;/a&gt;
        &lt;ul data-toc-name=&quot;building-the-prompt&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#streaming-tokens-without-blocking-the-main-thread&quot;&gt;Streaming tokens without blocking the main thread&lt;/a&gt;
        &lt;ul data-toc-name=&quot;streaming-tokens-without-blocking-the-main-thread&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#the-wasm-fallback&quot;&gt;The WASM fallback&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-wasm-fallback&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#text-to-speech-that-stays-local&quot;&gt;How do you keep browser text-to-speech fully local?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;text-to-speech-that-stays-local&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#the-voice-filtering-problem&quot;&gt;The voice filtering problem&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-voice-filtering-problem&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#speaking-a-sentence&quot;&gt;Speaking a sentence&lt;/a&gt;
        &lt;ul data-toc-name=&quot;speaking-a-sentence&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#queue-management&quot;&gt;Queue management&lt;/a&gt;
        &lt;ul data-toc-name=&quot;queue-management&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#handling-interruptions&quot;&gt;Handling interruptions&lt;/a&gt;
        &lt;ul data-toc-name=&quot;handling-interruptions&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#managing-conversational-state&quot;&gt;How do you prevent a voice agent from hearing itself?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;managing-conversational-state&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#the-three-states&quot;&gt;The three states&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-three-states&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#gating-the-mic&quot;&gt;Gating the mic&lt;/a&gt;
        &lt;ul data-toc-name=&quot;gating-the-mic&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#wiring-the-transitions&quot;&gt;Wiring the transitions&lt;/a&gt;
        &lt;ul data-toc-name=&quot;wiring-the-transitions&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#handling-interruptions-within-the-state-machine&quot;&gt;Handling interruptions within the state machine&lt;/a&gt;
        &lt;ul data-toc-name=&quot;handling-interruptions-within-the-state-machine&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#the-complete-implementation&quot;&gt;Where can you see the complete voice AI agent implementation?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-complete-implementation&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/voice-ai-agent-browser/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;Most voice AI architectures chain together three distinct external APIs: speech-to-text, an LLM, and text-to-speech. That setup means three separate network round trips, three vendors, extra points of failure, and a frustrating 2–3 second latency before the agent responds, not to mention shipping raw audio off to third-party servers.&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;895&quot; height=&quot;597&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/How-to-build-a-real-time-voice-AI-agent-in-the-browser.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/How-to-build-a-real-time-voice-AI-agent-in-the-browser.png 895w, https://blog.logrocket.com/wp-content/uploads/2026/08/How-to-build-a-real-time-voice-AI-agent-in-the-browser-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/08/How-to-build-a-real-time-voice-AI-agent-in-the-browser-768x512.png 768w&quot; sizes=&quot;(max-width: 895px) 100vw, 895px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;In this guide, you’ll build a voice agent that eliminates every single one of those network hops. Speech recognition, reasoning, and synthesis all execute right inside the browser tab using Transformers.js v4 powered by WebGPU (with an automatic fallback to WASM when WebGPU isn’t available). Everything- audio inputs, transcripts, and model outputs remains completely local.&lt;/p&gt;
&lt;p&gt;Thanks to WebGPU, modern browsers are finally fast enough to run a compact Whisper model alongside a quantized LLM for real-time conversational flow. We’ll cover how to stitch these three stages together, gracefully handle lower-spec hardware, and manage system state so your agent never talks over itself.&lt;/p&gt;
&lt;h2 id=&quot;what-we-are-building&quot;&gt;What does this voice AI agent’s tech stack include?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;STT&lt;/strong&gt;: A quantized &lt;a href=&quot;https://blog.logrocket.com/using-whisper-speech-recognition-react-native/&quot;&gt;Whisper&lt;/a&gt; model executed directly via the Transformers.js pipeline API&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM&lt;/strong&gt;: A compact, quantized language model running on the exact same runtime and backend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTS&lt;/strong&gt;: The browser’s built-in &lt;code&gt;SpeechSynthesis&lt;/code&gt; API, strictly filtered to local, on-device voices&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the end, you’ll have a working agent and a clear idea of what running everything locally means for latency, model size, and browser support.&lt;/p&gt;
&lt;p class=&quot;codepen&quot; style=&quot;height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot; data-height=&quot;300&quot; data-pen-title=&quot;Real-time voice agent &quot; data-default-tab=&quot;html,result&quot; data-slug-hash=&quot;OPWZvyG&quot; data-user=&quot;JudeIV&quot;&gt;See the Pen &lt;a href=&quot;https://codepen.io/JudeIV/pen/OPWZvyG&quot;&gt;&lt;br&gt;
Real-time voice agent &lt;/a&gt; by Miracle Jude (&lt;a href=&quot;https://codepen.io/JudeIV&quot;&gt;@JudeIV&lt;/a&gt;)&lt;br&gt;
on &lt;a href=&quot;https://codepen.io/&quot;&gt;CodePen&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Launch the app in a WebGPU-capable desktop browser like Chrome or Edge. The initial load downloads around 400MB of model weights, which are cached locally for future runs. Grant microphone access, wait for the models to load, and start speaking.&lt;/p&gt;
&lt;h2 id=&quot;how-the-stack-fits-together&quot;&gt;How does Transformers.js run speech-to-text and an LLM in the same browser tab?&lt;/h2&gt;
&lt;p&gt;Although the pipeline relies on three distinct components, they all operate within a single unified runtime powered by Transformers.js.&lt;/p&gt;
&lt;p&gt;Transformers.js brings Hugging Face’s core libraries directly into JavaScript. Built on ONNX Runtime Web, version 4 uses &lt;a href=&quot;https://blog.logrocket.com/webgpu-accelerate-ml-workloads-browser/&quot;&gt;WebGPU&lt;/a&gt; as its primary acceleration engine with a &lt;a href=&quot;https://podrocket.logrocket.com/wasm3-andreas-rossberg&quot;&gt;WASM&lt;/a&gt; fallback. Because both Whisper and the LLM share the exact same &lt;code&gt;pipeline()&lt;/code&gt; API, you get to skip the headache of managing separate engines or disparate API abstractions.&lt;/p&gt;
&lt;h3 id=&quot;the-end-to-end-flow&quot;&gt;The end-to-end flow&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Mic capture&lt;/strong&gt;: Raw audio is captured via the Web Audio API, sliced into short buffers, and pushed to Whisper&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real-time transcription&lt;/strong&gt;: Whisper transcribes incoming chunks, streaming interim text updates while the user speaks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Turn transition&lt;/strong&gt;: Once the user stops talking (triggered via silence detection or an explicit cutoff), the full transcript is passed to the local LLM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token generation &amp;amp; speech&lt;/strong&gt;: The LLM streams tokens, handing off completed sentences to the &lt;code&gt;SpeechSynthesis&lt;/code&gt; API&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Feedback loop protection&lt;/strong&gt;: The microphone remains muted while &lt;code&gt;SpeechSynthesis&lt;/code&gt; speaks, preventing the agent from hearing and transcribing its own voice&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This loop control is more critical than it looks. A voice agent isn’t just three independent stages; it’s a closed loop with a strict state gate at each transition. Mic capture must pause before TTS begins, and TTS must fully wrap up before the mic reopens. If you miss this step, the agent ends up listening to itself, and the entire flow breaks down fast.&lt;/p&gt;
&lt;p&gt;WebGPU handles the heavy lifting for both Whisper and the LLM, giving you direct access to GPU power right inside the browser. That compute is what makes running a local language model at conversational speeds possible without a server backend. If a user’s device lacks WebGPU support, Transformers.js automatically degrades to WASM, so both models still function, just at a slower pace. It’s important to understand how much slower before going live, which we’ll break down with real benchmark numbers next.&lt;/p&gt;
&lt;h2 id=&quot;speech-to-text-with-whisper&quot;&gt;How do you run Whisper speech-to-text in the browser?&lt;/h2&gt;
&lt;p&gt;Whisper takes care of transcription, but this stage can be tricky on lower-end hardware. Picking a model that is too large causes high latency, while picking one that is too small leads to transcription errors.&lt;/p&gt;
&lt;h3 id=&quot;loading-the-model&quot;&gt;Loading the model&lt;/h3&gt;
&lt;p&gt;Transformers.js exposes Whisper through the &lt;code&gt;automatic-speech-recognition&lt;/code&gt; pipeline. Pass &lt;code&gt;device: &#39;webgpu&#39;&lt;/code&gt; explicitly in the options to enforce GPU execution rather than letting the runtime guess:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;import { pipeline } from &#39;@huggingface/transformers&#39;;

const transcriber = await pipeline(
  &#39;automatic-speech-recognition&#39;,
  &#39;onnx-community/whisper-base&#39;,
  {
    device: &#39;webgpu&#39;,
    dtype: &#39;q4&#39;, // quantized weights, smaller download, faster inference
  }
);&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;dtype&lt;/code&gt; option is what makes this setup practical for the web. Standard full-precision Whisper weights balloon into hundreds of megabytes, but when quantized to 4-bit (&lt;code&gt;q4&lt;/code&gt;), a model like &lt;code&gt;whisper-base&lt;/code&gt; drops to roughly 40MB, small enough to fetch quickly and cache in the browser for instant reloads.&lt;/p&gt;
&lt;h3 id=&quot;selecting-a-model-size&quot;&gt;Selecting a model size&lt;/h3&gt;
&lt;p&gt;Whisper comes in five standard sizes: &lt;code&gt;tiny&lt;/code&gt;, &lt;code&gt;base&lt;/code&gt;, &lt;code&gt;small&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, and &lt;code&gt;large&lt;/code&gt;. For real-time conversational agents, stick strictly to &lt;code&gt;tiny&lt;/code&gt; or &lt;code&gt;base&lt;/code&gt;. Anything larger introduces too much processing latency on consumer devices, breaking the flow of natural conversation.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Params&lt;/th&gt;
&lt;th&gt;Quantized size (q4)&lt;/th&gt;
&lt;th&gt;Accuracy&lt;/th&gt;
&lt;th&gt;Real-time on mid-range GPU&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;tiny&lt;/td&gt;
&lt;td&gt;39M&lt;/td&gt;
&lt;td&gt;~15MB&lt;/td&gt;
&lt;td&gt;Noticeably rougher on accents, background noise&lt;/td&gt;
&lt;td&gt;Yes, comfortably&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;base&lt;/td&gt;
&lt;td&gt;74M&lt;/td&gt;
&lt;td&gt;~40MB&lt;/td&gt;
&lt;td&gt;Solid for clear speech, single speaker&lt;/td&gt;
&lt;td&gt;Yes, with headroom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;small&lt;/td&gt;
&lt;td&gt;244M&lt;/td&gt;
&lt;td&gt;~120MB&lt;/td&gt;
&lt;td&gt;Meaningfully better&lt;/td&gt;
&lt;td&gt;Borderline on laptops without a discrete GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Start with the &lt;code&gt;base&lt;/code&gt; model. It delivers solid transcription accuracy without the heavy load times or compute costs of the &lt;code&gt;small&lt;/code&gt; model. If you need to target lower-end devices, fall back to the &lt;code&gt;tiny&lt;/code&gt; model and provide clear UI feedback so users understand why accuracy might take a slight hit.&lt;/p&gt;

&lt;h3 id=&quot;chunking-mic-audio&quot;&gt;Chunking mic audio&lt;/h3&gt;
&lt;p&gt;Whisper isn’t built for native stream processing out of the box; it expects fixed-length audio buffers. To make it feel real-time, you need to chunk the incoming mic stream before passing it to the pipeline. A rolling audio buffer solves this: slice short audio segments, run each through Whisper, and maintain an overlapping context window so you don’t chop words in half mid-sentence.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;// chunker-worklet.js — runs on the audio rendering thread, not the main thread
class ChunkerProcessor extends AudioWorkletProcessor {
  constructor() {
    super();
    this.buffer = [];
    this.chunkSize = 3000 * (16000 / 1000); // 3s at 16kHz
  }

  process(inputs) {
    const input = inputs[0][0];
    if (!input) return true;

    this.buffer.push(...input);

    if (this.buffer.length &amp;gt;= this.chunkSize) {
      this.port.postMessage(new Float32Array(this.buffer));
      this.buffer = [];
    }

    return true; // keep the processor alive
  }
}

registerProcessor(&#39;chunker-processor&#39;, ChunkerProcessor);&lt;/pre&gt;
&lt;p&gt;This worklet runs in its own file on the dedicated audio rendering thread, a high-priority background thread reserved by the browser for low-latency audio tasks. Because it operates entirely off the main thread, it won’t block UI rendering or network requests, which is critical when timing matters. The main thread simply registers the worklet and listens for completed audio chunks over a message channel:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;// main thread
const audioContext = new AudioContext({ sampleRate: 16000 });
await audioContext.audioWorklet.addModule(&#39;chunker-worklet.js&#39;);

const source = audioContext.createMediaStreamSource(micStream);
const chunker = new AudioWorkletNode(audioContext, &#39;chunker-processor&#39;);

chunker.port.onmessage = ({ data }) =&amp;gt; {
  transcribeChunk(data); // hand the chunk off to the Whisper worker
};

source.connect(chunker);&lt;/pre&gt;
&lt;p&gt;This setup is intentional: the audio rendering thread handles raw capture, the Whisper worker processes transcription, and the main thread manages UI state. Because each runs on its own thread, they won’t block one another during heavy workloads.&lt;/p&gt;
&lt;p&gt;There is one drawback to a strict fixed-window approach: a hard 3-second boundary might slice a word in half, causing Whisper to hallucinate or mis-transcribe both fragments. The rolling buffer’s overlap helps smooth this out, but for a production-ready agent, it is better to use energy-based Voice Activity Detection (VAD). VAD segments audio dynamically at natural speech pauses rather than relying on a rigid timer, which is the exact approach used in the demo at the top.&lt;/p&gt;
&lt;h3 id=&quot;streaming-interim-transcripts-without-freezing-the-ui&quot;&gt;Streaming interim transcripts without freezing the UI&lt;/h3&gt;
&lt;p&gt;Two things will paralyze your UI thread: running heavy model inference directly on it, and updating the &lt;a href=&quot;https://blog.logrocket.com/exploring-essential-dom-methods-frontend-development/&quot;&gt;DOM&lt;/a&gt; with every partial transcript token.&lt;/p&gt;
&lt;p&gt;Run the transcriber inside a Web Worker. Transformers.js runs smoothly off the main thread, and moving it there is essential for building real-time, responsive agents. Without offloading inference to a worker, every single transcription step will cause the UI to stutter and freeze during live conversations.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;// worker.js
self.onmessage = async ({ data }) =&amp;gt; {
  const result = await transcriber(data, {
    return_timestamps: false,
  });
  self.postMessage({ text: result.text });
};&lt;/pre&gt;
&lt;p&gt;The worker only manages the model. It doesn’t touch the DOM or handle state; it just takes audio in and sends text out. This keeps the main thread free to update the user interface:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;// main thread
const worker = new Worker(&#39;worker.js&#39;);

worker.onmessage = ({ data }) =&amp;gt; {
  updateTranscriptUI(data.text); // cheap DOM update, not a re-render of the whole tree
};

function transcribeChunk(audio) {
  worker.postMessage(audio);
}&lt;/pre&gt;
&lt;p&gt;Make UI updates as lightweight as possible. In React, target individual text nodes or localized component states rather than triggering a re-render of the entire conversation list. If partial transcription tokens are streaming in rapidly, batch your state updates to keep frame rates smooth.&lt;/p&gt;
&lt;h2 id=&quot;running-the-local-llm&quot;&gt;How do you run a local LLM in the browser for voice responses?&lt;/h2&gt;
&lt;p&gt;Now that you have clean text, this stage feels much more familiar: load a model, pass in a system prompt, and stream the generated tokens back. The main challenge is handling all of this without a server backend and without freezing the browser tab while a multi-hundred-megabyte model loads into memory.&lt;/p&gt;
&lt;h3 id=&quot;loading-the-model-2&quot;&gt;Loading the model&lt;/h3&gt;
&lt;p&gt;You’ll use the same &lt;code&gt;pipeline()&lt;/code&gt; API as Whisper, but with a different task type:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;import { pipeline } from &#39;@huggingface/transformers&#39;;

const generator = await pipeline(
  &#39;text-generation&#39;,
  &#39;onnx-community/Qwen2.5-0.5B-Instruct&#39;,
  {
    device: &#39;webgpu&#39;,
    dtype: &#39;q4&#39;,
  }
);&lt;/pre&gt;
&lt;p&gt;A 0.5B parameter model won’t write your unit tests, but it’s fast enough for back-and-forth conversation, which is what actually counts here. Quantized to 4-bit, it sits at around 350MB, a heavy initial download, but one the browser caches for subsequent loads.&lt;/p&gt;
&lt;p&gt;If your target hardware can handle a larger model, something like &lt;code&gt;Qwen2.5-1.5B-Instruct&lt;/code&gt; delivers noticeably better responses, though you will trade off longer load times and higher VRAM consumption.&lt;/p&gt;
&lt;p&gt;Keep a close eye on total memory footprint. Holding both Whisper and the LLM in WebGPU simultaneously requires roughly 1 to 1.5 GB of VRAM. Desktop GPUs won’t even blink at that, but a low-end mobile browser under strict memory limits will outright kill the WebGPU context, which manifests as the agent silently dying mid-session rather than throwing a clean, catchable error. If you’re targeting mobile devices, that VRAM ceiling is a far bigger constraint than the initial download size.&lt;/p&gt;
&lt;h3 id=&quot;building-the-prompt&quot;&gt;Building the prompt&lt;/h3&gt;
&lt;p&gt;Keep the conversation history and feed it through the model’s chat template rather than hand-rolling a prompt string:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const messages = [
  { role: &#39;system&#39;, content: &#39;You are a concise voice assistant. Keep responses short and conversational.&#39; },
  ...conversationHistory,
  { role: &#39;user&#39;, content: transcript },
];&lt;/pre&gt;
&lt;p&gt;The system prompt is especially important here. If a voice agent replies with long paragraphs or lists, TTS will read everything, and users will stop listening before it’s done. Make sure to tell the model to keep responses short.&lt;/p&gt;
&lt;h3 id=&quot;streaming-tokens-without-blocking-the-main-thread&quot;&gt;Streaming tokens without blocking the main thread&lt;/h3&gt;
&lt;p&gt;Just like with Whisper, run this in a Web Worker, not on the main thread. Even though a 0.5B model is small for an LLM, generating text still takes time. If the tab freezes, even for a second, it ruins the feel of a live conversation.&lt;/p&gt;

&lt;pre class=&quot;language-javascript hljs&quot;&gt;// llm-worker.js
import { TextStreamer } from &#39;@huggingface/transformers&#39;;

self.onmessage = async ({ data }) =&amp;gt; {
  const streamer = new TextStreamer(tokenizer, {
    skip_prompt: true,
    callback_function: (token) =&amp;gt; {
      self.postMessage({ type: &#39;token&#39;, token });
    },
  });

  await generator(data.messages, {
    max_new_tokens: 200,
    streamer,
  });

  self.postMessage({ type: &#39;done&#39; });
};&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;TextStreamer&lt;/code&gt; helps make the experience feel real-time instead of waiting for a long response. Each token is sent to the main thread as soon as it’s generated, so the UI can show it right away. More importantly, TTS can start speaking the first sentence before the model finishes the whole response.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;// main thread
const llmWorker = new Worker(&#39;llm-worker.js&#39;);
let currentSentence = &#39;&#39;;

llmWorker.onmessage = ({ data }) =&amp;gt; {
  if (data.type === &#39;token&#39;) {
    currentSentence += data.token;
    updateResponseUI(currentSentence);

    // Hand off complete sentences to TTS as they finish,
    // don&#39;t wait for the full response
    if (/[.!?]\s*$/.test(currentSentence)) {
      speakSentence(currentSentence);
      currentSentence = &#39;&#39;;
    }
  }
};&lt;/pre&gt;
&lt;p&gt;The sentence-boundary check is important. Without it, TTS waits for the full response before speaking, and any latency savings from running locally are lost while waiting for the LLM to finish.&lt;/p&gt;
&lt;h3 id=&quot;the-wasm-fallback&quot;&gt;The WASM fallback&lt;/h3&gt;
&lt;p&gt;WebGPU isn’t available everywhere yet. Safari support is still improving, and many users have older hardware or GPUs that don’t support compute shaders in the browser. Transformers.js handles this by falling back to WASM if WebGPU isn’t available, but this fallback comes with a performance cost.&lt;/p&gt;
&lt;p&gt;Detecting support takes one extra step people usually skip. Checking that &lt;code&gt;navigator.gpu&lt;/code&gt; exists isn’t enough, since some devices expose the API but still fail to return a usable adapter. Request one and confirm it’s there before committing to WebGPU:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;async function getDevice() {
  if (!navigator.gpu) return &#39;wasm&#39;;
  const adapter = await navigator.gpu.requestAdapter().catch(() =&amp;gt; null);
  return adapter ? &#39;webgpu&#39; : &#39;wasm&#39;;
}&lt;/pre&gt;
&lt;p&gt;Then pass the result into the pipeline instead of guessing inline:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const generator = await pipeline(
  &#39;text-generation&#39;,
  &#39;onnx-community/Qwen2.5-0.5B-Instruct&#39;,
  {
    device: await getDevice(),
    dtype: &#39;q4&#39;,
  }
);&lt;/pre&gt;
&lt;p&gt;Resolving the device up front lets you set expectations before the model even loads, rather than letting the user discover the slowdown mid-conversation.&lt;/p&gt;
&lt;p&gt;The difference in performance is clear. With WebGPU, the 0.5B model produces about 20 to 30 tokens per second, but with WASM, that drops to just 1 to 3 tokens per second. This speed might be fine for text generation, but for a voice agent, where quick responses are important, WASM is usually too slow to feel natural. You can either use a smaller model with WASM or let users know in the interface that their device may be slower.&lt;/p&gt;
&lt;h2 id=&quot;text-to-speech-that-stays-local&quot;&gt;How do you keep browser text-to-speech fully local?&lt;/h2&gt;
&lt;p&gt;This is where the ‘fully local’ setup can fail if you’re not careful. &lt;code&gt;SpeechSynthesis&lt;/code&gt; is a built-in browser API, so there’s no download or model to load. However, not every voice it offers runs on your device; some use a network service, and the API doesn’t clearly show which ones are local.&lt;/p&gt;
&lt;h3 id=&quot;the-voice-filtering-problem&quot;&gt;The voice filtering problem&lt;/h3&gt;
&lt;p&gt;When you call &lt;code&gt;speechSynthesis.getVoices()&lt;/code&gt;, you’ll see both local and cloud-based voices, sometimes from the same vendor and with similar names. On Chrome, Google’s higher-quality voices use the network, while the local ones sound more robotic and are less prominently listed.&lt;/p&gt;
&lt;p&gt;The API actually tells you which is which; you have to check:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;function getLocalVoices() {
  const voices = speechSynthesis.getVoices();
  return voices.filter((voice) =&amp;gt; voice.localService === true);
}&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;localService&lt;/code&gt; flag is what matters. If you ignore it, your agent might claim to be fully local but actually send generated text to a third party as soon as it starts speaking.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;function pickVoice(preferredLang = &#39;en-US&#39;) {
  const localVoices = getLocalVoices();

  // Prefer a voice matching the target language, fall back to any local voice
  const match = localVoices.find((v) =&amp;gt; v.lang === preferredLang);
  return match || localVoices[0] || null;
}&lt;/pre&gt;
&lt;p&gt;Voice options depend on the user’s OS and browser, so always check for &lt;code&gt;null&lt;/code&gt; and have a backup plan. If needed, let users know if their browser doesn’t have a usable local voice.&lt;/p&gt;
&lt;h3 id=&quot;speaking-a-sentence&quot;&gt;Speaking a sentence&lt;/h3&gt;
&lt;p&gt;Once you’ve got a local voice, speaking is the easy part:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;function speakSentence(text, voice) {
  const utterance = new SpeechSynthesisUtterance(text);
  utterance.voice = voice;
  utterance.rate = 1.05; // a slightly faster rate sounds more natural for short responses
  return new Promise((resolve) =&amp;gt; {
    utterance.onend = resolve;
    speechSynthesis.speak(utterance);
  });
}&lt;/pre&gt;
&lt;p&gt;Wrapping this in a promise that resolves when onend fires is important. You need to know exactly when speech finishes so you can safely reopen the mic.&lt;/p&gt;
&lt;h3 id=&quot;queue-management&quot;&gt;Queue management&lt;/h3&gt;
&lt;p data-path-to-node=&quot;2&quot;&gt;Because the LLM worker streams completed sentences out as they are generated, multiple calls to &lt;code data-path-to-node=&quot;2&quot; data-index-in-node=&quot;96&quot;&gt;speak()&lt;/code&gt; can pile up far faster than the browser can physically speak them.&lt;/p&gt;
&lt;p data-path-to-node=&quot;3&quot;&gt;While the Web Speech API’s &lt;code data-path-to-node=&quot;3&quot; data-index-in-node=&quot;27&quot;&gt;SpeechSynthesis&lt;/code&gt; interface maintains an internal utterance queue by default, relying on browser-native queuing makes handling real-time interruptions messy and unpredictable. It is much better to manage an explicit application-level queue yourself.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;class SpeechQueue {
  constructor(voice) {
    this.voice = voice;
    this.queue = [];
    this.speaking = false;
  }

  add(text) {
    this.queue.push(text);
    if (!this.speaking) this.next();
  }

  async next() {
    if (this.queue.length === 0) {
      this.speaking = false;
      return;
    }
    this.speaking = true;
    const text = this.queue.shift();
    await speakSentence(text, this.voice);
    this.next();
  }

  clear() {
    this.queue = [];
    speechSynthesis.cancel();
    this.speaking = false;
  }
}&lt;/pre&gt;
&lt;p&gt;Managing your own queue lets you use a single &lt;code&gt;clear()&lt;/code&gt; method to handle interruptions, instead of dealing with the browser’s internal queue state.&lt;/p&gt;
&lt;h3 id=&quot;handling-interruptions&quot;&gt;Handling interruptions&lt;/h3&gt;
&lt;p&gt;Voice agents often get interrupted. If the user starts talking before the response is finished, the agent should stop mid-sentence instead of continuing to speak over the user.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;function handleUserSpeaking() {
  speechQueue.clear(); // stop TTS immediately

  llmWorker.postMessage({ type: &#39;abort&#39; }); // stop generation too, no point finishing a response nobody will hear
}&lt;/pre&gt;
&lt;p&gt;The LLM worker actually needs to respect that abort message; otherwise, it keeps generating tokens into a queue nobody’s draining:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;// llm-worker.js
let aborted = false;

self.onmessage = async ({ data }) =&amp;gt; {
  if (data.type === &#39;abort&#39;) {
    aborted = true;
    return;
  }

  aborted = false;
  const streamer = new TextStreamer(tokenizer, {
    skip_prompt: true,
    callback_function: (token) =&amp;gt; {
      if (aborted) return;
      self.postMessage({ type: &#39;token&#39;, token });
    },
  });

  await generator(data.messages, { max_new_tokens: 200, streamer });
};&lt;/pre&gt;
&lt;p&gt;This doesn’t fully cancel generation; the model still computes tokens in the background, but it stops sending them. A better solution would use an &lt;code&gt;AbortController&lt;/code&gt; if supported, but just silencing the output is enough to make interruptions feel instant for users.&lt;/p&gt;
&lt;h2 id=&quot;managing-conversational-state&quot;&gt;How do you prevent a voice agent from hearing itself?&lt;/h2&gt;
&lt;p&gt;So far, each stage works in isolation: Whisper transcribes, the LLM generates, and &lt;code&gt;SpeechSynthesis&lt;/code&gt; speaks. But if you chain them together without a central state machine, the microphone can easily stay active while the agent is talking, causing it to transcribe its own voice and feed those outputs back to the LLM as user input.&lt;/p&gt;
&lt;h3 id=&quot;the-three-states&quot;&gt;The three states&lt;/h3&gt;
&lt;p&gt;At any given moment, a voice agent only needs three core states:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;LISTENING&lt;/code&gt;&lt;/strong&gt;: The mic is open, Whisper is actively transcribing, and the system is waiting for the user to finish talking&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;THINKING&lt;/code&gt;&lt;/strong&gt;: The mic is muted, the transcript has been handed off to the LLM, and tokens are actively streaming back&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SPEAKING&lt;/code&gt;&lt;/strong&gt;: The mic stays muted, &lt;code&gt;SpeechSynthesis&lt;/code&gt; is playing back audio, and the sentence queue is draining&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The transitions are more important than the states themselves. &lt;code&gt;LISTENING&lt;/code&gt; should only switch to &lt;code&gt;THINKING&lt;/code&gt; when the user has actually stopped talking, not just paused to take a breath. &lt;code&gt;THINKING&lt;/code&gt; should cut over to &lt;code&gt;SPEAKING&lt;/code&gt; as soon as the very first sentence is ready, rather than waiting for the entire LLM response to complete. Finally, &lt;code&gt;SPEAKING&lt;/code&gt; returns to &lt;code&gt;LISTENING&lt;/code&gt; only after the playback queue is completely empty.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const STATES = {
  LISTENING: &#39;listening&#39;,
  THINKING: &#39;thinking&#39;,
  SPEAKING: &#39;speaking&#39;,
};

class ConversationController {
  constructor() {
    this.state = STATES.LISTENING;
  }

  transition(next) {
    console.log(`${this.state} -&amp;gt; ${next}`);
    this.state = next;
  }
}&lt;/pre&gt;
&lt;p&gt;The state machine is simple, but just tracking the state isn’t enough. To keep the mic quiet during &lt;code&gt;SPEAKING&lt;/code&gt;, you need to actively enforce access control on the input stream based on the current state, rather than just logging transitions.&lt;/p&gt;
&lt;h3 id=&quot;gating-the-mic&quot;&gt;Gating the mic&lt;/h3&gt;
&lt;p&gt;The microphone should only pass audio to Whisper while the controller is explicitly in the &lt;code&gt;LISTENING&lt;/code&gt; state. In every other state, audio buffers still arrive from the hardware, but they must be immediately dropped before reaching the transcription worker:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;chunker.port.onmessage = ({ data }) =&amp;gt; {
  if (controller.state !== STATES.LISTENING) return;
  transcribeChunk(data);
};&lt;/pre&gt;
&lt;p&gt;That single guard clause prevents the agent from hearing itself. Without it, &lt;code&gt;SpeechSynthesis&lt;/code&gt; output plays through the speakers, the mic picks it up, Whisper transcribes it, and the LLM ends up responding to its own voice. On laptops with speakers positioned right next to the internal microphone, this feedback loop triggers instantly and can be brutal to debug if you don’t block the input at the gate.&lt;/p&gt;
&lt;h3 id=&quot;wiring-the-transitions&quot;&gt;Wiring the transitions&lt;/h3&gt;
&lt;p&gt;Attach state transitions directly to the event handlers where the work actually happens, rather than relying on a separate coordinator polling for status changes:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;// End of user speech detected (silence threshold hit)
function onUserFinishedSpeaking(transcript) {
  controller.transition(STATES.THINKING);
  llmWorker.postMessage({ messages: buildMessages(transcript) });
}

// First sentence ready from the LLM
function onFirstSentenceReady(sentence) {
  controller.transition(STATES.SPEAKING);
  speechQueue.add(sentence);
}

// Speech queue fully drained
function onSpeechQueueEmpty() {
  controller.transition(STATES.LISTENING);
}&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;onSpeechQueueEmpty&lt;/code&gt; should be triggered directly by the queue itself, never by an arbitrary timer or guess. Update the &lt;code&gt;SpeechQueue&lt;/code&gt; class from the TTS section to fire a callback whenever &lt;code&gt;next()&lt;/code&gt; finds no remaining text to process:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;async next() {
  if (this.queue.length === 0) {
    this.speaking = false;
    this.onEmpty?.(); // notify the controller
    return;
  }
  this.speaking = true;
  const text = this.queue.shift();
  await speakSentence(text, this.voice);
  this.next();
}&lt;/pre&gt;
&lt;h3 id=&quot;handling-interruptions-within-the-state-machine&quot;&gt;Handling interruptions within the state machine&lt;/h3&gt;
&lt;p&gt;The interruption mechanism from the TTS section slots directly into this flow. If the user begins speaking while the controller is in the &lt;code&gt;THINKING&lt;/code&gt; or &lt;code&gt;SPEAKING&lt;/code&gt; state, treat it as a deliberate user interrupt rather than background noise to throw away.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;function onUserStartedSpeaking() {
  if (controller.state === STATES.LISTENING) return; // already listening, nothing to interrupt

  speechQueue.clear();
  llmWorker.postMessage({ type: &#39;abort&#39; });
  controller.transition(STATES.LISTENING);
}&lt;/pre&gt;
&lt;p&gt;Detecting when the user starts speaking while the mic is “off” presents a separate challenge, since Whisper isn’t running during the &lt;code&gt;THINKING&lt;/code&gt; or &lt;code&gt;SPEAKING&lt;/code&gt; states. A lightweight Voice Activity Detector (VAD) that monitors raw audio energy levels, rather than running full transcription, works perfectly here. It doesn’t need to parse words; it simply flags when sound crosses a defined volume threshold.&lt;/p&gt;
&lt;p&gt;With the state machine controlling the mic and every transition bound to an actual runtime event rather than an arbitrary timer, the loop runs reliably: the agent listens, thinks, speaks, and cleanly hands control back at the exact right moment.&lt;/p&gt;
&lt;h2 id=&quot;the-complete-implementation&quot;&gt;Where can you see the complete voice AI agent implementation?&lt;/h2&gt;
&lt;p data-path-to-node=&quot;2&quot;&gt;Every piece detailed above comes together in the accompanying CodePen demo: the VAD worklet, both Web Workers, the custom speech queue, and the state machine, all organized inside a single reference file.&lt;/p&gt;
&lt;p data-path-to-node=&quot;3&quot;&gt;To try it out, open the demo in a WebGPU-capable browser (such as desktop Chrome or Edge). The initial run downloads roughly 400MB of model weights, which are cached locally for subsequent visits. Grant microphone permissions, wait for the models to finish loading into VRAM, and start talking.&lt;/p&gt;
&lt;p class=&quot;codepen&quot; style=&quot;height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot; data-height=&quot;300&quot; data-pen-title=&quot;Real-time voice agent &quot; data-default-tab=&quot;html,result&quot; data-slug-hash=&quot;OPWZvyG&quot; data-user=&quot;JudeIV&quot;&gt;See the Pen &lt;a href=&quot;https://codepen.io/JudeIV/pen/OPWZvyG&quot;&gt;&lt;br&gt;
Real-time voice agent &lt;/a&gt; by Miracle Jude (&lt;a href=&quot;https://codepen.io/JudeIV&quot;&gt;@JudeIV&lt;/a&gt;)&lt;br&gt;
on &lt;a href=&quot;https://codepen.io/&quot;&gt;CodePen&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Two things differ from the code in this article, both deliberate. The workers are created from Blob URLs rather than separate files, since CodePen serves a single document. And the demo uses energy-based voice activity detection to segment utterances instead of fixed 3-second chunks, which is more reliable for a live conversation than transcribing on a timer.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The entire pipeline runs locally on your device: Whisper handles transcription, a quantized &lt;a href=&quot;https://blog.logrocket.com/llm-context-problem-strategies-2026/&quot;&gt;LLM&lt;/a&gt; generates responses, and &lt;code&gt;SpeechSynthesis&lt;/code&gt; provides playback, all powered by Transformers.js with WebGPU acceleration (and WASM as a CPU fallback). No audio or text ever leaves the browser tab.&lt;/p&gt;
&lt;p&gt;The core tradeoff here is between local capability, complete privacy, and operational latency:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Capability&lt;/strong&gt;: A 0.5B or 1.5B parameter model cannot match a server-hosted, GPT-4-class API in reasoning quality or zero-shot breadth&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy&lt;/strong&gt;: Zero network requests mean zero data leakage, making this architecture completely compliant by default and functional offline after the initial model load&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency&lt;/strong&gt;: While WebGPU delivers snappy inference on dedicated hardware, the WASM fallback runs roughly ten times slower, which, for a conversational voice agent, is the difference between a natural back-and-forth and an unusable, frustrating delay&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before choosing an in-browser WebGPU pipeline over a traditional server-based backend, make sure you clearly weigh those hardware requirements against your application’s privacy and latency thresholds.&lt;/p&gt;


&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/ai/&quot;&gt;#ai&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/voice-ai-agent-browser/</link><guid isPermaLink="false">https://blog.logrocket.com/voice-ai-agent-browser/</guid><pubDate>Invalid Date</pubDate><author>Jude Miracle</author></item><item><title>Comparing AI agent sandbox platforms: E2B, Modal, Daytona, and more</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/comparing-ai-agent-sandbox-platforms-e2b-modal-daytona-and-more/#cold-start&quot;&gt;Cold start&lt;/a&gt;
        &lt;ul data-toc-name=&quot;cold-start&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/comparing-ai-agent-sandbox-platforms-e2b-modal-daytona-and-more/#isolation-model&quot;&gt;Isolation model&lt;/a&gt;
        &lt;ul data-toc-name=&quot;isolation-model&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/comparing-ai-agent-sandbox-platforms-e2b-modal-daytona-and-more/#session-persistence&quot;&gt;Session persistence&lt;/a&gt;
        &lt;ul data-toc-name=&quot;session-persistence&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/comparing-ai-agent-sandbox-platforms-e2b-modal-daytona-and-more/#sdk-ergonomics&quot;&gt;SDK ergonomics&lt;/a&gt;
        &lt;ul data-toc-name=&quot;sdk-ergonomics&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/comparing-ai-agent-sandbox-platforms-e2b-modal-daytona-and-more/#pricing-model&quot;&gt;Pricing model&lt;/a&gt;
        &lt;ul data-toc-name=&quot;pricing-model&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/comparing-ai-agent-sandbox-platforms-e2b-modal-daytona-and-more/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;Five dimensions separate AI agent sandbox platforms from one another: cold start, isolation, session persistence, SDK ergonomics, and pricing. No platform wins all five. The platforms that claim to do everything well usually optimize for one or two dimensions and accept tradeoffs everywhere else.&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;1536&quot; height=&quot;1024&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/featured-image.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;Comparing AI agent sandbox platforms: E2B, Modal, Daytona, and more&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/featured-image.png 1536w, https://blog.logrocket.com/wp-content/uploads/2026/08/featured-image-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/08/featured-image-1024x683.png 1024w, https://blog.logrocket.com/wp-content/uploads/2026/08/featured-image-768x512.png 768w&quot; sizes=&quot;(max-width: 1536px) 100vw, 1536px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;For most agents, one dimension matters more than the rest. A coding assistant that spins up an environment on every edit depends on cold start. An agent that runs untrusted user code depends on isolation. A research agent that holds a dataset across 20 tool calls depends on persistence. The right way to choose is to identify the dimension where a wrong decision would break the agent, the security model, or the budget. That becomes your shortlist, and the other dimensions become filters.&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;The matrix below compares 15 sandbox platforms across those dimensions, plus GPU support and deployment model.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Cold start&lt;/th&gt;
&lt;th&gt;Isolation&lt;/th&gt;
&lt;th&gt;Persistence&lt;/th&gt;
&lt;th&gt;SDK&lt;/th&gt;
&lt;th&gt;Pricing model&lt;/th&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;Deployment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E2B&lt;/td&gt;
&lt;td&gt;717ms create / 662ms resume (measured)&lt;/td&gt;
&lt;td&gt;Firecracker microVM&lt;/td&gt;
&lt;td&gt;Pause/resume, snapshot&lt;/td&gt;
&lt;td&gt;Python-first, TS&lt;/td&gt;
&lt;td&gt;Per-sec full duration, $150/mo floor&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Managed, BYOC (enterprise)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daytona&lt;/td&gt;
&lt;td&gt;742ms create / 1254ms resume (measured)&lt;/td&gt;
&lt;td&gt;Docker container by default; Kata/Sysbox opt-in&lt;/td&gt;
&lt;td&gt;Fork, pause/resume, snapshot&lt;/td&gt;
&lt;td&gt;Python-first, TS&lt;/td&gt;
&lt;td&gt;Per-sec, same rate as E2B&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Managed, self-host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modal&lt;/td&gt;
&lt;td&gt;2437ms create / 2347ms snapshot boot (measured)&lt;/td&gt;
&lt;td&gt;gVisor&lt;/td&gt;
&lt;td&gt;Filesystem/memory snapshot; no in-place resume&lt;/td&gt;
&lt;td&gt;Python-first&lt;/td&gt;
&lt;td&gt;Per-sec active, scale to zero&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blaxel&lt;/td&gt;
&lt;td&gt;2824ms create / 1924ms resume (measured)&lt;/td&gt;
&lt;td&gt;microVM&lt;/td&gt;
&lt;td&gt;Perpetual hibernation, snapshot&lt;/td&gt;
&lt;td&gt;Async Python, TS&lt;/td&gt;
&lt;td&gt;Memory-tier per-sec, zero idle&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;1852ms create / 3333ms resume (measured)&lt;/td&gt;
&lt;td&gt;Firecracker microVM&lt;/td&gt;
&lt;td&gt;Persistent by default, snapshot&lt;/td&gt;
&lt;td&gt;TS-first, Python&lt;/td&gt;
&lt;td&gt;Active CPU, memory full wall-clock&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Managed (iad1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Northflank&lt;/td&gt;
&lt;td&gt;~sub-sec (vendor)&lt;/td&gt;
&lt;td&gt;Kata microVM + gVisor, per workload&lt;/td&gt;
&lt;td&gt;Configurable, snapshot&lt;/td&gt;
&lt;td&gt;REST/SDK&lt;/td&gt;
&lt;td&gt;Per-sec, lowest published PaaS rate; BYOC&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Managed, BYOC, self-host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fly Sprites&lt;/td&gt;
&lt;td&gt;~300ms checkpoint (vendor)&lt;/td&gt;
&lt;td&gt;Firecracker microVM&lt;/td&gt;
&lt;td&gt;Persistent-first, checkpoint&lt;/td&gt;
&lt;td&gt;TS/SDK&lt;/td&gt;
&lt;td&gt;Per-sec, nothing when idle&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare&lt;/td&gt;
&lt;td&gt;Isolate in ms (vendor) / container start&lt;/td&gt;
&lt;td&gt;Sandbox SDK = container; Worker Loader = V8 isolate&lt;/td&gt;
&lt;td&gt;Container persistence; isolate ephemeral&lt;/td&gt;
&lt;td&gt;TS-first&lt;/td&gt;
&lt;td&gt;Active CPU + Workers/DO + $5/mo&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Managed (edge)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runloop&lt;/td&gt;
&lt;td&gt;~25ms resume (vendor)&lt;/td&gt;
&lt;td&gt;Dual-layer microVM + container&lt;/td&gt;
&lt;td&gt;Snapshot, suspend/resume, zero idle&lt;/td&gt;
&lt;td&gt;Python/TS&lt;/td&gt;
&lt;td&gt;$0.108/CPU-hr + $0.0252/GB-hr, $250/mo Pro&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Managed, VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CodeSandbox SDK&lt;/td&gt;
&lt;td&gt;1-3s VM clone (vendor)&lt;/td&gt;
&lt;td&gt;Firecracker microVM&lt;/td&gt;
&lt;td&gt;Memory snapshot, live VM cloning&lt;/td&gt;
&lt;td&gt;TS-only&lt;/td&gt;
&lt;td&gt;Usage tiers, $170/mo Scale&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Lambda MicroVMs&lt;/td&gt;
&lt;td&gt;Near-instant snapshot resume (vendor)&lt;/td&gt;
&lt;td&gt;Firecracker microVM&lt;/td&gt;
&lt;td&gt;Snapshot, suspend/resume ≤8h&lt;/td&gt;
&lt;td&gt;AWS API/SDK&lt;/td&gt;
&lt;td&gt;Per-sec vCPU+RAM, ceases when suspended&lt;/td&gt;
&lt;td&gt;No (ARM64)&lt;/td&gt;
&lt;td&gt;Managed (AWS, 5 regions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Agent Sandbox&lt;/td&gt;
&lt;td&gt;Sub-second create (vendor)&lt;/td&gt;
&lt;td&gt;gVisor (GKE-native)&lt;/td&gt;
&lt;td&gt;Stateful TTL ≤14 days&lt;/td&gt;
&lt;td&gt;GKE/SDK&lt;/td&gt;
&lt;td&gt;GKE-based; billing from July 1, 2026&lt;/td&gt;
&lt;td&gt;Via GKE&lt;/td&gt;
&lt;td&gt;Managed (GKE), self-host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenSandbox&lt;/td&gt;
&lt;td&gt;Backend-dependent&lt;/td&gt;
&lt;td&gt;Pluggable: gVisor / Kata / Firecracker&lt;/td&gt;
&lt;td&gt;Backend-dependent&lt;/td&gt;
&lt;td&gt;Python/TS/Go/Java&lt;/td&gt;
&lt;td&gt;Self-host, no platform fee&lt;/td&gt;
&lt;td&gt;Backend-dependent&lt;/td&gt;
&lt;td&gt;Self-host (Docker/K8s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsandbox&lt;/td&gt;
&lt;td&gt;Sub-200ms (vendor)&lt;/td&gt;
&lt;td&gt;libkrun microVM&lt;/td&gt;
&lt;td&gt;Persistent or ephemeral&lt;/td&gt;
&lt;td&gt;Python/TS, MCP&lt;/td&gt;
&lt;td&gt;Self-host, no platform fee&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Self-host, local&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AIO Sandbox&lt;/td&gt;
&lt;td&gt;Container start&lt;/td&gt;
&lt;td&gt;Docker, pluggable secure runtime&lt;/td&gt;
&lt;td&gt;Container persistence&lt;/td&gt;
&lt;td&gt;Multi-language, MCP&lt;/td&gt;
&lt;td&gt;Self-host, no platform fee&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Self-host (Docker)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;The following sections look at each dimension in turn. This article focuses on code-execution sandboxes: the environments where an agent runs code, shell commands, and package installs behind an API.&lt;/p&gt;
&lt;h2 id=&quot;cold-start&quot;&gt;Cold start&lt;/h2&gt;
&lt;p&gt;Cold start is really two measurements that vendors often blur together: create-from-scratch and resume-from-standby. Create-from-scratch provisions a fresh sandbox. Resume-from-standby brings back a paused one. Those numbers can diverge, sometimes in opposite directions across platforms.&lt;/p&gt;
&lt;p&gt;The isolation primitive sets the floor. A microVM boots a real guest kernel, which usually costs more than a container that shares the host kernel. A container usually costs more than a V8 isolate inside an already running process. No amount of tuning can move a platform below what its primitive allows.&lt;/p&gt;
&lt;p&gt;We measured directly rather than relying on published figures. The harness created a sandbox, ran a command, confirmed that the command executed inside the sandbox, paused the sandbox, then timed the resume to the first command. “Ready” means the command actually executed, not that the API acknowledged the request.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/blog-image.png&quot; alt=&quot;Benchmark Bar Chart Comparing Cold Create And Warm Resume Times Across Five Platforms&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;E2B was the fastest and most consistent in this benchmark, at 717ms to create and 662ms to resume. Daytona created almost as quickly, at 742ms, but resumed more slowly at 1254ms because its stop-then-start flow rebuilds more than a memory-preserving pause. Vercel created in 1852ms but resumed in 3333ms, making it the one platform where resume took longer than fresh create despite its persistence-by-default model. Modal and Blaxel had the slowest create times, at 2437ms and 2824ms respectively, though both recovered somewhat on resume.&lt;/p&gt;
&lt;p&gt;There is an important caveat: resume does not mean the same operation everywhere. E2B and Daytona pause and resume in place. Vercel resumes its persistent sandbox. Blaxel performs a real standby-to-active wake, which we forced past its auto-standby window before timing. Modal has no in-place resume, so its 2347ms figure is snapshot-boot time, not pause/resume time.&lt;/p&gt;
&lt;p&gt;Cold start should drive your decision when the agent creates or resumes environments frequently and a human is waiting on the result. Examples include a coding assistant that spins up an environment per interaction or an eval pipeline that runs thousands of short-lived sandboxes. In those workloads, E2B’s 717ms create time versus Blaxel’s 2824ms create time compounds across every invocation.&lt;/p&gt;
&lt;h2 id=&quot;isolation-model&quot;&gt;Isolation model&lt;/h2&gt;
&lt;p&gt;Isolation determines your security ceiling. These platforms occupy four tiers, based on where the boundary sits relative to the host kernel. Configuration can harden a platform, but it cannot fully move the platform out of its underlying tier.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hardware microVM:&lt;/strong&gt; Each sandbox gets its own guest kernel behind a virtualization boundary, with Firecracker (E2B, Vercel) and libkrun (Microsandbox) as common implementations. A kernel exploit inside the sandbox should not reach the host, which makes this the strongest boundary in the category.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;gVisor:&lt;/strong&gt; gVisor sits one tier down. Its user-space kernel, called the Sentry, intercepts syscalls and re-implements them in user space so the application does not call the host kernel directly. The attack surface shrinks to the Sentry itself. Modal uses gVisor, and Northflank offers it alongside microVM isolation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared-kernel container:&lt;/strong&gt; Namespaces and cgroups separate workloads, but every sandbox shares the host kernel. A kernel exploit can break the boundary. Daytona runs Docker by default, with stronger options available through configuration. This is reasonable for trusted code but riskier for untrusted model output or user-submitted code. An agent that reaches a mounted Docker socket can often escape most container isolation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;V8 isolate:&lt;/strong&gt; Cloudflare’s Worker Loader runs code in an isolate inside a running process. It starts in milliseconds, but it isolates only at the process level, which leaves a larger attack surface than a hardware VM. Cloudflare’s container-based Sandbox SDK is a separate and stronger product.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The BeyondTrust research on AWS AgentCore shows why the tier label is not the whole story. AgentCore runs on Firecracker, and the compute isolation itself was not broken. The failure was one layer above that. AgentCore’s “Sandbox” mode promised no external access but still permitted outbound DNS A and AAAA queries. Researchers built a command-and-control channel over DNS and used the interpreter’s over-broad IAM role to read S3 buckets with PII and credentials. AWS first called this intended functionality, then remediated DNS tunneling on April 15, 2026.&lt;/p&gt;
&lt;p&gt;Isolation is a stack, not a checkbox. The microVM-versus-container question sets the floor, but it is not the whole answer. Above the compute boundary, you still need to know what the sandbox permits by default on the network, whether it filters DNS, what its metadata service exposes, and how far its own credentials reach. When running untrusted model output, &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/&quot;&gt;securing your full-stack projects from NPM attacks&lt;/a&gt; is one more layer that belongs in the same threat model.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/blog-image-1.png&quot; alt=&quot;Four Horizontal Bands Showing Where Each Sandbox Primitive Draws Its Isolation Boundary&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;Isolation should drive your decision when the code is untrusted, generated from user input, or submitted directly by users. Default to a microVM tier and treat anything weaker as deliberate risk acceptance.&lt;/p&gt;
&lt;h2 id=&quot;session-persistence&quot;&gt;Session persistence&lt;/h2&gt;
&lt;p&gt;Persistence determines whether a stateful agent can continue real work or has to rebuild its world on every invocation. There are four main persistence models:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ephemeral:&lt;/strong&gt; The sandbox exists for one execution and is then discarded. This is a poor fit for an agent that clones a repo, installs dependencies, and then works across 20 tool calls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pause/resume:&lt;/strong&gt; The full state is preserved, including memory and filesystem, and the same instance comes back later. E2B does this through &lt;code&gt;beta_pause()&lt;/code&gt; and reconnects to the same sandbox ID.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Snapshot/fork:&lt;/strong&gt; State is captured as a reusable artifact that can boot new sandboxes. Because one snapshot can seed many sandboxes, an agent can fork a prepared environment into parallel branches. Modal, Daytona, and Vercel expose this model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Perpetual hibernation:&lt;/strong&gt; The sandbox stays alive indefinitely but bills nothing while it sleeps. Blaxel and Sprites build around this model, which means no teardown, no rebuild, and near-zero idle cost.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Persistence and idle billing are two sides of the same decision. If a sandbox holds state between invocations, it has to exist between invocations, and someone pays for that time. The persistence model is therefore also a cost model, which the pricing section covers later.&lt;/p&gt;
&lt;p&gt;Our benchmark measured the resume side. E2B resumed in 662ms, Blaxel woke from standby in 1924ms, Vercel resumed in 3333ms, and Modal’s snapshot boot took 2347ms. These are the delays a stateful agent pays every time it comes back.&lt;/p&gt;

&lt;p&gt;Here is pause/resume in practice using E2B. The sandbox preserves full state across a gap where the sandbox costs nothing:&lt;/p&gt;
&lt;pre class=&quot;language-python hljs&quot;&gt;from e2b import Sandbox

# First invocation: set up expensive state, then pause.
sbx = Sandbox.create()
sbx.commands.run(&quot;git clone https://github.com/example/repo /work&quot;)
sbx.commands.run(&quot;cd /work &amp;amp;&amp;amp; pip install -r requirements.txt&quot;)
sbx.commands.run(&quot;echo &#39;analysis_step_1_complete&#39; &amp;gt; /work/state.txt&quot;)

sandbox_id = sbx.sandbox_id
sbx.beta_pause()   # memory + filesystem frozen, billing stops

# ...minutes or hours later, a separate process, a later agent turn...

# Second invocation: reconnect to the same sandbox, state intact.
resumed = Sandbox.connect(sandbox_id)
result = resumed.commands.run(&quot;cat /work/state.txt&quot;)
print(result.stdout)          # -&amp;gt; analysis_step_1_complete
# repo is still cloned, deps are still installed, no rebuild
resumed.kill()
&lt;/pre&gt;
&lt;p&gt;The clone and install do not run again because the agent picks up where it stopped. That is the value of persistence.&lt;/p&gt;
&lt;p&gt;Snapshot/fork solves a different problem: capture state once, then spawn many copies. Modal’s &lt;code&gt;snapshot_filesystem()&lt;/code&gt; returns an image, and every sandbox booted from it starts from the same prepared state:&lt;/p&gt;
&lt;pre class=&quot;language-python hljs&quot;&gt;import modal

app = modal.App.lookup(&quot;agent-work&quot;, create_if_missing=True)
base = modal.Image.from_registry(&quot;python:3.13-slim&quot;)

# Prepare an environment once.
sb = modal.Sandbox.create(app=app, image=base)
sb.exec(&quot;bash&quot;, &quot;-c&quot;, &quot;pip install pandas numpy scikit-learn&quot;)
prepared = sb.snapshot_filesystem()   # reusable image
sb.terminate()

# Fork three parallel sandboxes from the same prepared state.
branches = [
    modal.Sandbox.create(app=app, image=prepared)
    for _ in range(3)
]

for i, branch in enumerate(branches):
    branch.exec(&quot;bash&quot;, &quot;-c&quot;, f&quot;echo &#39;running trajectory {i}&#39;&quot;)

# Each branch explores a different approach from the same starting point.
for branch in branches:
    branch.terminate()
&lt;/pre&gt;
&lt;p&gt;Pause/resume answers “can my agent continue?” Snapshot/fork answers “can my agent branch?” A long-running session wants the first. An eval harness running many trajectories from a fixed baseline wants the second.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/blog-image-2.png&quot; alt=&quot;Terminal Output From The E2B Pause Resume Script Confirming The Cloned Repo Is Still Present&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;Persistence should drive your decision when work spans invocations and losing state means redoing expensive setup. It matters less for stateless, single-shot execution, where an ephemeral sandbox is simpler and cheaper.&lt;/p&gt;
&lt;h2 id=&quot;sdk-ergonomics&quot;&gt;SDK ergonomics&lt;/h2&gt;
&lt;p&gt;SDK ergonomics determine how much infrastructure thinking leaks into your application code. The five platforms we tested fall into three broad shapes.&lt;/p&gt;
&lt;p&gt;The first shape is create-run-teardown with almost no ceremony. E2B and Daytona both give you a sandbox from a single call, let you run a command, and then let you tear it down:&lt;/p&gt;
&lt;pre class=&quot;language-python hljs&quot;&gt;# E2B
from e2b import Sandbox

sbx = Sandbox.create()
result = sbx.commands.run(&quot;echo hello&quot;)
print(result.stdout)
sbx.kill()
&lt;/pre&gt;
&lt;pre class=&quot;language-python hljs&quot;&gt;# Daytona
from daytona import Daytona, DaytonaConfig

daytona = Daytona(DaytonaConfig(api_key=&quot;...&quot;))
sandbox = daytona.create()
result = sandbox.process.exec(&quot;echo hello&quot;)
print(result.result)
daytona.delete(sandbox)
&lt;/pre&gt;
&lt;p&gt;They are nearly identical in shape, except for naming friction: E2B returns &lt;code&gt;.stdout&lt;/code&gt;, while Daytona returns &lt;code&gt;.result&lt;/code&gt; through a &lt;code&gt;.process&lt;/code&gt; namespace.&lt;/p&gt;
&lt;p&gt;The second shape requires more setup. Modal needs an App and Image first because sandboxes are one primitive inside a broader serverless platform:&lt;/p&gt;
&lt;pre class=&quot;language-python hljs&quot;&gt;# Modal
import modal

app = modal.App.lookup(&quot;my-app&quot;, create_if_missing=True)
image = modal.Image.from_registry(&quot;python:3.13-slim&quot;)

sb = modal.Sandbox.create(app=app, image=image)
p = sb.exec(&quot;bash&quot;, &quot;-c&quot;, &quot;echo hello&quot;)
print(p.stdout.read())
sb.terminate()
&lt;/pre&gt;
&lt;p&gt;That preamble is what lets Modal attach GPUs, define images in code, and scale to thousands of jobs. It is worth it if you need those capabilities, but it is extra friction if you only need a sandbox.&lt;/p&gt;
&lt;p&gt;The third shape is Cloudflare, which does not run as a standalone script. Its Sandbox SDK lives inside a Worker and requires a Dockerfile, a &lt;code&gt;wrangler.jsonc&lt;/code&gt; binding, and local Docker for development:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;// Cloudflare — inside a Worker, not a standalone script
import { getSandbox } from &quot;@cloudflare/sandbox&quot;;
export { Sandbox } from &quot;@cloudflare/sandbox&quot;;

export default {
  async fetch(request: Request, env: Env) {
    const sandbox = getSandbox(env.Sandbox, &quot;my-sandbox&quot;);
    const result = await sandbox.exec(&quot;echo hello&quot;);
    return new Response(result.stdout);
  },
};
&lt;/pre&gt;
&lt;p&gt;You are not calling a sandbox API here. You are deploying a Worker that contains one. That feels natural for a team already on Workers, but it is a platform commitment for everyone else.&lt;/p&gt;
&lt;p&gt;Vercel sits between these models. You get a single &lt;code&gt;Sandbox.create()&lt;/code&gt; call like E2B, but off-platform it requires three auth values: the team ID, the project ID, and a token. If you’re already building on Vercel’s ecosystem, understanding &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/&quot;&gt;Vercel Eve for AI agents&lt;/a&gt; gives useful context on how the platform handles agentic workloads more broadly.&lt;/p&gt;
&lt;p&gt;Language support also matters. E2B, Daytona, and Modal are Python-first with TypeScript support alongside. Cloudflare and Vercel are TypeScript-first. Blaxel ships both, but its Python SDK is async-only, so every call is an &lt;code&gt;await&lt;/code&gt;. Match the SDK’s primary language to your stack or you may inherit the second-class surface.&lt;/p&gt;
&lt;p&gt;There are also abstraction layers. ComputeSDK exposes one TypeScript API over nine providers, and VibeKit does the same for coding agents across E2B, Daytona, Modal, and Fly. Abstraction makes sense when avoiding lock-in matters. It is less useful when you need a platform’s distinctive features because the layer usually exposes the common denominator. Teams building with MCP-compatible sandboxes may also find value in reviewing &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/&quot;&gt;the top MCP servers for AI projects&lt;/a&gt; when deciding how their agent’s tools connect to execution environments.&lt;/p&gt;
&lt;p&gt;SDK ergonomics should drive your decision when the team is moving fast or may switch providers. It matters less once you have already committed to a platform for isolation, persistence, GPU, or pricing reasons.&lt;/p&gt;
&lt;h2 id=&quot;pricing-model&quot;&gt;Pricing model&lt;/h2&gt;
&lt;p&gt;Pricing determines whether a prototype survives production volume. The trap is comparing per-hour rates because the rate rarely determines the bill. The billing model does.&lt;/p&gt;
&lt;p&gt;Four questions matter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do you pay per second of wall-clock time or only for active CPU?&lt;/li&gt;
&lt;li&gt;Is idle time charged?&lt;/li&gt;
&lt;li&gt;Is there a subscription floor?&lt;/li&gt;
&lt;li&gt;How is memory billed relative to compute?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The active-CPU question separates these platforms most clearly. E2B and Daytona charge CPU and memory for the full duration a sandbox is alive, at $0.0504/vCPU-hr and $0.0162/GiB-hr, billed per second (rates from vendor pages and independent comparisons, April to June 2026). Vercel bills CPU at $0.128/vCPU-hr for active time only, with memory at $0.0212/GB-hr for the full wall-clock. Modal drops compute charges to zero when idle. Blaxel charges nothing for compute in standby and prices by memory tier, with its 4GB tier at $0.1656/hr. Fly Sprites also charges nothing while idle. Cloudflare bills active CPU at $0.072/vCPU-hr, on top of Workers requests, Durable Objects, and a $5/month base.&lt;/p&gt;
&lt;p&gt;These differences invert the ranking by workload shape.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/blog-image-1.png&quot; alt=&quot;Side By Side Pricing Charts Comparing Scenario A And Scenario B Monthly Cost By Platform&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;Consider a bursty interactive agent running 30,000 sessions a month. Each sandbox stays alive for about five minutes, uses CPU for only 30 seconds, and carries no state between sessions. At 2 vCPU and 4GB, Modal lands near $36 because it charges for almost no idle CPU. Vercel follows at around $276 because active-CPU billing skips the waiting. E2B, Daytona, and Blaxel all cluster near $414 because full-duration billing charges for every second of that wait. In this scenario, active-CPU platforms win by an order of magnitude.&lt;/p&gt;
&lt;p&gt;A long-running stateful agent reorders the comparison. Picture 50 agents each holding one persistent sandbox, active for two hours a day and idle for the other 22, with state retained for instant resume. Modal looks cheapest at around $426, but only because the model counts active compute and not the cost of rebuilding state on every wake. E2B, Daytona, and Blaxel land near $533 for active compute plus near-zero idle storage while genuinely preserving state. Vercel becomes the most expensive at roughly $3,800 because keeping a sandbox resident for instant resume means its memory bills across all 24 hours. The platform that wins the first scenario loses the second on the same memory-billing rule.&lt;/p&gt;
&lt;p&gt;Pricing should drive your decision when the agent runs at volume and you know the workload shape. It matters less for prototypes and low-volume internal tools where free tiers or small bills absorb the load. Product managers evaluating sandbox costs at scale may find it useful to apply a &lt;a href=&quot;https://blog.logrocket.com/product-management/how-to-use-technical-debt-register/&quot;&gt;technical debt register&lt;/a&gt; to track the long-term cost implications of platform lock-in decisions.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;There is no universal best sandbox platform. There is only the best sandbox for your binding constraint. Cold start decides interactive agents. Isolation decides untrusted code. Persistence decides stateful work. The billing model decides cost at scale, and it can flip the ranking by workload shape.&lt;/p&gt;
&lt;p&gt;Start by finding the dimension where a wrong choice would break the agent or the budget. Let that dimension create the shortlist, then filter on the rest.&lt;/p&gt;
&lt;p&gt;Trust your own numbers over marketing copy. Our measured cold starts ran above vendor claims because vendors often measure warm, in-region paths while real agents pay for setup, distance, and the round trip. Pricing inverted by workload. AgentCore showed that strong compute isolation is not enough if the network layer leaks. Model your workload against the billing structure, verify the isolation boundary that matters for your threat model, and your decision framework will outlast any one platform. For teams building agentic systems more broadly, &lt;a href=&quot;https://blog.logrocket.com/context-rot-slowing-down-your-ai-agent-how-fix/&quot;&gt;context rot in AI agents&lt;/a&gt; is a related performance problem worth addressing alongside sandbox selection. If you’re evaluating how &lt;a href=&quot;https://blog.logrocket.com/product-management/human-in-the-loop-ai-decision-making/&quot;&gt;human-in-the-loop AI&lt;/a&gt; fits into your agentic architecture, the ownership and oversight model you choose will also shape which sandbox features matter most.&lt;/p&gt;


&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/ai/&quot;&gt;#ai&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/comparing-ai-agent-sandbox-platforms-e2b-modal-daytona-and-more/</link><guid isPermaLink="false">https://blog.logrocket.com/comparing-ai-agent-sandbox-platforms-e2b-modal-daytona-and-more/</guid><pubDate>Invalid Date</pubDate><author>Ikeh Akinyemi</author></item><item><title>How to use Vercel eve: The Next.js framework for AI agents</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#what-is-eve&quot;&gt;What is Vercel eve?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-eve&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#how-does-eve-work&quot;&gt;How does Vercel eve work?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-does-eve-work&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#what-ships-in-the-box&quot;&gt;What ships in the box?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-ships-in-the-box&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#resumble-sessions&quot;&gt;Resumable sessions&lt;/a&gt;
        &lt;ul data-toc-name=&quot;resumble-sessions&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#isolated-sandbox&quot;&gt;Isolated sandbox&lt;/a&gt;
        &lt;ul data-toc-name=&quot;isolated-sandbox&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#human-in-the-loop-approval&quot;&gt;Human-in-the-loop approval&lt;/a&gt;
        &lt;ul data-toc-name=&quot;human-in-the-loop-approval&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#observability-and-extensions&quot;&gt;Observability and extensions&lt;/a&gt;
        &lt;ul data-toc-name=&quot;observability-and-extensions&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#channels-and-connections&quot;&gt;Channels and connections&lt;/a&gt;
        &lt;ul data-toc-name=&quot;channels-and-connections&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#getting-started-with-eve&quot;&gt;How to get started with Vercel eve?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;getting-started-with-eve&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#setting-eve-up-in-an-existing-app&quot;&gt;How to install Vercel eve in an existing project?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;setting-eve-up-in-an-existing-app&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#creating-your-first-eve-agent&quot;&gt;How to build your first AI agent with Vercel eve?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;creating-your-first-eve-agent&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#agentts&quot;&gt;&lt;code&gt;agent.ts&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;agentts&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#instructionsmd&quot;&gt;&lt;code&gt;instructions.md&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;instructionsmd&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#toolsts&quot;&gt;&lt;code&gt;tools/*.ts&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;toolsts&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#skillsmd&quot;&gt;&lt;code&gt;skills/*.md&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;skillsmd&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#subagents&quot;&gt;&lt;code&gt;subagents/*&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;subagents&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#production-use-cases&quot;&gt;How is Vercel using eve in production?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;production-use-cases&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#eve-vs-manually-crafted-agent-stack&quot;&gt;Vercel eve vs. traditional agent stacks: Key differences&lt;/a&gt;
        &lt;ul data-toc-name=&quot;eve-vs-manually-crafted-agent-stack&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/vercel-eve-ai-agents/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;Claude Code and Codex are excellent AI agents for coding and general tasks, but there comes a time when you’ll need a custom agent for specific workflows. At that point, it makes sense to build your own. It will not only be more efficient, but also save tokens and improve accuracy. There’s just one problem: building a custom harness isn’t the most straightforward thing to do.&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;895&quot; height=&quot;597&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/Vercel-eve.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/Vercel-eve.png 895w, https://blog.logrocket.com/wp-content/uploads/2026/08/Vercel-eve-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/08/Vercel-eve-768x512.png 768w&quot; sizes=&quot;(max-width: 895px) 100vw, 895px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;A few weeks back, Vercel open-sourced eve, its in-house AI framework for building and running agents. According to Vercel, eve powers more than a hundred of its agents in production today.&lt;/p&gt;
&lt;p&gt;Eve was created to improve the process of building, running, and scaling AI agents using familiar patterns from modern frontend frameworks like Next.js and Nuxt, where the building blocks are file-based and modular.&lt;/p&gt;
&lt;p&gt;In this guide, we’ll explore how eve differs from the traditional way of building an AI agent and how it works by creating a basic agent. By the end of this article, you’ll understand the building blocks of eve and know how to build your own AI agent with it.&lt;/p&gt;
&lt;h2 id=&quot;what-is-eve&quot;&gt;What is Vercel eve?&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://blog.logrocket.com/ux-design/ai-driven-ux-design-patterns/&quot;&gt;AI&lt;/a&gt; ecosystem is still the Wild West, and building agents from the ground up often means stitching together different pieces until something works. It’s similar to frontend development before libraries and frameworks like React and Next.js came along and standardized how web applications are built. That’s the gap eve aims to fill.&lt;/p&gt;
&lt;p&gt;In other words, eve is an agent framework designed to simplify how developers build and deploy production-ready AI agents. It provides a structured architecture with runtime capabilities, deployment options, and production infrastructure for building agentic software out of the box.&lt;/p&gt;
&lt;p&gt;The biggest idea behind eve isn’t a new prompting technique or model. It’s that building an agent should feel like building a Next.js app: organize your agent into files and folders, and let the framework handle the tedious parts.&lt;/p&gt;
&lt;h2 id=&quot;how-does-eve-work&quot;&gt;How does Vercel eve work?&lt;/h2&gt;
&lt;p&gt;Eve is designed around a filesystem-first architecture where each file represents a component of the agent. Instead of writing complex boilerplate, developers can structure their entire agent using directories and files, where behaviors and skills are defined in &lt;code&gt;.md&lt;/code&gt; using Markdown instructions, and tools and specialized subagents are added as &lt;a href=&quot;https://blog.logrocket.com/typescript-at-scale-2026/&quot;&gt;TypeScript&lt;/a&gt; files.&lt;/p&gt;
&lt;p&gt;A basic eve project structure might look like this:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;agent/
├── instructions.md
├── agent.ts
├── tools/
│   ├── search.ts
│   └── github.ts
├── skills/
│   └── research.md
├── subagents/
└── channels/&lt;/pre&gt;
&lt;p&gt;At build time, eve automatically detects these components and exposes them to the model without requiring additional boilerplate to register them. This way, you can focus on what your agent does rather than how it does it.&lt;/p&gt;
&lt;h2 id=&quot;what-ships-in-the-box&quot;&gt;What ships in the box?&lt;/h2&gt;
&lt;p&gt;The framework comes with several production-oriented capabilities right out of the box that eliminate the infrastructure setup usually associated with building AI agents from scratch. These include:&lt;/p&gt;
&lt;h3 id=&quot;resumble-sessions&quot;&gt;Resumable sessions&lt;/h3&gt;
&lt;p&gt;Unlike stateless requests, eve relies on durable execution and state persistence. The framework remembers customer sessions across turns and checkpoints its steps. If a step is expensive or requires human approval, the agent can pause the task, wait for feedback, and resume exactly where it left off without losing context.&lt;/p&gt;
&lt;h3 id=&quot;isolated-sandbox&quot;&gt;Isolated sandbox&lt;/h3&gt;
&lt;p&gt;AI agents often write or execute code that should not be trusted inside your main application runtime. Eve handles this by giving every agent its own isolated environment. It runs agents in hardware-isolated microVMs on the server and uses Docker locally. If the agent needs to clone a repo, run shell commands, or read and write files, it does so safely within this disposable sandbox without compromising your core system.&lt;/p&gt;
&lt;h3 id=&quot;human-in-the-loop-approval&quot;&gt;Human-in-the-loop approval&lt;/h3&gt;
&lt;p&gt;Eve allows any action to be set to require approval. This way, the agent pauses and waits indefinitely without wasting compute, and when the request is approved, eve continues from where it left off.&lt;/p&gt;
&lt;h3 id=&quot;observability-and-extensions&quot;&gt;Observability and extensions&lt;/h3&gt;
&lt;p&gt;Another capability is support for observability features based on OpenTelemetry, where every model call, tool invocation, and sandbox command is recorded as a trace that can be exported to monitoring platforms or viewed through Vercel’s observability interface.&lt;/p&gt;
&lt;h3 id=&quot;channels-and-connections&quot;&gt;Channels and connections&lt;/h3&gt;
&lt;p&gt;Eve makes it easy to deploy agents across multiple communication channels such as GitHub, Slack, Salesforce, or Notion without changing the core implementation of your agent. This allows you to use the same agent across different channels.&lt;/p&gt;

&lt;p&gt;It also provides built-in support for connecting agents to external services through MCP servers or APIs from services such as Discord, Teams, and Notion. Developers can also add providers through custom adapters.&lt;/p&gt;
&lt;h2 id=&quot;getting-started-with-eve&quot;&gt;How to get started with Vercel eve?&lt;/h2&gt;
&lt;p&gt;Before getting started with eve, you need to have &lt;a href=&quot;https://blog.logrocket.com/node-js-project-architecture-best-practices/&quot;&gt;Node.js&lt;/a&gt; installed on your computer. It must be at least version 24 or higher, and you also need your model credentials, such as API keys.&lt;/p&gt;
&lt;p&gt;That said, the quickest way to get started with eve is by running the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npx eve@latest init my-agent&lt;/pre&gt;
&lt;p&gt;This command will create a new eve project, install the necessary dependencies like Vercel’s &lt;code&gt;ai&lt;/code&gt; package and &lt;code&gt;zod&lt;/code&gt;, and start the development server.&lt;/p&gt;
&lt;p&gt;But before starting the server, eve prompts you to choose how it should authenticate with an AI model provider. You can either use Vercel’s AI Gateway, which provides a unified layer for accessing hundreds of AI models from different providers, or connect directly through other providers.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;alignnone size-full wp-image-214657&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/nlrkzcF4-1.png&quot; alt=&quot;&quot; width=&quot;697&quot; height=&quot;363&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/nlrkzcF4-1.png 697w, https://blog.logrocket.com/wp-content/uploads/2026/08/nlrkzcF4-1-300x156.png 300w&quot; sizes=&quot;(max-width: 697px) 100vw, 697px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;If you choose to authenticate using the first option, which is authenticating with AI Gateway, eve will connect to a Vercel project on your &lt;a href=&quot;https://blog.logrocket.com/vercel-json-render-dynamic-ui/&quot;&gt;Vercel&lt;/a&gt; account and use it to authenticate whichever model you connect with through the gateway.&lt;/p&gt;
&lt;p&gt;The second option is almost the same, except eve expects you to already have an AI Gateway API key.&lt;/p&gt;
&lt;p&gt;You’ll need to enter something like:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;AI_GATEWAY_API_KEY=vg_xxxxxxxxxxxxxxxxx&lt;/pre&gt;
&lt;p&gt;The last option, other providers, lets you provide your model’s API key and allows eve to connect directly to a provider like OpenAI or Anthropic. However, if you choose this option, you will need to create an environment variable for your provider’s key in the &lt;code&gt;.env.local&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; class=&quot;alignnone size-full wp-image-214658&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/S6K93464-1.png&quot; alt=&quot;&quot; width=&quot;1132&quot; height=&quot;361&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/S6K93464-1.png 1132w, https://blog.logrocket.com/wp-content/uploads/2026/08/S6K93464-1-300x96.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/08/S6K93464-1-1024x327.png 1024w, https://blog.logrocket.com/wp-content/uploads/2026/08/S6K93464-1-768x245.png 768w&quot; sizes=&quot;auto, (max-width: 1132px) 100vw, 1132px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then, set the model to the provider whose key you provided in the &lt;code&gt;agent.ts&lt;/code&gt; file. For example, if you want to use DeepSeek, you would set the model as follows:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { deepseek } from &quot;@ai-sdk/deepseek&quot;;
import { defineAgent } from &quot;eve&quot;;


export default defineAgent({
  model: deepseek(&quot;deepseek-chat&quot;),
});&lt;/pre&gt;
&lt;p&gt;By default, models are specified using a string. Typically, this is all you need when using Vercel’s AI Gateway, which supports hundreds of models out of the box and simplifies configuration. However, in this example, we are using a provider-specific SDK package from the Vercel AI SDK.&lt;/p&gt;
&lt;p&gt;While the core &lt;code&gt;ai&lt;/code&gt; package is installed during the initialization process, it does not include every individual provider package by default. You will need to install the specific provider’s package separately. In this case, we are using DeepSeek, so you must install its corresponding package:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npm install @ai-sdk/deepseek&lt;/pre&gt;
&lt;p&gt;You can check out &lt;a href=&quot;https://blog.logrocket.com/tanstack-vs-vercel-ai-library-react/&quot;&gt;Vercel’s AI SDK&lt;/a&gt; documentation to see the available AI SDK providers.&lt;/p&gt;
&lt;p&gt;Once you have completed the setup, the development server will spin up automatically and launch the interactive terminal UI, where you can prompt your agent right away, as shown in the GIF below.&lt;/p&gt;
&lt;p&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; class=&quot;alignnone size-full wp-image-214659&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/image3-1.gif&quot; alt=&quot;&quot; width=&quot;863&quot; height=&quot;425&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;If the development server doesn’t start automatically, you can start it by running the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npm run dev&lt;/pre&gt;
&lt;p&gt;Getting to this point should take less than 5 minutes. If you’ve built agents before, you know how much work is usually required to get here. With Eve, all it takes is running a command and providing your model keys.&lt;/p&gt;
&lt;h2 id=&quot;setting-eve-up-in-an-existing-app&quot;&gt;How to install Vercel eve in an existing project?&lt;/h2&gt;
&lt;p&gt;To set up eve in an existing application, you have two options: using the initialization command provided by Vercel or setting it up manually.&lt;/p&gt;
&lt;p&gt;To set it up manually, the first thing you need to do is add the following to your project’s &lt;code&gt;package.json&lt;/code&gt; file to declare a compatible Node.js runtime:&lt;/p&gt;
&lt;pre class=&quot;language-json hljs&quot;&gt;{
  &quot;engines&quot;: {
    &quot;node&quot;: &quot;24.x&quot;
  }
}&lt;/pre&gt;
&lt;p&gt;Then, install eve and its dependencies, the &lt;code&gt;ai&lt;/code&gt; and &lt;code&gt;zod&lt;/code&gt; packages, using the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npm install eve@latest ai zod&lt;/pre&gt;
&lt;p&gt;After the installation is complete, you can create an &lt;code&gt;agent/&lt;/code&gt; directory in the root folder of your project and add the necessary project files. We’ll look at these in the next section.&lt;/p&gt;
&lt;p&gt;Setting it up manually is useful for understanding how Eve connects to your application, but it is quicker to use the initializer because all you have to do is run the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;eve init .&lt;/pre&gt;
&lt;p&gt;This takes care of everything you would otherwise have to do manually, including creating the &lt;code&gt;agent/&lt;/code&gt;directory with a minimal agent configuration.&lt;/p&gt;
&lt;h2 id=&quot;creating-your-first-eve-agent&quot;&gt;How to build your first AI agent with Vercel eve?&lt;/h2&gt;
&lt;p&gt;Now that your eve project is set up, you can begin creating agents. First, however, you need to understand the project structure, since it’s the foundation of eve. In this section, we will examine the function of each folder and file and learn how to use them to build your first eve agent.&lt;/p&gt;
&lt;h3 id=&quot;agentts&quot;&gt;&lt;code&gt;agent.ts&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The first file you want to work on is the &lt;code&gt;agent.ts&lt;/code&gt; file. This is the core of an eve project and where you set the runtime configuration of the agent. It is where you choose an AI model, set system options, and customize the overall agent runtime using the &lt;code&gt;defineEngine&lt;/code&gt; function:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { defineAgent } from &quot;eve&quot;;


export default defineAgent({
  model: &quot;anthropic/claude-opus-4.8&quot;,
});&lt;/pre&gt;
&lt;p&gt;The configuration above uses Anthropic’s &lt;code&gt;claude-opus-3.5&lt;/code&gt; model. Note that if you chose a different provider during initialization, such as the DeepSeek integration we demonstrated earlier, your configuration might look a bit different.&lt;/p&gt;
&lt;p&gt;However, if you don’t plan to choose a model or add any additional configuration, you can omit the `agent.ts` file, and eve will default to &lt;code&gt;claude-sonnet-5&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For a full list of configuration options, refer to the official documentation&lt;/p&gt;
&lt;h3 id=&quot;instructionsmd&quot;&gt;&lt;code&gt;instructions.md&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The next thing you want to do is set a system prompt, which you can do by editing the &lt;code&gt;instructions.md&lt;/code&gt; file. This is where you define the instructions for your agent’s overall behavior, rules, and persona. If your agent is designed to function as a research analyst, your instructions might look like this:&lt;/p&gt;
&lt;pre class=&quot;language-markdown hljs&quot;&gt;You are a startup research analyst.


Your task is to discover companies related to the user&#39;s request and summarize them objectively.


Prioritize:


- Recently launched startups
- Funding announcements
- Product launches
- Team size
- Market category
- Unique differentiators
- Target customers


...&lt;/pre&gt;
&lt;p&gt;Eve automatically injects these instructions into every model call, ensuring your agent maintains its persona and follows your rules consistently.&lt;/p&gt;
&lt;h3 id=&quot;toolsts&quot;&gt;&lt;code&gt;tools/*.ts&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;tools/&lt;/code&gt; folder is where you define your agent’s capabilities and the tools it has at its disposal. You can create a tool by adding a TypeScript &lt;code&gt;(.ts)&lt;/code&gt; file to this folder and using eve’s defineTool function to define the tool’s description, &lt;code&gt;inputSchema&lt;/code&gt; (validated with Zod), and &lt;code&gt;execute()&lt;/code&gt; function.&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { defineTool } from &quot;eve/tools&quot;;
import { z } from &quot;zod&quot;;


export default defineTool({
  description: &quot;Retrieve information about a GitHub repository.&quot;,


  inputSchema: z.object({
    owner: z.string().describe(&quot;Repository owner&quot;),
    repo: z.string().describe(&quot;Repository name&quot;),
  }),


  async execute({ owner, repo }) {
    const response = await fetch(
      `https://api.github.com/repos/${owner}/${repo}`,
    );


    if (!response.ok) {
      throw new Error(&quot;Repository not found.&quot;);
    }


    const repository = await response.json();


    return {
      name: repository.name,
      description: repository.description,
      stars: repository.stargazers_count,
      forks: repository.forks_count,
      language: repository.language,
      url: repository.html_url,
    };
  },
});&lt;/pre&gt;
&lt;p&gt;In the example above, we created a GitHub tool that retrieves information about specific repositories.&lt;/p&gt;
&lt;p&gt;Any TypeScript file placed in the &lt;code&gt;tools/&lt;/code&gt; directory is automatically discovered, so no manual registration is required. Eve uses the filename as the tool’s name, which is how the model identifies and invokes it. For example, if you name your file &lt;code&gt;github.ts&lt;/code&gt;, the model will recognize it as &lt;code&gt;github&lt;/code&gt; when it needs to call it.&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;├── tools/
│   └── github.ts&lt;/pre&gt;
&lt;p&gt;This especially showcases the flexibility that eve offers. Most frameworks would require you to register these tools before they can be functional, with a lot of preceding boilerplate code.&lt;/p&gt;
&lt;p&gt;Now, if we ask the agent something like, “Tell me about the &lt;a href=&quot;https://blog.logrocket.com/unified-ai-interfaces-vercel-sdk/&quot;&gt;Vercel AI SDK&lt;/a&gt; repository”, it will return detailed information about the repository on GitHub, as shown in the image below.&lt;/p&gt;
&lt;p&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; class=&quot;alignnone size-full wp-image-214660&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/image2.png&quot; alt=&quot;&quot; width=&quot;1487&quot; height=&quot;641&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/image2.png 1487w, https://blog.logrocket.com/wp-content/uploads/2026/08/image2-300x129.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/08/image2-1024x441.png 1024w, https://blog.logrocket.com/wp-content/uploads/2026/08/image2-768x331.png 768w&quot; sizes=&quot;auto, (max-width: 1487px) 100vw, 1487px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;If you want to require approval each time the agent attempts to invoke a tool, you can do so by adding an &lt;code&gt;approval&lt;/code&gt; field to the tool definition like so:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;export default defineTool({
  description: &quot;Retrieve information about a GitHub repository.&quot;,


  inputSchema: z.object({
...
  }),


  approval: always(),


  async execute({ owner, repo }) {
...
    );


    return {
...
    };
  },&lt;/pre&gt;
&lt;p&gt;Now, whenever the agent attempts to invoke the tool, you will see a prompt requesting approval before execution proceeds.&lt;/p&gt;
&lt;p&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; class=&quot;alignnone size-full wp-image-214661&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/image1.png&quot; alt=&quot;&quot; width=&quot;688&quot; height=&quot;333&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/08/image1.png 688w, https://blog.logrocket.com/wp-content/uploads/2026/08/image1-300x145.png 300w&quot; sizes=&quot;auto, (max-width: 688px) 100vw, 688px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;skillsmd&quot;&gt;&lt;code&gt;skills/*.md&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;One limitation of building an agent from scratch is that the system prompt tends to bloat over time and overload the agent. Eve mitigates this by letting you create Markdown files with specific instructions and workflows in the `skills/` folder. Your agent only loads these files into context when they’re relevant.&lt;/p&gt;
&lt;p&gt;For example, if you want to add an &lt;code&gt;SQL&lt;/code&gt; skill to your agent’s knowledge base, you simply move the skill’s Markdown file into the `skills/` folder:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;agent/skills
└── sql.md&lt;/pre&gt;
&lt;p&gt;Now, eve automatically knows your agent has SQL expertise. Whenever it needs to work on anything SQL-related, this skill gets loaded into the context.&lt;/p&gt;
&lt;h3 id=&quot;subagents&quot;&gt;&lt;code&gt;subagents/*&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Subagents are independent child agents that your root agent can delegate focused tasks to. They run the task independently, then return the results to the root agent.&lt;/p&gt;
&lt;p&gt;So when your agent’s tasks get too large and become prone to errors, you can create subagents in the &lt;code&gt;subagents/&lt;/code&gt; folder to take the workload off your main agent.&lt;/p&gt;
&lt;p&gt;Each subfolder inside the &lt;code&gt;subagents/ &lt;/code&gt; folder is a subagent with the same structure as the parent directory. This means it has its own instructions, tools, and execution environment.&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;agent/subagents/researcher/
├── instructions.md
├── agent.ts
├── tools/&lt;/pre&gt;
&lt;p&gt;The files and folders we’ve covered so far are the core agent definitions, and they’re all you need to build a full-fledged AI agent. The rest, however, handles the production and infrastructure layers. These include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&lt;strong&gt;/channels/*&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt;:&lt;/strong&gt; This is where you configure where you want your agent to be deployed and the channels in which users can interact with your agent (e.g., Slack, Discord, Microsoft Teams, or custom HTTP APIs)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/connections/*&lt;/code&gt;&lt;/strong&gt;: This is where you connect your agent to external services via &lt;a href=&quot;https://blog.logrocket.com/introducing-the-logrocket-mcp/&quot;&gt;MCP servers&lt;/a&gt; or any HTTP API (e.g., Slack, Discord, Microsoft Teams, or custom HTTP APIs)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/schedules/&lt;/code&gt;&lt;/strong&gt;: This holds configurations for cron jobs or automated tasks that trigger the agent to run recurring background processes (e.g., daily research or clean-up tasks)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/sandbox/&lt;/code&gt;&lt;/strong&gt;: This is where you define eve’s sandbox config when you want to customize the isolated microVM used by the agent to execute code securely&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;production-use-cases&quot;&gt;How is Vercel using eve in production?&lt;/h2&gt;
&lt;p&gt;Eve is already used internally at Vercel across a range of production use cases that you can build around too.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;d0&lt;/code&gt;&lt;/strong&gt;: This is a data analyst agent Vercel used internally to answer thousands of questions monthly, and it scopes every query to the asker’s permission&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Autonomous SDR&lt;/strong&gt;: This is an AI sales representative that works new leads and follows up on its own. According to Vercel, it costs about $5,000/year to run, returning 32× its cost&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Athena&lt;/strong&gt;: This is a sales cockpit AI agent that answers pipeline and sales forecast questions by querying Snowflake and Salesforce using natural language&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vertex&lt;/strong&gt;: This is a customer support engineer agent that handles tickets across the help center, documentation, and Slack. It reportedly resolves 92% of support tickets automatically, escalating only the remainder to human support engineers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;draft0&lt;/code&gt;&lt;/strong&gt;: This is a content agent, and it is used throughout the company to review drafts before they reach the content team. It performs an initial editorial pass, identifies obvious issues, and analyzes what an article is about so editors can focus on higher-value feedback&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Vercel team has shipped even more with Eve, but these examples should give you a solid sense of what’s possible.&lt;/p&gt;
&lt;h2 id=&quot;eve-vs-manually-crafted-agent-stack&quot;&gt;Vercel eve vs. traditional agent stacks: Key differences&lt;/h2&gt;
&lt;p&gt;There are several ways eve supercharges agent development compared to traditional stacks, but these are the biggest distinctions:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Traditional agent development&lt;/th&gt;
&lt;th&gt;Building with eve&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Manually configure the project structure.&lt;/td&gt;
&lt;td&gt;Start with eve &lt;code&gt;init &lt;/code&gt; or install Eve into an existing project.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write and maintain a large system prompt in code.&lt;/td&gt;
&lt;td&gt;Store instructions in &lt;code&gt;instructions. md &lt;/code&gt; for easier editing and versioning.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Register every tool manually with the agent.&lt;/td&gt;
&lt;td&gt;Drop a tool into &lt;code&gt;agent/tools/&lt;/code&gt; and Eve discovers it automatically.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load all context into the prompt, regardless of relevance.&lt;/td&gt;
&lt;td&gt;Organize knowledge into &lt;code&gt;skills/&lt;/code&gt;, and Eve loads only the relevant skills.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execute AI-generated code in your own environment or build a sandbox yourself.&lt;/td&gt;
&lt;td&gt;AI-generated code runs in an isolated sandbox by default.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create your own system for coordinating multiple agents.&lt;/td&gt;
&lt;td&gt;Organize specialist agents under &lt;code&gt;subagents/&lt;/code&gt; and let Eve handle delegation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You build an agent from low-level building blocks.&lt;/td&gt;
&lt;td&gt;You describe your agent’s capabilities, and Eve provides the runtime and infrastructure.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you have experience with the &lt;a href=&quot;https://blog.logrocket.com/vercel-ai-elements/&quot;&gt;Vercel AI SDK&lt;/a&gt; and tool calling, you’ll appreciate how eve abstracts away much of the orchestration you currently write manually. It doesn’t try to replace the SDK. Instead, it gives you a higher-level runtime and conventions to ship production-ready agents.&lt;/p&gt;
&lt;p&gt;We’ve covered enough here to build your first agent. The examples were basic, but complexity doesn’t change the process with eve. The building steps are the same whether your agent is basic or advanced.&lt;/p&gt;
&lt;p&gt;However, there’s a lot more to eve than this article could cover, such as evals and more. I encourage you to check out the documentation to understand the framework better.&lt;/p&gt;



&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/nextjs/&quot;&gt;#nextjs&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/vercel-eve-ai-agents/</link><guid isPermaLink="false">https://blog.logrocket.com/vercel-eve-ai-agents/</guid><pubDate>Invalid Date</pubDate><author>David Omotayo</author></item><item><title>How to implement JWT authentication in NestJS</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#repository-technology&quot;&gt;Repository and technology&lt;/a&gt;
        &lt;ul data-toc-name=&quot;repository-technology&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#nestjs&quot;&gt;What is NestJS?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;nestjs&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#jwt-authentication-works-nestjs&quot;&gt;How JWT authentication works in NestJS&lt;/a&gt;
        &lt;ul data-toc-name=&quot;jwt-authentication-works-nestjs&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#getting-started&quot;&gt;Getting started&lt;/a&gt;
        &lt;ul data-toc-name=&quot;getting-started&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-user-module&quot;&gt;Creating the user module&lt;/a&gt;
        &lt;ul data-toc-name=&quot;creating-user-module&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-user-entity&quot;&gt;Creating the user entity&lt;/a&gt;
        &lt;ul data-toc-name=&quot;creating-user-entity&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-user-service&quot;&gt;Creating the user service&lt;/a&gt;
        &lt;ul data-toc-name=&quot;creating-user-service&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-dto&quot;&gt;Creating the DTO&lt;/a&gt;
        &lt;ul data-toc-name=&quot;creating-dto&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-user-controller&quot;&gt;Creating the user controller&lt;/a&gt;
        &lt;ul data-toc-name=&quot;creating-user-controller&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-auth-module&quot;&gt;Creating the auth module&lt;/a&gt;
        &lt;ul data-toc-name=&quot;creating-auth-module&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#configuring-jwt&quot;&gt;Configuring JWT&lt;/a&gt;
        &lt;ul data-toc-name=&quot;configuring-jwt&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-auth-service&quot;&gt;Creating the auth service&lt;/a&gt;
        &lt;ul data-toc-name=&quot;creating-auth-service&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-auth-guards&quot;&gt;Creating auth guards&lt;/a&gt;
        &lt;ul data-toc-name=&quot;creating-auth-guards&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-auth-controller&quot;&gt;Creating the auth controller&lt;/a&gt;
        &lt;ul data-toc-name=&quot;creating-auth-controller&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#testing-application&quot;&gt;Testing the application&lt;/a&gt;
        &lt;ul data-toc-name=&quot;testing-application&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#endpoints-role&quot;&gt;The endpoints and their role&lt;/a&gt;
        &lt;ul data-toc-name=&quot;endpoints-role&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#common-security-mistakes-avoid&quot;&gt;Common security mistakes to avoid&lt;/a&gt;
        &lt;ul data-toc-name=&quot;common-security-mistakes-avoid&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Editor’s note&lt;/strong&gt;: This article was reviewed and updated by &lt;a href=&quot;https://blog.logrocket.com/author/rosariodechiara/&quot;&gt;Rosario De Chiara&lt;/a&gt; on 3 August 2026 to modernize the NestJS JWT authentication flow, add DTO validation, improve password hashing guidance, and clarify the roles of guards, strategies, and protected routes.&lt;/em&gt;&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;730&quot; height=&quot;487&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2022/08/how-implement-jwt-authentication-nestjs-nocdn.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;Two Logos Over Green Background&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2022/08/how-implement-jwt-authentication-nestjs-nocdn.png 730w, https://blog.logrocket.com/wp-content/uploads/2022/08/how-implement-jwt-authentication-nestjs-nocdn-300x200.png 300w&quot; sizes=&quot;(max-width: 730px) 100vw, 730px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;Authentication is one of the most important parts of any backend application. Before an API returns protected data or allows a user to perform an account-specific action, it needs a reliable way to verify who that user is.&lt;/p&gt;
&lt;p&gt;In NestJS, a common way to do this is with JSON Web Tokens (JWTs). The user signs in with a username and password, the server validates those credentials, and the application returns a signed access token. The client then sends that token with future requests, usually in the &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header, so protected routes can verify the user without checking the password again.&lt;/p&gt;
&lt;p&gt;It is important to implement this flow carefully. OWASP lists identification and authentication failures among the top web application security risks, and weak authentication code can expose accounts, tokens, and sensitive user data.&lt;/p&gt;
&lt;p&gt;In this tutorial, we’ll build a basic JWT authentication flow in a NestJS API. We’ll use SQLite and TypeORM for persistence, Passport strategies for local and JWT authentication, DTOs for request validation, and &lt;code&gt;bcrypt&lt;/code&gt; for password hashing.&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;By the end, you’ll have the following endpoints:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Auth required&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/users/signup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a user account and hash the password&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/auth/login&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Validate credentials and return a JWT access token&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/auth/profile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return the authenticated user’s token payload&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;&lt;em&gt;Jump ahead:&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#repository-technology&quot;&gt;Repository and technology&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#nestjs&quot;&gt;What is NestJS?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#jwt-authentication-works-nestjs&quot;&gt;How JWT authentication works in NestJS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#getting-started&quot;&gt;Getting started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-user-module&quot;&gt;Creating the user module&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-user-entity&quot;&gt;Creating the user entity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-user-service&quot;&gt;Creating the user service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-dto&quot;&gt;Creating the DTO&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-user-controller&quot;&gt;Creating the user controller&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-auth-module&quot;&gt;Creating the auth module&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#configuring-jwt&quot;&gt;Configuring JWT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-auth-service&quot;&gt;Creating the auth service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-auth-guards&quot;&gt;Creating auth guards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#creating-auth-controller&quot;&gt;Creating the auth controller&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#testing-application&quot;&gt;Testing the application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#common-security-mistakes-avoid&quot;&gt;Common security mistakes to avoid&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;repository-technology&quot;&gt;Repository and technology&lt;/h2&gt;
&lt;p&gt;The source code for this project is available in this &lt;a href=&quot;https://github.com/rosdec/nestjs_jwt_example&quot;&gt;GitHub repository&lt;/a&gt;. The application uses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NestJS for the API framework&lt;/li&gt;
&lt;li&gt;SQLite as a local database&lt;/li&gt;
&lt;li&gt;TypeORM to model and query the user table&lt;/li&gt;
&lt;li&gt;Passport for local and JWT authentication strategies&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@nestjs/jwt&lt;/code&gt; to sign access tokens&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bcrypt&lt;/code&gt; to hash passwords&lt;/li&gt;
&lt;li&gt;&lt;code&gt;class-validator&lt;/code&gt; and &lt;code&gt;class-transformer&lt;/code&gt; to validate request DTOs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The example uses SQLite to keep local setup simple. In a production application, you could use another database, such as PostgreSQL, MySQL, or MongoDB, with the same general authentication flow.&lt;/p&gt;
&lt;h2 id=&quot;nestjs&quot;&gt;What is NestJS?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://nestjs.com/&quot;&gt;NestJS&lt;/a&gt; is a server-side framework for building Node.js applications. It is written in TypeScript and provides a structured application architecture around modules, controllers, services, decorators, dependency injection, and guards.&lt;/p&gt;
&lt;p&gt;NestJS is often compared to Angular because it uses similar concepts, including dependency injection and decorators. That structure makes it useful for APIs that need clear separation between routing, business logic, validation, persistence, and security.&lt;/p&gt;
&lt;p&gt;For authentication specifically, Nest integrates well with Passport through &lt;code&gt;@nestjs/passport&lt;/code&gt;. Passport handles authentication strategies, while Nest guards decide whether a given request can reach a route handler.&lt;/p&gt;
&lt;h2 id=&quot;jwt-authentication-works-nestjs&quot;&gt;How JWT authentication works in NestJS&lt;/h2&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;Before we write code, it helps to separate the authentication flow into three parts:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;Main NestJS pieces&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Signup&lt;/td&gt;
&lt;td&gt;The API receives a username and password, hashes the password, and stores the user&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UsersController&lt;/code&gt;, &lt;code&gt;UsersService&lt;/code&gt;, DTO validation, &lt;code&gt;bcrypt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Login&lt;/td&gt;
&lt;td&gt;The API validates the username and password and returns a signed access token&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LocalStrategy&lt;/code&gt;, &lt;code&gt;LocalAuthGuard&lt;/code&gt;, &lt;code&gt;AuthService&lt;/code&gt;, &lt;code&gt;JwtService&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protected request&lt;/td&gt;
&lt;td&gt;The API verifies the JWT and allows the request if the token is valid&lt;/td&gt;
&lt;td&gt;&lt;code&gt;JwtStrategy&lt;/code&gt;, &lt;code&gt;JwtAuthGuard&lt;/code&gt;, protected controller route&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;A JWT should not contain sensitive data like passwords. In this example, the token payload contains the user ID in the &lt;code&gt;sub&lt;/code&gt; claim and the username. Protected routes can then identify the authenticated user from the verified token payload.&lt;/p&gt;
&lt;h2 id=&quot;getting-started&quot;&gt;Getting started&lt;/h2&gt;
&lt;p&gt;To run the project locally, clone the repository, install dependencies, and copy the example environment file:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npm install
cp .env.example .env&lt;/pre&gt;
&lt;p&gt;On Windows, use &lt;code&gt;copy .env.example .env&lt;/code&gt; instead of &lt;code&gt;cp&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next, start the application in watch mode:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;nest start --watch&lt;/pre&gt;
&lt;p&gt;The application will restart automatically when you make changes, which is useful while building and testing the authentication flow.&lt;/p&gt;
&lt;h2 id=&quot;creating-user-module&quot;&gt;Creating the user module&lt;/h2&gt;
&lt;p&gt;To keep the application organized, create a dedicated users module:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;nest g module users&lt;/pre&gt;
&lt;p&gt;This command creates a &lt;code&gt;users&lt;/code&gt; folder with a &lt;code&gt;users.module.ts&lt;/code&gt; file and updates &lt;code&gt;app.module.ts&lt;/code&gt; for you.&lt;/p&gt;
&lt;h2 id=&quot;creating-user-entity&quot;&gt;Creating the user entity&lt;/h2&gt;
&lt;p&gt;Create a &lt;code&gt;user.entity.ts&lt;/code&gt; file in the &lt;code&gt;src/users/entities&lt;/code&gt; folder and add the following code:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from &#39;typeorm&#39;;

@Entity({ name: &#39;users&#39; })
export class User {
  @PrimaryGeneratedColumn(&#39;uuid&#39;)
  id!: string;

  @Column({ unique: true })
  username!: string;

  @Column()
  password!: string;

  @CreateDateColumn()
  createdAt!: Date;

  @UpdateDateColumn()
  updatedAt!: Date;
}&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;@Entity()&lt;/code&gt; decorator tells TypeORM to map this class to a database table. The &lt;code&gt;id&lt;/code&gt; field is generated as a UUID, &lt;code&gt;username&lt;/code&gt; is unique, and &lt;code&gt;password&lt;/code&gt; stores the hashed password. The &lt;code&gt;createdAt&lt;/code&gt; and &lt;code&gt;updatedAt&lt;/code&gt; fields are managed automatically by TypeORM.&lt;/p&gt;

&lt;p&gt;Next, update &lt;code&gt;users.module.ts&lt;/code&gt; so the &lt;code&gt;User&lt;/code&gt; entity is available through TypeORM:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { Module } from &#39;@nestjs/common&#39;;
import { TypeOrmModule } from &#39;@nestjs/typeorm&#39;;
import { User } from &#39;./entities/user.entity&#39;;
import { UsersController } from &#39;./users.controller&#39;;
import { UsersService } from &#39;./users.service&#39;;

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}&lt;/pre&gt;
&lt;p&gt;Exporting &lt;code&gt;UsersService&lt;/code&gt; is important because the auth module will need to use it when validating login credentials.&lt;/p&gt;
&lt;h2 id=&quot;creating-user-service&quot;&gt;Creating the user service&lt;/h2&gt;
&lt;p&gt;Create the users service with the Nest CLI:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;nest g service users&lt;/pre&gt;
&lt;p&gt;This command creates &lt;code&gt;users.service.ts&lt;/code&gt;. You can create the file manually, but the CLI keeps the module wiring consistent.&lt;/p&gt;
&lt;p&gt;Now add the following code to &lt;code&gt;users.service.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { ConflictException, Injectable } from &#39;@nestjs/common&#39;;
import { InjectRepository } from &#39;@nestjs/typeorm&#39;;
import { Repository } from &#39;typeorm&#39;;
import { User } from &#39;./entities/user.entity&#39;;

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepository: Repository&amp;lt;User&amp;gt;,
  ) {}

  async createUser(username: string, password: string): Promise&amp;lt;User&amp;gt; {
    const existingUser = await this.findByUsername(username);

    if (existingUser) {
      throw new ConflictException(&#39;Username already exists&#39;);
    }

    const user = this.usersRepository.create({ username, password });
    return this.usersRepository.save(user);
  }

  findByUsername(username: string): Promise&amp;lt;User | null&amp;gt; {
    return this.usersRepository.findOne({ where: { username } });
  }

  findById(id: string): Promise&amp;lt;User | null&amp;gt; {
    return this.usersRepository.findOne({ where: { id } });
  }
}&lt;/pre&gt;
&lt;p&gt;Here, &lt;code&gt;@InjectRepository(User)&lt;/code&gt; injects the TypeORM repository for the &lt;code&gt;User&lt;/code&gt; entity. The service checks for duplicate usernames before creating a user, then exposes lookup methods that the auth flow will use later.&lt;/p&gt;
&lt;h2 id=&quot;creating-dto&quot;&gt;Creating the DTO&lt;/h2&gt;
&lt;p&gt;In NestJS, a DTO, or Data Transfer Object, defines the shape of data a route expects to receive. DTOs are useful because they give your controllers a clear request contract and work with Nest’s validation pipeline.&lt;/p&gt;
&lt;p&gt;Generate an auth credentials DTO:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;nest generate class auth/dto/auth-credentials.dto --no-spec&lt;/pre&gt;
&lt;p&gt;Then add the following code to &lt;code&gt;auth/dto/auth-credentials.dto.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { IsNotEmpty, IsString, MinLength } from &#39;class-validator&#39;;

export class AuthCredentialsDto {
  @IsString()
  @IsNotEmpty()
  username!: string;

  @IsString()
  @IsNotEmpty()
  @MinLength(6)
  password!: string;
}&lt;/pre&gt;
&lt;p&gt;This DTO requires both &lt;code&gt;username&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt;, and it enforces a minimum password length.&lt;/p&gt;
&lt;p&gt;To make DTO validation run automatically, enable Nest’s &lt;code&gt;ValidationPipe&lt;/code&gt; in &lt;code&gt;main.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { ValidationPipe } from &#39;@nestjs/common&#39;;
import { NestFactory } from &#39;@nestjs/core&#39;;
import { AppModule } from &#39;./app.module&#39;;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;whitelist: true&lt;/code&gt; strips properties that are not defined in the DTO. &lt;code&gt;forbidNonWhitelisted: true&lt;/code&gt; rejects requests that include unexpected fields. &lt;code&gt;transform: true&lt;/code&gt; lets Nest transform incoming payloads into DTO instances.&lt;/p&gt;
&lt;h2 id=&quot;creating-user-controller&quot;&gt;Creating the user controller&lt;/h2&gt;
&lt;p&gt;Create a user controller to define the signup route:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;nest g controller users&lt;/pre&gt;
&lt;p&gt;Add the following code to &lt;code&gt;users.controller.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { Body, Controller, Post } from &#39;@nestjs/common&#39;;
import * as bcrypt from &#39;bcrypt&#39;;
import { AuthCredentialsDto } from &#39;../auth/dto/auth-credentials.dto&#39;;
import { User } from &#39;./entities/user.entity&#39;;
import { UsersService } from &#39;./users.service&#39;;

type SafeUser = Omit&amp;lt;User, &#39;password&#39;&amp;gt;;

@Controller(&#39;users&#39;)
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post(&#39;signup&#39;)
  async signUp(@Body() credentialsDto: AuthCredentialsDto): Promise&amp;lt;SafeUser&amp;gt; {
    const hashedPassword = await bcrypt.hash(credentialsDto.password, 10);
    const user = await this.usersService.createUser(
      credentialsDto.username,
      hashedPassword,
    );

    const { password, ...safeUser } = user;
    return safeUser;
  }
}&lt;/pre&gt;
&lt;p&gt;The controller receives the validated DTO, hashes the password with &lt;code&gt;bcrypt&lt;/code&gt;, and delegates user creation to &lt;code&gt;UsersService&lt;/code&gt;. Notice that the response removes the &lt;code&gt;password&lt;/code&gt; field before returning the user. Even though the value is hashed, password hashes should not be returned to clients.&lt;/p&gt;
&lt;h2 id=&quot;creating-auth-module&quot;&gt;Creating the auth module&lt;/h2&gt;
&lt;p&gt;Create the auth module with the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;nest g module auth&lt;/pre&gt;
&lt;p&gt;This command creates an &lt;code&gt;auth&lt;/code&gt; folder with an &lt;code&gt;auth.module.ts&lt;/code&gt; file and updates &lt;code&gt;app.module.ts&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;configuring-jwt&quot;&gt;Configuring JWT&lt;/h2&gt;
&lt;p&gt;Now we can add JWT support to the application. Install the required packages:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npm install @nestjs/jwt @nestjs/passport passport passport-local passport-jwt bcrypt @nestjs/config
npm install --save-dev @types/passport-local @types/passport-jwt&lt;/pre&gt;
&lt;p&gt;Next, create &lt;code&gt;auth/strategies/local.strategy.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { Injectable, UnauthorizedException } from &#39;@nestjs/common&#39;;
import { PassportStrategy } from &#39;@nestjs/passport&#39;;
import { Strategy } from &#39;passport-local&#39;;
import { AuthService } from &#39;../auth.service&#39;;

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super();
  }

  async validate(username: string, password: string) {
    const user = await this.authService.validateUser(username, password);

    if (!user) {
      throw new UnauthorizedException(&#39;Invalid credentials&#39;);
    }

    return user;
  }
}&lt;/pre&gt;
&lt;p&gt;The local strategy handles username/password authentication. By default, &lt;code&gt;passport-local&lt;/code&gt; expects &lt;code&gt;username&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt; fields in the request body.&lt;/p&gt;
&lt;p&gt;Next, create &lt;code&gt;auth/strategies/jwt.strategy.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { Injectable } from &#39;@nestjs/common&#39;;
import { ConfigService } from &#39;@nestjs/config&#39;;
import { PassportStrategy } from &#39;@nestjs/passport&#39;;
import { ExtractJwt, Strategy } from &#39;passport-jwt&#39;;

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.getOrThrow&amp;lt;string&amp;gt;(&#39;JWT_SECRET&#39;),
    });
  }

  async validate(payload: { sub: string; username: string }) {
    return { userId: payload.sub, username: payload.username };
  }
}&lt;/pre&gt;
&lt;p&gt;The JWT strategy extracts the bearer token from the &lt;code&gt;Authorization&lt;/code&gt; header, verifies the signature and expiration, and returns a user object that Nest attaches to &lt;code&gt;req.user&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then replace the code in &lt;code&gt;auth/auth.module.ts&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { Module } from &#39;@nestjs/common&#39;;
import { ConfigModule, ConfigService } from &#39;@nestjs/config&#39;;
import { JwtModule, JwtModuleOptions } from &#39;@nestjs/jwt&#39;;
import { PassportModule } from &#39;@nestjs/passport&#39;;
import { UsersModule } from &#39;../users/users.module&#39;;
import { AuthController } from &#39;./auth.controller&#39;;
import { AuthService } from &#39;./auth.service&#39;;
import { JwtStrategy } from &#39;./strategies/jwt.strategy&#39;;
import { LocalStrategy } from &#39;./strategies/local.strategy&#39;;

@Module({
  imports: [
    ConfigModule,
    UsersModule,
    PassportModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService): JwtModuleOptions =&amp;gt; ({
        secret: configService.getOrThrow&amp;lt;string&amp;gt;(&#39;JWT_SECRET&#39;),
        signOptions: {
          expiresIn: configService.get&amp;lt;string&amp;gt;(&#39;JWT_EXPIRES_IN&#39;) ?? &#39;1h&#39;,
        },
      }),
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, LocalStrategy, JwtStrategy],
})
export class AuthModule {}&lt;/pre&gt;
&lt;p&gt;Using &lt;code&gt;registerAsync()&lt;/code&gt; lets the JWT module read configuration from environment variables. Avoid hardcoding the JWT secret in source code. The repository includes an &lt;code&gt;.env.example&lt;/code&gt; file with placeholder values.&lt;/p&gt;
&lt;p&gt;A basic &lt;code&gt;.env&lt;/code&gt; file might look like this:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;JWT_SECRET=replace_this_with_a_long_random_secret
JWT_EXPIRES_IN=1h&lt;/pre&gt;
&lt;p&gt;For production, use a long, randomly generated secret and store it in your deployment platform’s secret manager.&lt;/p&gt;
&lt;h2 id=&quot;creating-auth-service&quot;&gt;Creating the auth service&lt;/h2&gt;
&lt;p&gt;Create the auth service and controller:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;nest generate service auth
nest generate controller auth&lt;/pre&gt;
&lt;p&gt;Then open &lt;code&gt;auth/auth.service.ts&lt;/code&gt; and add the following code:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { Injectable } from &#39;@nestjs/common&#39;;
import { JwtService } from &#39;@nestjs/jwt&#39;;
import * as bcrypt from &#39;bcrypt&#39;;
import { User } from &#39;../users/entities/user.entity&#39;;
import { UsersService } from &#39;../users/users.service&#39;;

@Injectable()
export class AuthService {
  constructor(
    private readonly usersService: UsersService,
    private readonly jwtService: JwtService,
  ) {}

  async validateUser(
    username: string,
    password: string,
  ): Promise&amp;lt;Pick&amp;lt;User, &#39;id&#39; | &#39;username&#39;&amp;gt; | null&amp;gt; {
    const user = await this.usersService.findByUsername(username);

    if (!user) {
      return null;
    }

    const passwordMatches = await bcrypt.compare(password, user.password);

    if (!passwordMatches) {
      return null;
    }

    return { id: user.id, username: user.username };
  }

  async login(user: Pick&amp;lt;User, &#39;id&#39; | &#39;username&#39;&amp;gt;) {
    const payload = { username: user.username, sub: user.id };

    return {
      access_token: await this.jwtService.signAsync(payload),
    };
  }
}&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;validateUser()&lt;/code&gt; checks the username and compares the submitted password with the stored password hash. If the credentials are valid, it returns only the fields needed for the JWT payload. &lt;code&gt;login()&lt;/code&gt; signs the payload and returns an access token.&lt;/p&gt;
&lt;h2 id=&quot;creating-auth-guards&quot;&gt;Creating auth guards&lt;/h2&gt;
&lt;p&gt;Guards decide whether a request can reach a route handler. Create &lt;code&gt;auth/guards/local-auth.guard.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { Injectable } from &#39;@nestjs/common&#39;;
import { AuthGuard } from &#39;@nestjs/passport&#39;;

@Injectable()
export class LocalAuthGuard extends AuthGuard(&#39;local&#39;) {}&lt;/pre&gt;
&lt;p&gt;Then create &lt;code&gt;auth/guards/jwt-auth.guard.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { Injectable } from &#39;@nestjs/common&#39;;
import { AuthGuard } from &#39;@nestjs/passport&#39;;

@Injectable()
export class JwtAuthGuard extends AuthGuard(&#39;jwt&#39;) {}&lt;/pre&gt;
&lt;p&gt;The local guard runs the local strategy on the login route. The JWT guard runs the JWT strategy on protected routes.&lt;/p&gt;
&lt;h2 id=&quot;creating-auth-controller&quot;&gt;Creating the auth controller&lt;/h2&gt;
&lt;p&gt;Now add the following code to &lt;code&gt;auth/auth.controller.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { Controller, Get, Post, Req, UseGuards } from &#39;@nestjs/common&#39;;
import { Request as ExpressRequest } from &#39;express&#39;;
import { AuthService } from &#39;./auth.service&#39;;
import { JwtAuthGuard } from &#39;./guards/jwt-auth.guard&#39;;
import { LocalAuthGuard } from &#39;./guards/local-auth.guard&#39;;

type LocalUser = { id: string; username: string };
type JwtUser = { userId: string; username: string };

@Controller(&#39;auth&#39;)
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @UseGuards(LocalAuthGuard)
  @Post(&#39;login&#39;)
  login(@Req() req: ExpressRequest &amp;amp; { user: LocalUser }) {
    return this.authService.login(req.user);
  }

  @UseGuards(JwtAuthGuard)
  @Get(&#39;profile&#39;)
  getProfile(@Req() req: ExpressRequest &amp;amp; { user: JwtUser }) {
    return req.user;
  }
}&lt;/pre&gt;
&lt;p&gt;When a user sends valid credentials to &lt;code&gt;/auth/login&lt;/code&gt;, the &lt;code&gt;LocalAuthGuard&lt;/code&gt; runs &lt;code&gt;LocalStrategy&lt;/code&gt;, which calls &lt;code&gt;AuthService.validateUser()&lt;/code&gt;. Passport attaches the returned user object to &lt;code&gt;req.user&lt;/code&gt;, and the controller passes it to &lt;code&gt;AuthService.login()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When a user requests &lt;code&gt;/auth/profile&lt;/code&gt;, the &lt;code&gt;JwtAuthGuard&lt;/code&gt; checks the bearer token. If the token is valid, the route returns the token payload.&lt;/p&gt;
&lt;h2 id=&quot;testing-application&quot;&gt;Testing the application&lt;/h2&gt;
&lt;p&gt;Start the application:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;nest start --watch&lt;/pre&gt;
&lt;p&gt;Then test the signup route in Postman by sending a &lt;code&gt;POST&lt;/code&gt; request to &lt;code&gt;localhost:3000/users/signup&lt;/code&gt; with the following JSON body:&lt;/p&gt;
&lt;pre class=&quot;language-json hljs&quot;&gt;{
  &quot;username&quot;: &quot;rosario&quot;,
  &quot;password&quot;: &quot;secret123&quot;
}&lt;/pre&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2022/08/postman-signup-route.png&quot; alt=&quot;Postman Signup Route&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;Next, test the login endpoint by sending a &lt;code&gt;POST&lt;/code&gt; request to &lt;code&gt;localhost:3000/auth/login&lt;/code&gt; with the same credentials:&lt;/p&gt;
&lt;pre class=&quot;language-json hljs&quot;&gt;{
  &quot;username&quot;: &quot;rosario&quot;,
  &quot;password&quot;: &quot;secret123&quot;
}&lt;/pre&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2022/08/post-request-endpoint.png&quot; alt=&quot;Post Request Endpoint&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;If the username and password exist in the database, the API returns an &lt;code&gt;access_token&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-json hljs&quot;&gt;{
  &quot;access_token&quot;: &quot;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...&quot;
}&lt;/pre&gt;
&lt;p&gt;To call the protected profile route, send a &lt;code&gt;GET&lt;/code&gt; request to &lt;code&gt;localhost:3000/auth/profile&lt;/code&gt; with this header:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;Authorization: Bearer &amp;lt;access_token&amp;gt;&lt;/pre&gt;
&lt;h2 id=&quot;endpoints-role&quot;&gt;The endpoints and their role&lt;/h2&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;Here is the complete endpoint flow:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns a basic greeting or health-check response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/users/signup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates a user account with a hashed password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/auth/login&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Authenticates a user and returns a JWT access token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/auth/profile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns the authenticated user payload from the verified JWT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;The signup flow is straightforward: the client sends a username and password to &lt;code&gt;/users/signup&lt;/code&gt;, the NestJS server hashes the password, stores the user in SQLite, and returns a &lt;code&gt;201 Created&lt;/code&gt; response.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_9FBAB9A37370BB30ED015346C7BEBE2B3B165652AE5C6E28EBF9E3CDCA2A8B88_1784643273345_Untitled+diagram-2026-07-21-141312.png&quot; alt=&quot;Signup Flow&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;The login flow starts when a registered user sends credentials to &lt;code&gt;/auth/login&lt;/code&gt;. The local strategy validates the credentials, and the auth service returns a signed JWT.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_9FBAB9A37370BB30ED015346C7BEBE2B3B165652AE5C6E28EBF9E3CDCA2A8B88_1784643422371_Untitled+diagram-2026-07-21-141342.png&quot; alt=&quot;Login Flow&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;Finally, the client sends that JWT to a protected endpoint.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_9FBAB9A37370BB30ED015346C7BEBE2B3B165652AE5C6E28EBF9E3CDCA2A8B88_1784888378523_Untitled+diagram-2026-07-24-101914.png&quot; alt=&quot;Protected Route Flow&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;For protected routes, the client and NestJS server are the main actors. The server does not need to check the user’s password again. Instead, it verifies the JWT signature and expiration. If the token is valid, the JWT strategy exposes the token payload to the route handler through &lt;code&gt;req.user&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;common-security-mistakes-avoid&quot;&gt;Common security mistakes to avoid&lt;/h2&gt;
&lt;p&gt;JWT authentication is easy to prototype, but several mistakes can make it unsafe in production:&lt;/p&gt;
&lt;ul&gt;
&lt;li style=&quot;padding-bottom: 10px;&quot;&gt;&lt;strong&gt;Returning password hashes to the client:&lt;/strong&gt; Hashes are not plaintext passwords, but they are still sensitive. Remove &lt;code&gt;password&lt;/code&gt; before returning a user object.&lt;/li&gt;
&lt;li style=&quot;padding-bottom: 10px;&quot;&gt;&lt;strong&gt;Hardcoding the JWT secret:&lt;/strong&gt; Keep secrets in environment variables or a secret manager. Do not commit them to source control.&lt;/li&gt;
&lt;li style=&quot;padding-bottom: 10px;&quot;&gt;&lt;strong&gt;Using weak or long-lived access tokens:&lt;/strong&gt; Short-lived access tokens reduce the blast radius if a token leaks. For longer sessions, use a refresh-token flow rather than extending access-token lifetime indefinitely.&lt;/li&gt;
&lt;li style=&quot;padding-bottom: 10px;&quot;&gt;&lt;strong&gt;Skipping DTO validation:&lt;/strong&gt; Validate login and signup payloads before they reach your service layer.&lt;/li&gt;
&lt;li style=&quot;padding-bottom: 10px;&quot;&gt;&lt;strong&gt;Confusing authentication and authorization:&lt;/strong&gt; JWT authentication proves who the user is. It does not decide what the user is allowed to do. Add role- or permission-based guards when different users need different access levels.&lt;/li&gt;
&lt;li style=&quot;padding-bottom: 10px;&quot;&gt;&lt;strong&gt;Ignoring token storage tradeoffs:&lt;/strong&gt; Bearer tokens in headers are straightforward for APIs, but frontend storage needs care. Local storage is vulnerable to token theft if your app has an XSS flaw. Cookies can reduce some exposure, but need &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;SameSite&lt;/code&gt;, and CSRF considerations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this tutorial, we implemented JWT authentication in a NestJS API using Passport, TypeORM, SQLite, DTO validation, and &lt;code&gt;bcrypt&lt;/code&gt; password hashing. The final application can create users, authenticate credentials, issue JWT access tokens, and protect routes with a JWT guard.&lt;/p&gt;
&lt;p&gt;The key pattern is to keep each responsibility separate: the users module owns persistence, the auth service owns credential validation and token signing, the local strategy handles login, and the JWT strategy protects authenticated routes. That structure is what makes the authentication flow easier to test and extend.&lt;/p&gt;
&lt;p&gt;For production, treat this as a foundation rather than a complete auth system. Add refresh tokens if users need long-lived sessions, enforce authorization rules for protected resources, store secrets outside source control, and consider whether headers or cookies are the safer token transport for your frontend architecture.&lt;/p&gt;
&lt;p&gt;To learn more, refer to the &lt;a href=&quot;https://docs.nestjs.com/security/authentication&quot;&gt;official NestJS authentication documentation&lt;/a&gt;.&lt;/p&gt;



&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/nestjs/&quot;&gt;#nestjs&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/</link><guid isPermaLink="false">https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/</guid><pubDate>Invalid Date</pubDate><author>Clara Ekekenta</author></item><item><title>A deep dive into React Fiber</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#what-is-react-fiber&quot;&gt;What is React Fiber?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-react-fiber&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#reacts-stack-reconciler&quot;&gt;What is React&#39;s stack reconciler?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;reacts-stack-reconciler&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#what-is-app&quot;&gt;What is &lt;code&gt;&amp;lt;App /&amp;gt;&lt;/code&gt;?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-app&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#why-does-react-use-elements-instead-of-dom-instances&quot;&gt;Why does React use elements instead of DOM instances?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;why-does-react-use-elements-instead-of-dom-instances&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#what-is-react-reconciliation&quot;&gt;What is React reconciliation?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-react-reconciliation&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#what-is-the-react-stack-reconciler&quot;&gt;What is the React stack reconciler?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-the-react-stack-reconciler&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#how-did-recursion-limit-reacts-old-reconciler&quot;&gt;How did recursion limit React&#39;s old reconciler?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-did-recursion-limit-reacts-old-reconciler&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#problems-with-dropped-frames&quot;&gt;What is frame rate and why does it cause UI jank?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;problems-with-dropped-frames&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#how-does-react-fiber-work&quot;&gt;How does React Fiber work under the hood?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-does-react-fiber-work&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#how-does-the-javascript-execution-stack-limit-rendering&quot;&gt;How does the JavaScript execution stack limit rendering?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-does-the-javascript-execution-stack-limit-rendering&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#singly-linked-list-of-fiber-nodes&quot;&gt;What is a singly-linked list of fiber nodes?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;singly-linked-list-of-fiber-nodes&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#type&quot;&gt;Type&lt;/a&gt;
        &lt;ul data-toc-name=&quot;type&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#key&quot;&gt;Key&lt;/a&gt;
        &lt;ul data-toc-name=&quot;key&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#child&quot;&gt;Child&lt;/a&gt;
        &lt;ul data-toc-name=&quot;child&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#sibling&quot;&gt;Sibling&lt;/a&gt;
        &lt;ul data-toc-name=&quot;sibling&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#return&quot;&gt;Return&lt;/a&gt;
        &lt;ul data-toc-name=&quot;return&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#pendingprops-and-memoizedprops&quot;&gt;&lt;code&gt;pendingProps&lt;/code&gt; and &lt;code&gt;memoizedProps&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;pendingprops-and-memoizedprops&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#pendingworkpriority&quot;&gt;&lt;code&gt;pendingWorkPriority&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;pendingworkpriority&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#alternate&quot;&gt;Alternate&lt;/a&gt;
        &lt;ul data-toc-name=&quot;alternate&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#output&quot;&gt;Output&lt;/a&gt;
        &lt;ul data-toc-name=&quot;output&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#render-phase&quot;&gt;What happens during React Fiber&#39;s render phase?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;render-phase&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#workloopsync&quot;&gt;&lt;code&gt;workLoopSync()&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;workloopsync&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#performunitofwork&quot;&gt;&lt;code&gt;performUnitOfWork()&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;performunitofwork&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#beginwork&quot;&gt;&lt;code&gt;beginWork()&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;beginwork&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#using-instancehandleclick&quot;&gt;Using &lt;code&gt;instance.handleClick()&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;using-instancehandleclick&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#render-phase&quot;&gt;What happens during React Fiber&#39;s commit phase?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;render-phase&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#overview-of-changes-and-improvements-since-react-v16&quot;&gt;What key features were added to React Fiber since v16?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;overview-of-changes-and-improvements-since-react-v16&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#concurrent-rendering&quot;&gt;What is concurrent rendering?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;concurrent-rendering&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#suspense&quot;&gt;How does suspense work with selective hydration in React?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;suspense&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#transitions&quot;&gt;What are transitions?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;transitions&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#usetransition&quot;&gt;How do you use &lt;code&gt;useTransition&lt;/code&gt;?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;usetransition&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#asynchronous-transitions-and-actions&quot;&gt;What are asynchronous transitions and actions in React 19?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;asynchronous-transitions-and-actions&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#deferred-values&quot;&gt;What is &lt;code&gt;useDeferredValue&lt;/code&gt;?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;deferred-values&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#activity&quot;&gt;What is the &lt;code&gt;&amp;lt;Activity&amp;gt;&lt;/code&gt; component in React 19.2?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;activity&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#supporting-external-stores-during-concurrent-rendering&quot;&gt;How does &lt;code&gt;useSyncExternalStore&lt;/code&gt; protect external state in concurrent React?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;supporting-external-stores-during-concurrent-rendering&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#automatic-batching&quot;&gt;What is automatic batching?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;automatic-batching&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#the-react-compiler-moved-optimization-to-build-time&quot;&gt;How does the React Compiler optimize code at build time?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-react-compiler-moved-optimization-to-build-time&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#react-fiber-vs-react-compiler-frequently-asked-questions&quot;&gt;React Fiber vs. React Compiler: Key differences &amp;amp; FAQs&lt;/a&gt;
        &lt;ul data-toc-name=&quot;react-fiber-vs-react-compiler-frequently-asked-questions&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Editor’s note: &lt;/strong&gt;This post was updated on 30 July 2026 by &lt;a href=&quot;https://blog.logrocket.com/author/davidomotayo/&quot;&gt;David Omotayo&lt;/a&gt; to reflect modern React architecture and updates to React Fiber from React v16 through React 19. Key additions include coverage of concurrent rendering, automatic batching, modern root APIs (&lt;code&gt;createRoot&lt;/code&gt;), Actions and asynchronous transitions in React 19, &lt;code&gt;useDeferredValue&lt;/code&gt;, the &lt;code&gt;Activity&lt;/code&gt; component introduced in React 19.2, expanded Suspense capabilities like selective hydration and sibling scheduling, and a new comparison of React Fiber and the React Compiler.&amp;nbsp;&lt;/em&gt;&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;730&quot; height=&quot;487&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/deep-dive-react-fiber.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;Deep Dive React Fiber&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/deep-dive-react-fiber.png 730w, https://blog.logrocket.com/wp-content/uploads/2019/11/deep-dive-react-fiber-300x200.png 300w&quot; sizes=&quot;(max-width: 730px) 100vw, 730px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p data-path-to-node=&quot;3,0&quot;&gt;Ever wondered what actually happens under the hood when you render a React application? We know that ReactDOM builds the DOM tree and displays your UI on screen, but how does React construct that tree, and how does it efficiently update it when state changes?&lt;/p&gt;
&lt;p data-path-to-node=&quot;3,1&quot;&gt;In this guide, we’ll explore React Fiber, React’s core reconciliation engine. We’ll look at how React built the DOM up through v15, the performance pitfalls of that early stack model, and how Fiber’s asynchronous architecture solved those issues from React v16 through React 19.&lt;/p&gt;
&lt;h2 id=&quot;what-is-react-fiber&quot;&gt;What is React Fiber?&lt;/h2&gt;
&lt;p&gt;React Fiber is an internal engine change geared to make React faster and smarter. The Fiber reconciler, which became the default reconciler for React 16 and above, is a complete rewrite of React’s reconciliation algorithm to solve some long-standing issues in React.&lt;/p&gt;
&lt;p&gt;Because Fiber is asynchronous, React can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pause, resume, and restart rendering work on components as new updates come in&lt;/li&gt;
&lt;li&gt;Reuse previously completed work and even abort it if not needed&lt;/li&gt;
&lt;li&gt;Split work into chunks and prioritize tasks based on importance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This change allows React to break away from the limits of the synchronous stack reconciler. Previously, you could add or remove items, for example, but it had to work until the stack was empty, and tasks couldn’t be interrupted.&lt;/p&gt;
&lt;p&gt;This change also allows React to fine-tune rendering components, ensuring that the most important updates happen as soon as possible. Now, to truly understand the powers of Fiber, let’s talk about the old reconciler: the stack reconciler.&lt;/p&gt;
&lt;h2 id=&quot;reacts-stack-reconciler&quot;&gt;What is React’s stack reconciler?&lt;/h2&gt;
&lt;p&gt;Let’s start with our familiar &lt;code&gt;ReactDOM.render(&amp;lt;App /&amp;gt;, document.getElementById(&#39;root&#39;))&lt;/code&gt;. The &lt;code&gt;ReactDOM&lt;/code&gt; module passes the &lt;code&gt;&amp;lt;App /&amp;gt;&lt;/code&gt; to the reconciler, but there are two questions here:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What does &lt;code&gt;&amp;lt;App /&amp;gt;&lt;/code&gt; refer to?&lt;/li&gt;
&lt;li&gt;What is the reconciler?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let’s unpack these two questions.&lt;/p&gt;
&lt;h3 id=&quot;what-is-app&quot;&gt;What is &lt;code&gt;&amp;lt;App /&amp;gt;&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;App /&amp;gt;&lt;/code&gt; is a React element, and “elements describe the tree.” &lt;a href=&quot;https://legacy.reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html#elements-describe-the-tree&quot;&gt;According to the React blog&lt;/a&gt;, “An element is a plain object describing a component instance or DOM node and its desired properties.”&lt;/p&gt;
&lt;p&gt;In other words, elements are not actual DOM nodes or component instances; they are a way to describe to React what kind of elements they are, what properties they hold, and who their children are.&lt;/p&gt;
&lt;p&gt;This is where React’s real power lies: React abstracts away the complex pieces of how to build, render, and manage the lifecycle of the actual DOM tree by itself, effectively making the life of the developer easier.&lt;/p&gt;
&lt;p&gt;To understand what this really means, let’s look at a traditional approach using object-oriented concepts.&lt;/p&gt;
&lt;h3 id=&quot;why-does-react-use-elements-instead-of-dom-instances&quot;&gt;Why does React use elements instead of DOM instances?&lt;/h3&gt;
&lt;p&gt;React elements let developers describe the desired interface without manually creating, updating, and destroying every DOM object.&lt;/p&gt;
&lt;p&gt;In the typical object-oriented programming world, developers must instantiate and manage the lifecycle of every DOM element. For instance, if you want to create a simple form and a submit button, the state management still requires some effort from the developer.&lt;/p&gt;
&lt;p&gt;Let’s assume the &lt;code&gt;Button&lt;/code&gt; component has a &lt;code&gt;isSubmitted&lt;/code&gt; state variable. The lifecycle of the &lt;code&gt;Button&lt;/code&gt; component looks something like the flowchart below, where each state must be managed by the app:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/button-component-lifecycle.png&quot; alt=&quot;Button Component Lifecycle Flowchart&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;The size of the flowchart and the number of lines of code grow exponentially as the number of state variables increases.&lt;/p&gt;

&lt;p&gt;So, React has elements to solve this problem; in React, there are two kinds of elements: the DOM element and the component element.&lt;/p&gt;
&lt;p&gt;The DOM element is an element that’s a string; for instance, &lt;code&gt;&amp;lt;button class=&quot;okButton&quot;&amp;gt; OK &amp;lt;/button&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The component element is a class or a function, for example, &lt;code&gt;&amp;lt;Button className=&quot;okButton&quot;&amp;gt; OK &amp;lt;/Button&amp;gt;&lt;/code&gt;, where &lt;code&gt;&amp;lt;Button&amp;gt;&lt;/code&gt; is either a class or a functional component. These are the typical React components we generally use.&lt;/p&gt;
&lt;p&gt;It is important to understand that both types are simple objects. They are mere descriptions of what must be rendered on the screen and don’t instigate rendering when you create and instantiate them.&lt;/p&gt;
&lt;h3 id=&quot;what-is-react-reconciliation&quot;&gt;What is React reconciliation?&lt;/h3&gt;
&lt;p&gt;React reconciliation is the process React uses to compare component trees and determine which parts of the rendered output need to change.&lt;/p&gt;
&lt;p&gt;This makes it easier for React to parse and traverse them to build the DOM tree. The actual rendering happens later when traversing finishes.&lt;/p&gt;
&lt;p&gt;When React encounters a class or a function component, it will ask that element what element it renders based on its props.&lt;/p&gt;
&lt;p&gt;For instance, if the &lt;code&gt;&amp;lt;App&amp;gt;&lt;/code&gt; component rendered the following, then React will ask the &lt;code&gt;&amp;lt;Form&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;Button&amp;gt;&lt;/code&gt; components what they render based on their corresponding props:&lt;/p&gt;
&lt;pre class=&quot;language-html hljs&quot;&gt;&amp;lt;Form&amp;gt;
  &amp;lt;Button&amp;gt;
    Submit
  &amp;lt;/Button&amp;gt;
&amp;lt;/Form&amp;gt;&lt;/pre&gt;
&lt;p&gt;So, if the &lt;code&gt;Form&lt;/code&gt; component is a functional component that looks like the following, React will call &lt;code&gt;render()&lt;/code&gt; to know what elements it renders and see that it renders a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; with a child&lt;/p&gt;

&lt;pre class=&quot;language-javascript hljs&quot;&gt;const Form = (props) =&amp;gt; {
  return(
    &amp;lt;div className=&quot;form&quot;&amp;gt;
      {props.form}
    &amp;lt;/div&amp;gt;
  )
}&lt;/pre&gt;
&lt;p&gt;React will repeat this process until it knows the underlying DOM tag elements for every component on the page.&lt;/p&gt;
&lt;p&gt;This exact process of recursively traversing a tree to know the underlying DOM tag elements of a React app’s component tree is known as reconciliation. By the end of the reconciliation, React knows the result of the DOM tree, and a renderer like &lt;code&gt;react-dom&lt;/code&gt; or &lt;code&gt;react-native&lt;/code&gt; applies the minimal set of changes necessary to update the DOM nodes. This means that when you call &lt;code&gt;ReactDOM.render()&lt;/code&gt; or &lt;code&gt;setState()&lt;/code&gt;, React performs reconciliation.&lt;/p&gt;
&lt;p&gt;In the case of &lt;code&gt;setState&lt;/code&gt;, it performs a traversal and determines what changed in the tree by diffing the new tree with the rendered tree. Then, it applies those changes to the current tree, thereby updating the state corresponding to the &lt;code&gt;setState()&lt;/code&gt; call.&lt;/p&gt;
&lt;p&gt;Note: &lt;code&gt;ReactDOM.render()&lt;/code&gt; is used in this section because it reflects the API available when Fiber was introduced. It was deprecated in React 18 and removed in React 19. Modern client-rendered applications use &lt;code&gt;createRoot()&lt;/code&gt; instead.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;import { createRoot } from &quot;react-dom/client&quot;;

const root = createRoot(document.getElementById(&quot;root&quot;));

root.render(&amp;lt;App /&amp;gt;);&lt;/pre&gt;
&lt;p&gt;Now that we understand what reconciliation is, let’s look at the pitfalls of this model.&lt;/p&gt;
&lt;h3 id=&quot;what-is-the-react-stack-reconciler&quot;&gt;What is the React stack reconciler?&lt;/h3&gt;
&lt;p&gt;The stack reconciler was React’s original reconciliation system. It recursively processed the component tree using the JavaScript call stack and could not pause once rendering began. Its name is derived from the stack data structure, which is a last-in, first-out (LIFO) mechanism.&lt;/p&gt;
&lt;p&gt;You might wonder what stack behavior has to do with what we just covered. As it turns out, because we are performing recursion, it has everything to do with the call stack.&lt;/p&gt;
&lt;h3 id=&quot;how-did-recursion-limit-reacts-old-reconciler&quot;&gt;How did recursion limit React’s old reconciler?&lt;/h3&gt;
&lt;p&gt;React’s old reconciler recursively rendered each component and then moved through its children until it reached the leaf DOM elements.&lt;/p&gt;
&lt;p&gt;To understand why that’s the case, let’s take a simple example and see what happens in the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Call_stack&quot;&gt;call stack&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;function fib(n) {
  if (n &amp;lt; 2){
    return n
  }
  return fib(n - 1) + fib (n - 2)
}

fib(10)&lt;/pre&gt;
&lt;p&gt;As we can see, the call stack pushes every call to &lt;code&gt;fib()&lt;/code&gt; into the stack until it pops &lt;code&gt;fib(1)&lt;/code&gt;, which is the first function call to return.&lt;/p&gt;
&lt;p&gt;Then, it continues pushing the recursive calls and pops again when it reaches the return statement. In this way, it effectively uses the call stack until &lt;code&gt;fib(3)&lt;/code&gt; returns and becomes the last item pop from the stack.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/call-stack-diagram.png&quot; alt=&quot;Call Stack Diagram&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;The reconciliation algorithm we just saw is a purely recursive algorithm. An update results in the entire subtree re-rendering immediately. While this works well, this has some limitations.&lt;/p&gt;
&lt;p&gt;As &lt;a href=&quot;https://github.com/acdlite/react-fiber-architecture&quot;&gt;Andrew Clark notes&lt;/a&gt;, in a UI, it’s not necessary for every update to apply immediately; in fact, doing so can be wasteful, causing frames to drop and degrading the user experience.&lt;/p&gt;
&lt;p&gt;Also, different types of updates have different priorities; an animation update must be completed faster than an update from a data store.&lt;/p&gt;
&lt;h3 id=&quot;problems-with-dropped-frames&quot;&gt;What is frame rate and why does it cause UI jank?&lt;/h3&gt;
&lt;p&gt;Frame rate is the frequency at which consecutive images appear on a display. Everything we see on our computer screens is composed of images or frames played on the screen at a rate that appears instantaneous to the eye.&lt;/p&gt;
&lt;p&gt;To understand what this means, think of the computer display as a flipbook and the pages of the flipbook as frames played at some rate when you flip them.&lt;/p&gt;
&lt;p&gt;Comparatively, a computer display is nothing but an automatic flipbook that plays continuously when things change on the screen.&lt;/p&gt;
&lt;p&gt;Typically, for a video to feel smooth and instantaneous to the human eye, the video must play at a rate of about 30 frames per second (FPS); anything higher gives a better experience.&lt;/p&gt;
&lt;p&gt;Most devices these days refresh their screens at 60 FPS, 1/60 = 16.67ms, which means a new frame displays every 16ms. This number is important because if the React renderer takes more than 16ms to render something on the screen, the browser drops that frame.&lt;/p&gt;
&lt;p&gt;In reality, however, the browser has housekeeping to do, so all your work must be completed within 10ms. When you fail to meet this budget, the frame rate drops, and the content judders on screen. This is often referred to as jank, and it negatively impacts the user’s experience.&lt;/p&gt;
&lt;p&gt;Of course, this is not a big concern for static and textual content. But in the case of displaying animations, this number is critical.&lt;/p&gt;
&lt;p&gt;If the React reconciliation algorithm traverses the entire app tree and re-renders it on every update, and that traversal takes longer than 16ms, the browser will drop frames.&lt;/p&gt;
&lt;p&gt;This is a big reason why many wanted updates categorized by priority and not blindly applying every update passed down to the reconciler. Also, many wanted the ability to pause and resume work in the next frame. This way, React could have better control over working with the 16ms rendering budget.&lt;/p&gt;
&lt;p&gt;This led the React team to rewrite the reconciliation algorithm, which is called Fiber. So, let’s look at how Fiber works to solve this problem.&lt;/p&gt;
&lt;h2 id=&quot;how-does-react-fiber-work&quot;&gt;How does React Fiber work under the hood?&lt;/h2&gt;
&lt;p&gt;Now that we know what motivated Fiber’s development, let’s summarize the features needed to achieve it. Again, I am referring to Andrew Clark’s notes for this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Assign priority to different types of work&lt;/li&gt;
&lt;li&gt;Pause work and come back to it later&lt;/li&gt;
&lt;li&gt;Abort work if it’s no longer needed&lt;/li&gt;
&lt;li&gt;Reuse previously completed work&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One of the challenges with implementing something like this is how the JavaScript engine works and the lack of threads in the language. To understand this, let’s briefly explore how the JavaScript engine handles execution contexts.&lt;/p&gt;
&lt;h3 id=&quot;how-does-the-javascript-execution-stack-limit-rendering&quot;&gt;How does the JavaScript execution stack limit rendering?&lt;/h3&gt;
&lt;p&gt;JavaScript normally executes synchronous functions until the call stack is empty. React’s old recursive reconciler relied on this stack, so it could not pause rendering to handle more urgent browser work.&lt;/p&gt;
&lt;p&gt;Whenever you write a function in JavaScript, the JavaScript engine creates a function execution context. Each time the JavaScript engine starts, it creates a global execution context that holds the global objects; for example, the &lt;code&gt;window&lt;/code&gt; object in the browser and the &lt;code&gt;global&lt;/code&gt; object in Node.js.&lt;/p&gt;
&lt;p&gt;JavaScript handles both contexts using a stack data structure also known as the execution stack. So, when you write something like this, the JavaScript engine first creates a global execution context and pushes it into the execution stack:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;function a() {
  console.log(&quot;i am a&quot;)
  b()
}

function b() {
  console.log(&quot;i am b&quot;)
}

a()&lt;/pre&gt;
&lt;p&gt;Then, it creates a function execution context for the &lt;code&gt;a()&lt;/code&gt; function. Since &lt;code&gt;b()&lt;/code&gt; is called inside &lt;code&gt;a()&lt;/code&gt;, it creates another function execution context for &lt;code&gt;b()&lt;/code&gt; and pushes it into the stack.&lt;/p&gt;
&lt;p&gt;When the &lt;code&gt;b()&lt;/code&gt; function returns, the engine destroys the context of &lt;code&gt;b()&lt;/code&gt;. When we exit the &lt;code&gt;a()&lt;/code&gt; function, the &lt;code&gt;a()&lt;/code&gt; context is destroyed. The stack during execution looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/execution-stack.png&quot; alt=&quot;Execution Stack Diagram&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;But, what happens when the browser makes an asynchronous event like an &lt;a href=&quot;https://blog.logrocket.com/axios-javascript/&quot;&gt;HTTP request&lt;/a&gt;? Does the JavaScript engine stack the execution stack and handle the asynchronous event, or does it wait until the event completes?&lt;/p&gt;
&lt;p&gt;The JavaScript engine does something different here: on top of the execution stack, the JavaScript engine has a queue data structure, also known as the event queue. The event queue handles asynchronous calls like HTTP or network events coming into the browser.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/event-queue-diagram.png&quot; alt=&quot;Event Queue Diagram&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;The JavaScript engine handles the items in the queue by waiting for the execution stack to empty. So, each time the execution stack empties, the JavaScript engine checks the event queue, pops items off the queue, and handles the event.&lt;/p&gt;
&lt;p&gt;It is important to note that the JavaScript engine checks the event queue only when the execution stack is empty or the only item in the execution stack is the global execution context.&lt;/p&gt;
&lt;p&gt;Although we call them asynchronous events, there is a subtle distinction here: the events are asynchronous with respect to when they arrive in the queue, but they’re not really asynchronous with respect to when they are actually handled.&lt;/p&gt;
&lt;p&gt;Coming back to our stack reconciler, when React traverses the tree, it does so in the execution stack. So, when the updates arrive, they arrive in the event queue (sort of). And only when the execution stack empties&amp;nbsp;are the updates handled.&lt;/p&gt;
&lt;p&gt;This is precisely the problem Fiber solves by almost reimplementing the stack with intelligent capabilities—pausing, resuming, and aborting, for example.&lt;/p&gt;
&lt;p&gt;Again referencing Andrew Clark, “Fiber is a reimplementation of the stack, specialized for React components. You can think of a single fiber as a virtual stack frame.&lt;/p&gt;
&lt;p&gt;“The advantage of reimplementing the stack is that you can keep stack frames in memory and execute them however and whenever you want. This is crucial for accomplishing the goals we have for scheduling.&lt;/p&gt;
&lt;p&gt;“Aside from scheduling, manually dealing with stack frames unlocks the potential for features such as concurrency and error boundaries.” We will cover these topics in future sections.&lt;/p&gt;
&lt;p&gt;In simple terms, a fiber represents a unit of work with its own virtual stack. In the previous implementation of the reconciliation algorithm, React created a tree of objects (React elements) that are immutable and traversed the tree recursively.&lt;/p&gt;
&lt;p&gt;In the current implementation, React creates a tree of fiber nodes that can mutate. The fiber node effectively holds the component’s state, props, and underlying DOM element it renders to.&lt;/p&gt;
&lt;p&gt;And, since fiber nodes can mutate, React doesn’t need to recreate every node for updates; it can simply clone and update the node when there is an update.&lt;/p&gt;
&lt;p&gt;In the case of a fiber tree, React doesn’t perform recursive traversal. Instead, it creates a singly-linked list and performs a parent-first, depth-first traversal.&lt;/p&gt;
&lt;h3 id=&quot;singly-linked-list-of-fiber-nodes&quot;&gt;What is a singly-linked list of fiber nodes?&lt;/h3&gt;
&lt;p&gt;A fiber node represents a stack frame and an instance of a React component. A fiber node comprises the following members:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Type&lt;/li&gt;
&lt;li&gt;Key&lt;/li&gt;
&lt;li&gt;Child&lt;/li&gt;
&lt;li&gt;Sibling&lt;/li&gt;
&lt;li&gt;Return&lt;/li&gt;
&lt;li&gt;Alternate&lt;/li&gt;
&lt;li&gt;Output&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;type&quot;&gt;Type&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;, for example, host components (strings), classes, or functions for composite components.&lt;/p&gt;
&lt;h3 id=&quot;key&quot;&gt;Key&lt;/h3&gt;
&lt;p&gt;The key is the same as the key we pass to the React element.&lt;/p&gt;
&lt;h3 id=&quot;child&quot;&gt;Child&lt;/h3&gt;
&lt;p&gt;Represents the element returned when we call &lt;code&gt;render()&lt;/code&gt; on the component:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const Name = (props) =&amp;gt; {
  return(
    &amp;lt;div className=&quot;name&quot;&amp;gt;
      {props.name}
    &amp;lt;/div&amp;gt;
  )
}&lt;/pre&gt;
&lt;p&gt;The child of &lt;code&gt;&amp;lt;Name&amp;gt;&lt;/code&gt; is &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; because it returns a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; element.&lt;/p&gt;
&lt;h3 id=&quot;sibling&quot;&gt;Sibling&lt;/h3&gt;
&lt;p&gt;Represents a case where &lt;code&gt;render&lt;/code&gt; returns a list of elements:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const Name = (props) =&amp;gt; {
  return([&amp;lt;Customdiv1 /&amp;gt;, &amp;lt;Customdiv2 /&amp;gt;])
}&lt;/pre&gt;
&lt;p&gt;In the above case, &lt;code&gt;&amp;lt;Customdiv1&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;Customdiv2&amp;gt;&lt;/code&gt; are the children of &lt;code&gt;&amp;lt;Name&amp;gt;&lt;/code&gt;, which is the parent. The two children form a singly-linked list.&lt;/p&gt;
&lt;h3 id=&quot;return&quot;&gt;Return&lt;/h3&gt;
&lt;p&gt;Return is the return back to the stack frame, which is a logical return back to the parent fiber node, and thus, represents the parent.&lt;/p&gt;
&lt;h3 id=&quot;pendingprops-and-memoizedprops&quot;&gt;&lt;code&gt;pendingProps&lt;/code&gt; and &lt;code&gt;memoizedProps&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Memoization means storing the values of a function execution’s result so you can use it later, thereby avoiding recomputation. &lt;code&gt;pendingProps&lt;/code&gt; represents the props passed to the component, and &lt;code&gt;memoizedProps&lt;/code&gt; initializes at the end of the execution stack, storing the props of this node. When the incoming &lt;code&gt;pendingProps&lt;/code&gt; are equal to &lt;code&gt;memoizedProps&lt;/code&gt;, it signals that the fiber’s previous output can be reused, preventing unnecessary work.&lt;/p&gt;
&lt;h3 id=&quot;pendingworkpriority&quot;&gt;&lt;code&gt;pendingWorkPriority&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pendingWorkPriority&lt;/code&gt; is a number indicating the priority of the work represented by the fiber. The &lt;code&gt;ReactPriorityLevel&lt;/code&gt; module lists the different priority levels and what they represent. With the exception of &lt;code&gt;NoWork&lt;/code&gt;, which is zero, a larger number indicates a lower priority.&lt;/p&gt;
&lt;p&gt;For example, you could use the following function to check if a fiber’s priority is at least as high as the given level. The scheduler uses the priority field to search for the next unit of work to perform:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;function matchesPriority(fiber, priority) {
  return fiber.pendingWorkPriority !== 0 &amp;amp;&amp;amp;
         fiber.pendingWorkPriority &amp;lt;= priority
}&lt;/pre&gt;
&lt;h3 id=&quot;alternate&quot;&gt;Alternate&lt;/h3&gt;
&lt;p&gt;At any time, a component instance has at most two fibers that correspond to it: the current fiber and the in-progress fiber. The alternate of the current fiber is the fiber in progress, and the alternate of the fiber in progress is the current fiber. The current fiber represents what is rendered already, and the in-progress fiber is conceptually the stack frame that has not returned.&lt;/p&gt;
&lt;h3 id=&quot;output&quot;&gt;Output&lt;/h3&gt;
&lt;p&gt;The output is the leaf nodes of a React application. They are specific to the rendering environment (for example, in a browser app, they are &lt;code&gt;div&lt;/code&gt; and &lt;code&gt;span&lt;/code&gt;). In JSX, they are denoted using lowercase tag names.&lt;/p&gt;
&lt;p&gt;Conceptually, the output of a fiber is the return value of a function. Every fiber eventually has an output, but the output is created only at the leaf nodes by host components. The output is then transferred up the tree.&lt;/p&gt;
&lt;p&gt;The output is eventually given to the renderer so that it can flush the changes to the rendering environment. For example, let’s look at how the fiber tree looks for an app with the following code:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const Parent1 = (props) =&amp;gt; {
  return([&amp;lt;Child11 /&amp;gt;, &amp;lt;Child12 /&amp;gt;])
}

const Parent2 = (props) =&amp;gt; {
  return(&amp;lt;Child21 /&amp;gt;)
}

class App extends Component {
  constructor(props) {
    super(props)
  }
  render() {
    &amp;lt;div&amp;gt;
      &amp;lt;Parent1 /&amp;gt;
      &amp;lt;Parent2 /&amp;gt;
    &amp;lt;/div&amp;gt;
  }
}

ReactDOM.render(&amp;lt;App /&amp;gt;, document.getElementById(&#39;root&#39;))&lt;/pre&gt;
&lt;p&gt;We can see that the fiber tree is composed of singly-linked lists of child nodes linked to each other (sibling relationship) and a linked list of parent-to-child relationships. This tree can be traversed using a &lt;a href=&quot;https://en.wikipedia.org/wiki/Depth-first_search&quot;&gt;depth-first search&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/fiber-tree-diagram.png&quot; alt=&quot;Fiber Tree Diagram&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;render-phase&quot;&gt;What happens during React Fiber’s render phase?&lt;/h3&gt;
&lt;p&gt;During the render phase, React builds or updates the work-in-progress fiber tree and determines which changes may need to be committed.&lt;/p&gt;
&lt;p&gt;To understand how React builds this tree and performs the reconciliation algorithm on it, let’s look at a unit test in the React source code with an attached debugger to follow the process; you can clone the React source code and navigate to this &lt;a href=&quot;https://github.com/react/react/tree/769b1f270e1251d9dbdce0fcbd9e92e502d059b8/packages/react-dom/src/__tests__&quot;&gt;directory&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To begin, add a Jest test and attach a debugger. This is a simple test for rendering a button with text. When you click the button, the app destroys the button and renders a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; with different text, so the text is a state variable here:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;&#39;use strict&#39;;

let React;
let ReactDOM;

describe(&#39;ReactUnderstanding&#39;, () =&amp;gt; {
  beforeEach(() =&amp;gt; {
    React = require(&#39;react&#39;);
    ReactDOM = require(&#39;react-dom&#39;);
  });

  it(&#39;works&#39;, () =&amp;gt; {
    let instance;

    class App extends React.Component {
      constructor(props) {
        super(props)
        this.state = {
          text: &quot;hello&quot;
        }
      }

      handleClick = () =&amp;gt; {
        this.props.logger(&#39;before-setState&#39;, this.state.text);
        this.setState({ text: &quot;hi&quot; })
        this.props.logger(&#39;after-setState&#39;, this.state.text);
      }

      render() {
        instance = this;
        this.props.logger(&#39;render&#39;, this.state.text);
        if(this.state.text === &quot;hello&quot;) {
        return (
          &amp;lt;div&amp;gt;
            &amp;lt;div&amp;gt;
              &amp;lt;button onClick={this.handleClick.bind(this)}&amp;gt;
                {this.state.text}
              &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;
        )} else {
          return (
            &amp;lt;div&amp;gt;
              hello
            &amp;lt;/div&amp;gt;
          )
        }
      }
    }
    const container = document.createElement(&#39;div&#39;);
    const logger = jest.fn();
    ReactDOM.render(&amp;lt;App logger={logger}/&amp;gt;, container);
    console.log(&quot;clicking&quot;);
    instance.handleClick();
    console.log(&quot;clicked&quot;);

    expect(container.innerHTML).toBe(
      &#39;&amp;lt;div&amp;gt;hello&amp;lt;/div&amp;gt;&#39;
    )

    expect(logger.mock.calls).toEqual(
      [[&quot;render&quot;, &quot;hello&quot;],
      [&quot;before-setState&quot;, &quot;hello&quot;],
      [&quot;render&quot;, &quot;hi&quot;],
      [&quot;after-setState&quot;, &quot;hi&quot;]]
    );
  })

});&lt;/pre&gt;
&lt;p&gt;In the initial render, React creates a current tree that renders initially. &lt;code&gt;createFiberFromTypesAndProps()&lt;/code&gt; is the function that creates each React fiber using the data from the specific React element. When we run the test, put a breakpoint at this function, and look at the call stack:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/function-call-stack-1.png&quot; alt=&quot;createFiberFromTypeAndProps() Call Stack&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;As we can see, the call stack tracks back to a &lt;code&gt;render()&lt;/code&gt; call, which eventually goes down to &lt;code&gt;createFiberFromTypeAndProps()&lt;/code&gt;. There are a few other functions that are of interest here: &lt;code&gt;workLoopSync()&lt;/code&gt;, &lt;code&gt;performUnitOfWork()&lt;/code&gt;, and &lt;code&gt;beginWork()&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;workloopsync&quot;&gt;&lt;code&gt;workLoopSync()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;workLoopSync()&lt;/code&gt; is when React starts building up the tree, starting with the &lt;code&gt;&amp;lt;App&amp;gt;&lt;/code&gt; node and recursively moving on to &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, which are the children of &lt;code&gt;&amp;lt;App&amp;gt;&lt;/code&gt;. The &lt;code&gt;workInProgress&lt;/code&gt; function holds a reference to the next fiber node that has work to do.&lt;/p&gt;
&lt;h3 id=&quot;performunitofwork&quot;&gt;&lt;code&gt;performUnitOfWork()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;performUnitOfWork()&lt;/code&gt; takes a fiber node as an input argument, gets the alternate of the node, and calls &lt;code&gt;beginWork()&lt;/code&gt;. This is the equivalent of starting the execution of the function execution contexts in the execution stack.&lt;/p&gt;
&lt;h3 id=&quot;beginwork&quot;&gt;&lt;code&gt;beginWork()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;When React builds the tree, &lt;code&gt;beginWork()&lt;/code&gt; simply leads up to &lt;code&gt;createFiberFromTypeAndProps()&lt;/code&gt; and creates the fiber nodes. React recursively performs work and eventually &lt;code&gt;performUnitOfWork()&lt;/code&gt; returns a null, indicating that it has reached the end of the tree.&lt;/p&gt;
&lt;h3 id=&quot;using-instancehandleclick&quot;&gt;Using &lt;code&gt;instance.handleClick()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Now, what happens when we perform &lt;code&gt;instance.handleClick()&lt;/code&gt;, which clicks the button and triggers a state update? In this case, React traverses the fiber tree, clones each node, and checks whether it needs to perform any work on each node.&lt;/p&gt;
&lt;p&gt;When we look at the call stack of this scenario, it looks something like this:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/function-call-stack-2.png&quot; alt=&quot;instance.handleClick() Call Stack&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;Although we did not see &lt;code&gt;completeUnitOfWork()&lt;/code&gt; and &lt;code&gt;completeWork()&lt;/code&gt; in the first call stack, we can see them here. Just like &lt;code&gt;performUnitOfWork()&lt;/code&gt; and &lt;code&gt;beginWork()&lt;/code&gt;, these two functions perform the completion part of the current execution, which means returning back to the stack.&lt;/p&gt;
&lt;p&gt;As we can see, together these four functions execute the unit of work and give control over the work being done currently, which is exactly what was missing in the stack reconciler.&lt;/p&gt;
&lt;p&gt;The image below shows that each fiber node is composed of four phases required to complete that unit of work.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/fiber-node-diagram.png&quot; alt=&quot;Fiber Node Diagram&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;It’s important to note here that each node doesn’t move to &lt;code&gt;completeUnitOfWork()&lt;/code&gt; until its children and siblings return &lt;code&gt;completeWork()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For instance, it starts with &lt;code&gt;performUnitOfWork()&lt;/code&gt; and &lt;code&gt;beginWork()&lt;/code&gt; for &lt;code&gt;&amp;lt;App/&amp;gt;&lt;/code&gt;, then moves on to &lt;code&gt;performUnitOfWork()&lt;/code&gt; and &lt;code&gt;beginWork()&lt;/code&gt; for &lt;code&gt;Parent1&lt;/code&gt;, and so on. It comes back and completes the work on &lt;code&gt;&amp;lt;App&amp;gt;&lt;/code&gt; once all the children of &lt;code&gt;&amp;lt;App/&amp;gt;&lt;/code&gt; complete the work.&lt;/p&gt;
&lt;p&gt;This is when React completes its render phase. The tree that’s newly built based on the &lt;code&gt;click()&lt;/code&gt; update is called the &lt;code&gt;workInProgress&lt;/code&gt; tree. This is basically the draft tree waiting to be rendered.&lt;/p&gt;
&lt;h3 id=&quot;render-phase&quot;&gt;What happens during React Fiber’s commit phase?&lt;/h3&gt;
&lt;p&gt;Once the render phase completes, React moves on to the commit phase, where it basically swaps the root pointers of the current tree and &lt;code&gt;workInProgress&lt;/code&gt; tree, thereby effectively swapping the current tree with the draft tree it built up based on the &lt;code&gt;click()&lt;/code&gt; update.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/commit-phase-diagram.png&quot; alt=&quot;Commit Phase Diagram&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;Not just that, React also reuses the old current tree after swapping the pointer from root to the &lt;code&gt;workInProgress&lt;/code&gt; tree. The net effect of this optimized process is a smooth transition from the previous state of the app to the next state and the next state, and so on.&lt;/p&gt;
&lt;p&gt;And what about the 16ms frame time? React effectively runs an internal timer for each unit of work being performed and constantly monitors this time limit while performing the work.&lt;/p&gt;
&lt;p&gt;The moment the time runs out, React pauses the current unit of work, hands the control back to the main thread, and lets the browser render whatever is finished at that point.&lt;/p&gt;
&lt;p&gt;Then, in the next frame, React picks up where it left off and continues building the tree. Then, when it has enough time, it commits the &lt;code&gt;workInProgress&lt;/code&gt; tree and completes the render.&lt;/p&gt;
&lt;h2 id=&quot;overview-of-changes-and-improvements-since-react-v16&quot;&gt;What key features were added to React Fiber since v16?&lt;/h2&gt;
&lt;p&gt;Fiber changed React from a synchronous tree traversal into a schedulable rendering system. Modern React builds on that foundation by allowing work to be prioritized, interrupted, streamed, moved between server and client environments, and optimized at build time.&lt;/p&gt;
&lt;p&gt;Here are the key improvements and changes to fiber since React v16:&lt;/p&gt;
&lt;h3 id=&quot;concurrent-rendering&quot;&gt;What is concurrent rendering?&lt;/h3&gt;
&lt;p&gt;The most significant update to React Fiber since its inception is concurrent rendering. It is a foundational enhancement to React’s rendering model that builds on the capabilities of React Fiber to provide interruptible rendering.&lt;/p&gt;
&lt;p&gt;Concurrent rendering lets React prepare certain updates in the background and interrupt them when more urgent work arrives. In other words, React can begin rendering one update, interrupt it to handle a more urgent update, and then later restart or abandon the earlier render.&lt;/p&gt;
&lt;p&gt;This is a stark contrast to the traditional synchronous rendering model, where React must complete each render in a single, uninterrupted transaction before responding to other work.&lt;/p&gt;
&lt;p&gt;Although concurrency was part of the original vision for Fiber, React 18 and React 19 made its capabilities broadly available through APIs such as transitions, Suspense, &lt;code&gt;useDeferredValue&lt;/code&gt;, and Activity, along with the new root API.&lt;/p&gt;
&lt;h3 id=&quot;suspense&quot;&gt;How does suspense work with selective hydration in React?&lt;/h3&gt;
&lt;p&gt;Suspense is a feature that is closely tied to concurrent mode and was initially introduced with React Fiber in v16, but its capabilities were greatly expanded with updates in React 17 and 18.&lt;/p&gt;
&lt;p&gt;The feature has undergone significant improvements since its initial release. It was originally designed to manage lazy loading (code splitting) of components using &lt;code&gt;React.lazy&lt;/code&gt;, but its functionality has expanded, especially with the introduction of concurrent rendering in v18.&lt;/p&gt;
&lt;p&gt;Now, Suspense plays a key role in handling asynchronous operations like data fetching and offers more granular control over UI rendering during these operations. You can declaratively specify what React should show when a part of the tree is not yet ready to render.&lt;/p&gt;
&lt;p&gt;This is done using a &lt;code&gt;fallback&lt;/code&gt; option, which renders a component or string while the asynchronous task is in operation:&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;&amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading data...&amp;lt;/div&amp;gt;}&amp;gt;
  &amp;lt;SomeComponent /&amp;gt;
&amp;lt;/Suspense&amp;gt;&lt;/pre&gt;
&lt;p&gt;Another key improvement is streaming in server-side rendering (SSR) and selective hydration. Instead of waiting for the server to finish rendering the full page before sending its HTML to the browser, Suspense boundaries allow React to divide the page into independent sections. The server can send an initial HTML shell first and stream the remaining content as it becomes ready.&lt;/p&gt;
&lt;p&gt;With selective hydration, Suspense boundaries divide the tree into smaller hydration units. React then hydrates these sections independently, allowing some controls to become interactive while other sections are still waiting for code or data.&lt;/p&gt;
&lt;p&gt;Suspense continues to evolve in React 19, particularly in how it schedules sibling work. When a component suspends, React can commit the nearest fallback sooner and then schedule another render to pre-warm lazy resources in the remaining sibling tree. This allows the fallback to appear sooner without preventing other resources from beginning to load.&lt;/p&gt;
&lt;p&gt;The Suspense API is nuanced. If you’d like to learn more about it, check out our &lt;a href=&quot;https://blog.logrocket.com/react-suspense-data-fetching/&quot;&gt;comprehensive article&lt;/a&gt; on Suspense.&lt;/p&gt;
&lt;h3 id=&quot;transitions&quot;&gt;What are transitions?&lt;/h3&gt;
&lt;p&gt;Transitions are one of the main APIs for using concurrent rendering. They let you distinguish between urgent and non-urgent state updates.&lt;/p&gt;
&lt;p&gt;Urgent updates include tasks like user inputs or animations where immediate feedback is expected. Non-urgent updates, like rendering a list of search results, are low priority, and slight delays are acceptable.&lt;/p&gt;
&lt;p&gt;The transitions API comprises a &lt;code&gt;useTransition&lt;/code&gt; hook which allows you to mark state updates as not urgent, as every state update not marked is considered urgent by default.&lt;/p&gt;
&lt;h3 id=&quot;usetransition&quot;&gt;How do you use &lt;code&gt;useTransition&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;useTransition&lt;/code&gt; hook handles transitions between UI states in a non-blocking manner. It returns an array with two values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;isPending&lt;/code&gt; — A boolean indicating if the transition is in progress&lt;/li&gt;
&lt;li&gt;&lt;code&gt;startTransition&lt;/code&gt; — A function to start the transition&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, consider a page with tabs where one tab takes a long time to render:&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;import { useState, useTransition } from &quot;react&quot;;
import About from &quot;./About&quot;;
import Posts from &quot;./Posts&quot;;

export default function TabContainer() {
  const [activeTab, setActiveTab] = useState(&quot;about&quot;);
  const [isPending, startTransition] = useTransition();

  function selectTab(nextTab) {
    startTransition(() =&amp;gt; {
      setActiveTab(nextTab);
    });
  }
  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; selectTab(&quot;about&quot;)}&amp;gt; About &amp;lt;/button&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; selectTab(&quot;posts&quot;)}&amp;gt; Posts &amp;lt;/button&amp;gt;
      {isPending &amp;amp;&amp;amp; &amp;lt;p&amp;gt;Loading tab...&amp;lt;/p&amp;gt;}
      {activeTab === &quot;about&quot; ? &amp;lt;About /&amp;gt; : &amp;lt;Posts /&amp;gt;}
    &amp;lt;/&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;The state update inside &lt;code&gt;startTransition&lt;/code&gt; is treated as non-urgent. If the user clicks another button while React is rendering the &lt;code&gt;Posts&lt;/code&gt; tab, React can interrupt the first render and handle the new interaction.&lt;/p&gt;
&lt;p&gt;Transitions do not delay the function passed to &lt;code&gt;startTransition&lt;/code&gt;. React calls that function immediately. Only the state updates scheduled while it runs are marked as transitions.&lt;/p&gt;
&lt;p&gt;This means that wrapping an expensive JavaScript calculation in &lt;code&gt;startTransition&lt;/code&gt; does not move the calculation into the background:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;startTransition(() =&amp;gt; {
  // This calculation still runs immediately
  const filteredItems = items.filter(expensiveFilter);
  setFilteredItems(filteredItems);
});&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;filter()&lt;/code&gt; call still blocks the main thread until it returns. &lt;code&gt;startTransition&lt;/code&gt; only changes the priority of the React state update.&lt;/p&gt;
&lt;h3 id=&quot;asynchronous-transitions-and-actions&quot;&gt;What are asynchronous transitions and actions in React 19?&lt;/h3&gt;
&lt;p&gt;In React 19, functions passed to &lt;code&gt;startTransition&lt;/code&gt; are called an Action. An Action can perform asynchronous work, such as a request, while &lt;code&gt;isPending&lt;/code&gt; represents the pending interaction.&lt;/p&gt;&lt;hr&gt;
&lt;iframe id=&quot;podrocket-plug&quot; src=&quot;https://open.spotify.com/embed/playlist/4j70ZVQNj5Lk3gtyCG5VWk?utm_source=generator&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot; allow=&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; loading=&quot;lazy&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;/iframe&gt;
&lt;hr&gt;
&lt;style&gt;
#podrocket-plug {
  border-radius: 12px;
  width: 75%;
  height: 352px;
  margin: 1rem auto;
  display: block;
}

&lt;/style&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;function SettingsForm() {
  const [settings, setSettings] = useState(initialSettings);
  const [isPending, startTransition] = useTransition();
  function saveSettings(nextSettings) {
    startTransition(async () =&amp;gt; {
      const savedSettings = await updateSettings(nextSettings);
      startTransition(() =&amp;gt; {
        setSettings(savedSettings);
      });
    });
  }
  return (
    &amp;lt;button
      disabled={isPending}
      onClick={() =&amp;gt; saveSettings({ theme: &quot;dark&quot; })}
    &amp;gt;
      {&quot; &quot;}
      {isPending ? &quot;Saving...&quot; : &quot;Save settings&quot;}{&quot; &quot;}
    &amp;lt;/button&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;State updates that occur after an &lt;code&gt;await&lt;/code&gt; currently need another &lt;code&gt;startTransition&lt;/code&gt; call to remain transition updates. This is a known limitation caused by React losing the transition context across the asynchronous boundary.&lt;/p&gt;
&lt;p&gt;Async transitions connect concurrent rendering with newer React features such as &lt;code&gt;Actions&lt;/code&gt;, &lt;code&gt;form Actions&lt;/code&gt;, &lt;code&gt;useActionState&lt;/code&gt;, and &lt;code&gt;useOptimistic&lt;/code&gt;. They allow React to coordinate pending UI, asynchronous work, and the final render as one interaction.&lt;/p&gt;
&lt;h3 id=&quot;deferred-values&quot;&gt;What is &lt;code&gt;useDeferredValue&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;useDeferredValue&lt;/code&gt; hook lets one part of the UI update later than another part. It is useful when a value changes urgently, but a component that depends on that value is slow to render.&lt;/p&gt;
&lt;p&gt;For example, an input should reflect each keystroke immediately, but a large list of search results can update at a lower priority:&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;import { Suspense, useDeferredValue, useState } from &quot;react&quot;;
export default function SearchPage() {
  const [query, setQuery] = useState(&quot;&quot;);
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  return (
    &amp;lt;&amp;gt;
      {&quot; &quot;}
      &amp;lt;input
        value={query}
        onChange={(event) =&amp;gt; setQuery(event.target.value)}
        placeholder=&quot;Search&quot;
      /&amp;gt;{&quot; &quot;}
      &amp;lt;div style={{ opacity: isStale ? 0.5 : 1 }}&amp;gt;
        {&quot; &quot;}
        &amp;lt;Suspense fallback={&amp;lt;p&amp;gt;Loading results...&amp;lt;/p&amp;gt;}&amp;gt;
          {&quot; &quot;}
          &amp;lt;SearchResults query={deferredQuery} /&amp;gt;{&quot; &quot;}
        &amp;lt;/Suspense&amp;gt;{&quot; &quot;}
      &amp;lt;/div&amp;gt;{&quot; &quot;}
    &amp;lt;/&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;In this example, when &lt;code&gt;query&lt;/code&gt; changes, React first renders the input with the latest value while &lt;code&gt;deferredQuery&lt;/code&gt; keeps its previous value.&lt;/p&gt;
&lt;p&gt;React then attempts another render in the background using the latest value. This background render is interruptible. If the user types again before it finishes, React can abandon it and restart with the newest value.&lt;/p&gt;
&lt;p&gt;Unlike traditional debouncing, &lt;code&gt;useDeferredValue&lt;/code&gt; does not wait for a fixed amount of time. Instead, React attempts the deferred render as soon as it can.&lt;/p&gt;
&lt;p&gt;The main difference between &lt;code&gt;useTransition&lt;/code&gt; and &lt;code&gt;useDeferredValue&lt;/code&gt; is where they are applied.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use a transition when you control the state update and can wrap its setter in &lt;code&gt;startTransition&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;useDeferredValue&lt;/code&gt; when you receive a value through props, a Hook, or another source and want a slower part of the tree to lag behind it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;activity&quot;&gt;What is the &lt;code&gt;&amp;lt;Activity&amp;gt;&lt;/code&gt; component in React 19.2?&lt;/h3&gt;
&lt;p&gt;React 19.2 introduced the &lt;code&gt;&amp;lt;Activity&amp;gt;&lt;/code&gt; component as another feature built on concurrent rendering. Activity lets React hide and restore part of the UI without discarding its internal state.&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;import { Activity, useState } from &quot;react&quot;;
export default function Tabs() {
  const [activeTab, setActiveTab] = useState(&quot;home&quot;);
  return (
    &amp;lt;&amp;gt;
      {&quot; &quot;}
      &amp;lt;button onClick={() =&amp;gt; setActiveTab(&quot;home&quot;)}&amp;gt; Home &amp;lt;/button&amp;gt;{&quot; &quot;}
      &amp;lt;button onClick={() =&amp;gt; setActiveTab(&quot;settings&quot;)}&amp;gt; Settings &amp;lt;/button&amp;gt;{&quot; &quot;}
      &amp;lt;Activity mode={activeTab === &quot;home&quot; ? &quot;visible&quot; : &quot;hidden&quot;}&amp;gt;
        {&quot; &quot;}
        &amp;lt;Home /&amp;gt;{&quot; &quot;}
      &amp;lt;/Activity&amp;gt;{&quot; &quot;}
      &amp;lt;Activity mode={activeTab === &quot;settings&quot; ? &quot;visible&quot; : &quot;hidden&quot;}&amp;gt;
        {&quot; &quot;}
        &amp;lt;Settings /&amp;gt;{&quot; &quot;}
      &amp;lt;/Activity&amp;gt;{&quot; &quot;}
    &amp;lt;/&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;When an Activity boundary is visible, React renders it normally and mounts its Effects. When it becomes hidden, React hides its DOM content, cleans up its Effects, and preserves its component and DOM state. Updates to hidden content can still be rendered, but they are scheduled at a lower priority than visible work. When the Activity becomes visible again, React restores the preserved state and recreates its Effects.&lt;/p&gt;
&lt;p&gt;Activity clearly demonstrates how Fiber can preserve completed work, lower its priority while it is hidden, and reuse it when it becomes relevant again.&lt;/p&gt;
&lt;p&gt;Together, transitions, Suspense, selective hydration, streaming server rendering, Activity, and deferred values expose different aspects of React’s concurrent rendering model.&lt;/p&gt;
&lt;h3 id=&quot;supporting-external-stores-during-concurrent-rendering&quot;&gt;How does &lt;code&gt;useSyncExternalStore&lt;/code&gt; protect external state in concurrent React?&lt;/h3&gt;
&lt;p&gt;Concurrent rendering introduces an important challenge for state stored outside React. Because React can pause while rendering a component tree, an external store may change during that pause. If different components read different versions of the store, React could produce an inconsistent user interface.&lt;/p&gt;
&lt;p&gt;React 18 introduced &lt;code&gt;useSyncExternalStore&lt;/code&gt; to give React a reliable way to subscribe to external stores and read a consistent snapshot of their state:&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;import { useSyncExternalStore } from &quot;react&quot;;
function OnlineStatus() {
  const isOnline = useSyncExternalStore(
    subscribe,
    getSnapshot,
    getServerSnapshot,
  );
  return &amp;lt;p&amp;gt;{isOnline ? &quot;Online&quot; : &quot;Offline&quot;}&amp;lt;/p&amp;gt;;
}&lt;/pre&gt;
&lt;p&gt;Here, the first argument subscribes to changes in the external source. The second returns its current value, and the optional third argument provides the value used during server rendering.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;useSyncExternalStore&lt;/code&gt; is especially useful for state-management libraries and browser APIs whose values can change outside React’s update system.&lt;/p&gt;
&lt;h3 id=&quot;automatic-batching&quot;&gt;What is automatic batching?&lt;/h3&gt;
&lt;p&gt;Automatic batching is a key feature that is the direct result of the underlying changes in React Fiber. It helps to reduce the number of re-renders that happen when a state changes by allowing React to group multiple state updates into a single re-render.&lt;/p&gt;
&lt;p&gt;For example, when you call multiple &lt;code&gt;setState&lt;/code&gt; functions in a single render cycle, React automatically batches them together into a single update. This can be observed in the code example below:&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;function MyComponent() {
  const [count, setCount] = useState(0);

  const handleClick = () =&amp;gt; {
    // Multiple state updates within a single event handler
    setCount(count + 1);
    setCount(count + 2); 
  };

  console.log(&quot;Rendering&quot;)

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;p&amp;gt;Count: {count}&amp;lt;/p&amp;gt;
      &amp;lt;button onClick={handleClick}&amp;gt;Increment&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;handleClick&lt;/code&gt; function calls &lt;code&gt;setCount&lt;/code&gt; twice, once to increment the count by &lt;code&gt;1&lt;/code&gt; and then again to increment it by &lt;code&gt;2&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;React automatically batches these two state updates into a single update. This means that only one re-render will occur, and “rendering” will only log once in the terminal.&lt;/p&gt;
&lt;p&gt;Before the release of React v18, batched updates were performed inside the React event handler. This meant that outside of React’s lifecycle (such as inside a &lt;code&gt;setTimeout&lt;/code&gt; or a promise), updates would not be batched automatically. With React Fiber’s improved update in v18, updates are batched across all contexts, including asynchronous code, &lt;code&gt;setTimeout&lt;/code&gt;, promises, and native event handlers.&lt;/p&gt;
&lt;h3 id=&quot;the-react-compiler-moved-optimization-to-build-time&quot;&gt;How does the React Compiler optimize code at build time?&lt;/h3&gt;
&lt;p&gt;The React Compiler complements Fiber by adding build-time memoization that reduces the amount of work Fiber needs to perform at runtime.&lt;/p&gt;
&lt;p&gt;Earlier, we saw that a fiber node stores both &lt;code&gt;pendingProps&lt;/code&gt; and &lt;code&gt;memoizedProps&lt;/code&gt; during reconciliation to decide whether work needs to be done for a fiber.&lt;/p&gt;
&lt;p&gt;By comparing &lt;code&gt;pendingProps&lt;/code&gt; with &lt;code&gt;memoizedProps&lt;/code&gt;, React can determine if the inputs to a component have changed. If they are the same and there are no other updates affecting the fiber, React can skip re-rendering that part of the tree and reuse the previous result. This process is known as memoization.&lt;/p&gt;
&lt;p&gt;Memoization happens at runtime after React has already received an update and entered the reconciliation process. Fiber then decides whether part of the tree can be reused.&lt;/p&gt;
&lt;p&gt;Hooks such as &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, and &lt;code&gt;useCallback&lt;/code&gt; can help React memoize components, values, or functions where appropriate.&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;import { memo, useCallback, useMemo } from &quot;react&quot;;
const ProductList = memo(function ProductList({ products, onSelect }) {
  const availableProducts = useMemo(() =&amp;gt; {
    return products.filter((product) =&amp;gt; product.inStock);
  }, [products]);
  const handleSelect = useCallback(
    (productId) =&amp;gt; {
      onSelect(productId);
    },
    [onSelect],
  );
  return (
    &amp;lt;ul&amp;gt;
      {&quot; &quot;}
      {availableProducts.map((product) =&amp;gt; (
        &amp;lt;Product key={product.id} product={product} onSelect={handleSelect} /&amp;gt;
      ))}{&quot; &quot;}
    &amp;lt;/ul&amp;gt;
  );
});&lt;/pre&gt;
&lt;p&gt;Each hook handles a different part of memoization:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;React.memo&lt;/code&gt;&lt;/strong&gt;: lets React skip rendering &lt;code&gt;ProductList&lt;/code&gt; when its props have not changed&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&lt;strong&gt;useMemo&lt;/strong&gt;&lt;/code&gt;: caches the filtered products&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;useCallback&lt;/code&gt;&lt;/strong&gt;: preserves the function reference between renders&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Although these APIs help reduce the amount of work that reaches Fiber, developers still have to decide what to memoize, where to memoize it, and keep dependency arrays up to date.&lt;/p&gt;
&lt;p&gt;The React Compiler was introduced to reduce this manual work by adding another layer of optimization. Instead of waiting until runtime to identify unnecessary work, the compiler analyzes components and Hooks during the build process and automatically inserts memoization where it is safe to do so.&lt;/p&gt;
&lt;p&gt;This means we can rewrite the same component without the manual memoization hooks:&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;function ProductList({ products, onSelect }) {
  const availableProducts = products.filter((product) =&amp;gt; product.inStock);
  function handleSelect(productId) {
    onSelect(productId);
  }
  return (
    &amp;lt;ul&amp;gt;
      {&quot; &quot;}
      {availableProducts.map((product) =&amp;gt; (
        &amp;lt;Product key={product.id} product={product} onSelect={handleSelect} /&amp;gt;
      ))}{&quot; &quot;}
    &amp;lt;/ul&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;During the build process, the compiler analyzes how values depend on props, state, and other values in the component. It can then generate cache checks that reuse &lt;code&gt;availableProducts&lt;/code&gt;, &lt;code&gt;handleSelect&lt;/code&gt;, JSX elements, or child component output as long as their relevant inputs remain unchanged.&lt;/p&gt;
&lt;p&gt;This does not mean the compiler stores the final DOM or bypasses reconciliation. The compiled component still runs within React’s normal rendering system. Fiber still creates and reconciles the work-in-progress tree and decides what should be committed.&lt;/p&gt;
&lt;p&gt;Because these two systems serve different purposes, it is easy to mix up their roles. The table below breaks down their key responsibilities and answers common questions about how they work together.&lt;/p&gt;
&lt;h3 id=&quot;react-fiber-vs-react-compiler-frequently-asked-questions&quot;&gt;React Fiber vs. React Compiler: Key differences &amp;amp; FAQs&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;React Fiber&lt;/th&gt;
&lt;th&gt;React Compiler&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;When does each system operate?&lt;/td&gt;
&lt;td&gt;Operates at runtime while React renders and reconciles the component tree&lt;/td&gt;
&lt;td&gt;Operates primarily at build time, analyzing and transforming the application code before it runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What information does each system rely on?&lt;/td&gt;
&lt;td&gt;Uses runtime data such as Fiber props, state, context, update priorities, lanes, and previously completed work&lt;/td&gt;
&lt;td&gt;Uses static analysis of components, Hooks, values, functions, JSX, and their dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How much developer involvement is required for optimization?&lt;/td&gt;
&lt;td&gt;React handles reconciliation automatically, but developers often need to manually add &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, and &lt;code&gt;useCallback&lt;/code&gt; to optimize performance&lt;/td&gt;
&lt;td&gt;The compiler handles optimization by adding much of this memoization automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What is the relationship between each system and concurrent rendering?&lt;/td&gt;
&lt;td&gt;Fiber acts as the scheduler that can pause, restart, prioritize, or abandon rendering work&lt;/td&gt;
&lt;td&gt;The compiler reduces the overall amount of work that the concurrent renderer needs to execute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do they affect the DOM?&lt;/td&gt;
&lt;td&gt;Fiber eventually commits the necessary DOM mutations through the renderer&lt;/td&gt;
&lt;td&gt;The compiler does not update the DOM directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does the React Compiler replace React Fiber?&lt;/td&gt;
&lt;td&gt;No. Fiber remains React’s core runtime reconciler&lt;/td&gt;
&lt;td&gt;Compiled components still render through Fiber at runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;React Fiber transformed React from a rigid tree-traversal tool into an intelligent, schedulable rendering engine. As React continues to evolve through v19 and beyond, Fiber remains the key mechanism powering it all. I hope you enjoyed reading this post.&lt;/p&gt;



&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/2026/&quot;&gt;#2026&lt;/a&gt;
        &lt;/li&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/react/&quot;&gt;#react&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/deep-dive-react-fiber/</link><guid isPermaLink="false">https://blog.logrocket.com/deep-dive-react-fiber/</guid><pubDate>Invalid Date</pubDate><author>Karthik Kalyanaraman</author></item><item><title>Skybridge: Build ChatGPT apps and MCP connectors</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#why-use-skybridge&quot;&gt;Why use Skybridge?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;why-use-skybridge&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#what-problem-does-skybridge-solve&quot;&gt;What problem does Skybridge solve?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-problem-does-skybridge-solve&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-does-skybridge-speed-up-mcp-app-development&quot;&gt;How does Skybridge speed up MCP app development?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-does-skybridge-speed-up-mcp-app-development&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#what-do-you-need-before-using-skybridge&quot;&gt;What do you need before using Skybridge?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-do-you-need-before-using-skybridge&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-create-a-skybridge-project&quot;&gt;How do you create a Skybridge project?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-create-a-skybridge-project&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#what-is-the-skybridge-developer-dashboard&quot;&gt;What is the Skybridge developer dashboard?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-the-skybridge-developer-dashboard&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-build-a-chatgpt-app-with-skybridge&quot;&gt;How do you build a ChatGPT app with Skybridge?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-build-a-chatgpt-app-with-skybridge&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-create-mcp-tools-in-skybridge&quot;&gt;How do you create MCP tools in Skybridge?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-create-mcp-tools-in-skybridge&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-enable-end-to-end-type-safety-in-skybridge&quot;&gt;How do you enable end-to-end type safety in Skybridge?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-enable-end-to-end-type-safety-in-skybridge&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-build-a-react-view-in-skybridge&quot;&gt;How do you build a React view in Skybridge?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-build-a-react-view-in-skybridge&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-add-more-tools-to-a-skybridge-app&quot;&gt;How do you add more tools to a Skybridge app?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-add-more-tools-to-a-skybridge-app&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-register-a-book-hotel-tool&quot;&gt;How do you register a book-hotel tool?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-register-a-book-hotel-tool&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-register-a-confirm-booking-tool&quot;&gt;How do you register a confirm-booking tool?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-register-a-confirm-booking-tool&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-create-the-booking-view-in-skybridge&quot;&gt;How do you create the booking view in Skybridge?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-create-the-booking-view-in-skybridge&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-test-a-skybridge-app&quot;&gt;How do you test a Skybridge app?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-test-a-skybridge-app&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#what-are-skybridges-limitations&quot;&gt;What are Skybridge&#39;s limitations?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-are-skybridges-limitations&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;Chatbots have moved beyond text-based Q&amp;amp;A to become execution environments. Today, developers use them as runtime hosts where tools execute, UI widgets render inline, and user interactions feed back to the model in real time.&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;895&quot; height=&quot;597&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/Skybridge.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/Skybridge.png 895w, https://blog.logrocket.com/wp-content/uploads/2026/07/Skybridge-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/07/Skybridge-768x512.png 768w&quot; sizes=&quot;(max-width: 895px) 100vw, 895px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;However, the developer experience is fragmented. OpenAI uses a proprietary Apps SDK for ChatGPT, while Anthropic Claude follows the Model Context Protocol (MCP). Building and maintaining separate integrations for each platform increases maintenance costs.&lt;/p&gt;
&lt;p&gt;Skybridge addresses these challenges. It is an open-source React framework that abstracts host-specific differences. You write one application and deploy it to ChatGPT, Claude, or any MCP-compatible client.&lt;/p&gt;
&lt;p&gt;This article demonstrates how to build a unified AI app from scratch, moving from basic tool calls to an interactive, state-synchronized interface.&lt;/p&gt;
&lt;h2 id=&quot;why-use-skybridge&quot;&gt;Why use Skybridge?&lt;/h2&gt;
&lt;p&gt;If you are wondering about Skybridge, check out the troubles it takes away from developers.&lt;/p&gt;
&lt;h3 id=&quot;what-problem-does-skybridge-solve&quot;&gt;What problem does Skybridge solve?&lt;/h3&gt;
&lt;p&gt;Each major AI platform comes with its own unique runtime behaviors, protocol specifications, and widget embedding rules. Skybridge acts as a write-once, run-anywhere abstraction layer that automatically manages protocol bridging and host-specific nuances, allowing a single React app to deploy effortlessly across &lt;a href=&quot;https://blog.logrocket.com/ux-design/19-chatgpt-prompts-for-faster-better-ux-research/&quot;&gt;ChatGPT&lt;/a&gt;, &lt;a href=&quot;https://blog.logrocket.com/getting-started-claude-4-api-developers-walkthrough/&quot;&gt;Claude&lt;/a&gt;, and VS Code.&lt;/p&gt;
&lt;h3 id=&quot;how-does-skybridge-speed-up-mcp-app-development&quot;&gt;How does Skybridge speed up MCP app development?&lt;/h3&gt;
&lt;p&gt;Testing an &lt;a href=&quot;https://blog.logrocket.com/introducing-the-logrocket-mcp/&quot;&gt;MCP&lt;/a&gt; app usually involves a tedious loop: deploying your app, opening a manual tunnel, connecting to a live chatbot, and constantly triggering model responses to check your changes. Skybridge eliminates this friction with a dedicated developer dashboard, which we’ll explore during our testing phase.&lt;/p&gt;
&lt;h2 id=&quot;what-do-you-need-before-using-skybridge&quot;&gt;What do you need before using Skybridge?&lt;/h2&gt;
&lt;p&gt;Ensure you have these tools and accounts ready:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js 24 or later&lt;/li&gt;
&lt;li&gt;Package manager: npm, pnpm, yarn, or bun&lt;/li&gt;
&lt;li&gt;ChatGPT account with developer access&lt;/li&gt;
&lt;li&gt;Claude account with connector access&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;how-do-you-create-a-skybridge-project&quot;&gt;How do you create a Skybridge project?&lt;/h2&gt;
&lt;p&gt;We will start by scaffolding a new project.&lt;/p&gt;
&lt;p&gt;Open your terminal and run the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npm create skybridge@latest hotel-app
cd hotel-app
npm install&lt;/pre&gt;
&lt;p&gt;This command creates an Express-based MCP server and a Vite-powered React application.&lt;/p&gt;
&lt;p&gt;The above command sets up a boilerplate with an Express-based MCP server and a Vite-powered React application.&lt;/p&gt;
&lt;p&gt;Skybridge offers direct integration with the Vite ecosystem.&lt;/p&gt;
&lt;p&gt;You can check out &lt;code&gt;vite.config.ts&lt;/code&gt; file for further configuration if you need to.&lt;/p&gt;

&lt;h2 id=&quot;what-is-the-skybridge-developer-dashboard&quot;&gt;What is the Skybridge developer dashboard?&lt;/h2&gt;
&lt;p&gt;Skybridge provides a local developer dashboard at &lt;code&gt;http://localhost:3000&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This comes with the following tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Alpic Playground&lt;/strong&gt;: Local sandbox with hot module replacement (HMR). Iterate on React widgets without connecting to a live AI model&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integrated Secure Tunnel&lt;/strong&gt;: Exposes your local server through a temporary public URL with one click. Paste this URL into ChatGPT or Claude connector settings&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Beacon Audit Tool:&lt;/strong&gt; Scans app metadata and security policies. It catches common rejection triggers before store submission&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;how-do-you-build-a-chatgpt-app-with-skybridge&quot;&gt;How do you build a ChatGPT app with Skybridge?&lt;/h2&gt;
&lt;p&gt;For this project, we will build a hotel booking app that works inside AI agents.&lt;/p&gt;
&lt;p&gt;We will start by setting up the MCP server and registering a tool to search for hotels.&lt;/p&gt;
&lt;h3 id=&quot;how-do-you-create-mcp-tools-in-skybridge&quot;&gt;How do you create MCP tools in Skybridge?&lt;/h3&gt;
&lt;p&gt;Create a server.ts file and register &lt;code&gt;search-hotels&lt;/code&gt; tool like so:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { McpServer } from &quot;skybridge/server&quot;;
import { z } from &quot;zod&quot;;

const server = new McpServer({
  name: &quot;hotel-booking-app&quot;,
  version: &quot;1.0.0&quot;
}, {});

server.registerTool({
  name: &quot;search-hotels&quot;,
  description: &quot;Searches for available hotels in a city.&quot;,
  inputSchema: {
    location: z.string().describe(&quot;The city to search in&quot;)
  },
  view: {
    component: &quot;hotel-results&quot;,
    description: &quot;Displays a list of available hotels.&quot;
  }
}, async ({ location }) =&amp;gt; {
  const hotels = [
    { id: &quot;h1&quot;, name: &quot;Grand Plaza&quot;, price: 200, rating: 4.5, location },
    { id: &quot;h2&quot;, name: &quot;Seaside Resort&quot;, price: 150, rating: 4.2, location },
  ];

  return {
    structuredContent: { hotels }, // Data passed to React
    content: [{ type: &quot;text&quot;, text: `Found ${hotels.length} hotels in ${location}.` }],
    isError: false,
  };
});

export type AppType = typeof server;&lt;/pre&gt;
&lt;p&gt;We have just defined a tool contract. The &lt;code&gt;view&lt;/code&gt; property tells the Skybridge runtime to render the &lt;code&gt;hotel-results&lt;/code&gt; component when the model calls &lt;code&gt;search-hotels&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;You can further refine the tool’s behavior with the &lt;code&gt;annotations&lt;/code&gt; property:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;server.registerTool({
  name: &quot;search-hotels&quot;,
  annotations: {
    title: &quot;Available Hotels&quot;,
  },
  // rest of config
});&lt;/pre&gt;
&lt;p&gt;Annotations help the AI host display readable titles in chat logs. Skybridge Beacon Audit Tool also checks this metadata as part of quality validation. For this demo, hotel data is hardcoded.&lt;/p&gt;
&lt;p&gt;Next, build the React component that renders search results for the discovery view.&lt;/p&gt;
&lt;h3 id=&quot;how-do-you-enable-end-to-end-type-safety-in-skybridge&quot;&gt;How do you enable end-to-end type safety in Skybridge?&lt;/h3&gt;
&lt;p&gt;Before we set up the discovery view, create a &lt;a href=&quot;https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/user/Desktop/articleHelp/demo/src/helpers.ts&quot;&gt; helpers.ts&lt;/a&gt; file.&lt;/p&gt;
&lt;p&gt;This file will provide the view with the exact tool input and output types from the server contract.&lt;/p&gt;
&lt;p&gt;This is the content of the file:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { generateHelpers } from &quot;skybridge/web&quot;;
import type { AppType } from &quot;./server.js&quot;;

export const { useToolInfo, useCallTool } = generateHelpers&amp;lt;AppType&amp;gt;();&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;generateHelpers&lt;/code&gt; uses your &lt;code&gt;McpServer&lt;/code&gt; TypeScript type to provide autocomplete and validation for tool names, inputs, and outputs in the views.&lt;/p&gt;

&lt;h3 id=&quot;how-do-you-build-a-react-view-in-skybridge&quot;&gt;How do you build a React view in Skybridge?&lt;/h3&gt;
&lt;p&gt;The discovery view is the first interface the user sees after the model calls &lt;code&gt;search-hotels&lt;/code&gt; tool.&lt;/p&gt;
&lt;p&gt;Create a &lt;code&gt;hotel.tsx&lt;/code&gt; file with the following code:&lt;/p&gt;
&lt;pre class=&quot;language-tsx hljs&quot;&gt;import { useState } from &quot;react&quot;;
import { useLayout } from &quot;skybridge/web&quot;;
import { useToolInfo } from &quot;../helpers.js&quot;;
import Booking from &quot;./booking.js&quot;;
import { Star, MapPin, Hotel, ArrowRight, Sparkles, Compass } from &quot;lucide-react&quot;;

export default function HotelResults() {
  const { theme } = useLayout();
  const { output } = useToolInfo&amp;lt;&quot;search-hotels&quot;&amp;gt;();
  const hotels = output?.hotels || [];
  const [selectedHotel, setSelectedHotel] = useState&amp;lt;{ id: string; name: string } | null&amp;gt;(null);

  if (selectedHotel) {
    return (
      &amp;lt;Booking
        initialHotelId={selectedHotel.id}
        initialHotelName={selectedHotel.name}
        onBack={() =&amp;gt; setSelectedHotel(null)}
      /&amp;gt;
    );
  }

  return (
    &amp;lt;div className={`app-shell font-sans ${theme === &quot;dark&quot; ? &quot;dark&quot; : &quot;&quot;}`}&amp;gt;
      &amp;lt;div className=&quot;mx-auto w-full max-w-5xl px-4 py-6 sm:px-6 sm:py-8&quot;&amp;gt;
        &amp;lt;section className=&quot;glass-panel overflow-hidden p-5 sm:p-7&quot;&amp;gt;
          &amp;lt;div className=&quot;flex flex-col gap-5 sm:flex-row sm:items-end sm:justify-between&quot;&amp;gt;
            &amp;lt;div className=&quot;space-y-2&quot;&amp;gt;
              &amp;lt;p className=&quot;pill bg-sky-100 text-sky-700 dark:bg-sky-950/60 dark:text-sky-300&quot;&amp;gt;
                &amp;lt;Sparkles className=&quot;h-3.5 w-3.5&quot; /&amp;gt; Curated Results
              &amp;lt;/p&amp;gt;
              &amp;lt;h2 className=&quot;font-mozilla text-3xl leading-tight text-deep dark:text-sky-100 sm:text-4xl&quot;&amp;gt;
                Stays near your destination
              &amp;lt;/h2&amp;gt;
              &amp;lt;p className=&quot;max-w-2xl text-sm text-zinc-600 dark:text-zinc-300&quot;&amp;gt;
                We found {hotels.length} options with instant booking support and synchronized steps.
              &amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div className=&quot;step-chip w-fit&quot;&amp;gt;
              &amp;lt;Compass className=&quot;mr-1.5 h-3.5 w-3.5&quot; /&amp;gt;
              Swipe or browse cards
            &amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/section&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;section className=&quot;mx-auto mt-5 grid w-full max-w-5xl grid-cols-1 gap-4 px-4 pb-8 sm:grid-cols-2 sm:px-6 lg:grid-cols-3&quot;&amp;gt;
        {hotels.map((hotel) =&amp;gt; (
          &amp;lt;article key={hotel.id} className=&quot;hotel-card&quot;&amp;gt;
            &amp;lt;div className=&quot;flex items-start justify-between gap-3&quot;&amp;gt;
              &amp;lt;div className=&quot;space-y-1&quot;&amp;gt;
                &amp;lt;h3 className=&quot;text-lg font-bold leading-tight&quot;&amp;gt;{hotel.name}&amp;lt;/h3&amp;gt;
                &amp;lt;div className=&quot;inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-[0.08em] text-zinc-500&quot;&amp;gt;
                  &amp;lt;MapPin className=&quot;h-3.5 w-3.5&quot; /&amp;gt;
                  {hotel.location}
                &amp;lt;/div&amp;gt;
              &amp;lt;/div&amp;gt;
              &amp;lt;div className=&quot;pill border border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-300&quot;&amp;gt;
                &amp;lt;Star className=&quot;mr-1 h-3.5 w-3.5 fill-current&quot; /&amp;gt;
                {hotel.rating}
              &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;div className=&quot;mt-auto space-y-3&quot;&amp;gt;
              &amp;lt;div className=&quot;flex items-end justify-between&quot;&amp;gt;
                &amp;lt;div&amp;gt;
                  &amp;lt;p className=&quot;text-[11px] uppercase tracking-[0.08em] text-zinc-500&quot;&amp;gt;From&amp;lt;/p&amp;gt;
                  &amp;lt;p className=&quot;text-3xl font-black text-deep dark:text-sky-100&quot;&amp;gt;${hotel.price}&amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;p className=&quot;text-xs font-medium text-zinc-500&quot;&amp;gt;per night&amp;lt;/p&amp;gt;
              &amp;lt;/div&amp;gt;

              &amp;lt;button
                type=&quot;button&quot;
                className=&quot;primary-action w-full&quot;
                onClick={() =&amp;gt; setSelectedHotel({ id: hotel.id, name: hotel.name })}
              &amp;gt;
                &amp;lt;Hotel className=&quot;h-4 w-4&quot; /&amp;gt;
                Start Booking
                &amp;lt;ArrowRight className=&quot;h-4 w-4&quot; /&amp;gt;
              &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/article&amp;gt;
        ))}
      &amp;lt;/section&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;This view reads &lt;code&gt;output.hotels&lt;/code&gt; with &lt;code&gt;useToolInfo&lt;/code&gt;, renders each hotel card, and transitions to the booking experience when the user clicks Start Booking.&lt;/p&gt;
&lt;p&gt;At this stage, the app can search and display hotels. Next, we will add tools to collect booking details and finalize reservations.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-add-more-tools-to-a-skybridge-app&quot;&gt;How do you add more tools to a Skybridge app?&lt;/h2&gt;
&lt;p&gt;To make the app interactive, we will add a Book Now and confirm booking tools.&lt;/p&gt;
&lt;h3 id=&quot;how-do-you-register-a-book-hotel-tool&quot;&gt;How do you register a book-hotel tool?&lt;/h3&gt;
&lt;p&gt;Update the &lt;code&gt;server.ts&lt;/code&gt; file to register the &lt;code&gt;book-hotel&lt;/code&gt; tool like so:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;server.registerTool({
  name: &quot;book-hotel&quot;,
  description: &quot;Initiates the booking process for a selected hotel.&quot;,
  inputSchema: {
    hotelId: z.string().describe(&quot;The ID of the hotel to book.&quot;),
    hotelName: z.string().describe(&quot;The name of the hotel.&quot;),
  },
  view: {
    component: &quot;booking&quot;,
    description: &quot;Multi-step booking form.&quot;
  }
}, async ({ hotelId, hotelName }) =&amp;gt; {
  return {
    structuredContent: { hotelId, hotelName },
    content: [{ type: &quot;text&quot;, text: `Starting booking for ${hotelName}...` }],
    isError: false,
  };
});&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;book-hotel&lt;/code&gt; tool defines the required schema and links to the &lt;code&gt;booking&lt;/code&gt; view component.&lt;/p&gt;
&lt;p&gt;It runs when the user starts the booking flow.&lt;/p&gt;
&lt;h3 id=&quot;how-do-you-register-a-confirm-booking-tool&quot;&gt;How do you register a confirm-booking tool?&lt;/h3&gt;
&lt;p&gt;To finalize a booking after form completion, we will add a confirm-booking tool for this.&lt;/p&gt;
&lt;p&gt;Go ahead and add this to the server.ts file:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;server.registerTool({
  name: &quot;confirm-booking&quot;,
  description: &quot;Finalize the hotel booking&quot;,
  inputSchema: {
    hotelId: z.string(),
    guestName: z.string(),
  },
}, async (details) =&amp;gt; {
  const confirmationNumber = `BK-${Math.random().toString(36).substring(2, 9).toUpperCase()}`;
  return {
    structuredContent: { ...details, confirmationNumber },
    content: [{ type: &quot;text&quot;, text: `Booking confirmed! #:${confirmationNumber}` }],
    isError: false,
  };
});&lt;/pre&gt;
&lt;p&gt;By omitting the &lt;code&gt;view&lt;/code&gt; property, you create an execution tool. The AI can call it in the background after it gathers required arguments from view state or chat context.&lt;/p&gt;
&lt;h3 id=&quot;how-do-you-create-the-booking-view-in-skybridge&quot;&gt;How do you create the booking view in Skybridge?&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;booking.tsx&lt;/code&gt; file and add the following code to it:&lt;/p&gt;
&lt;pre class=&quot;language-tsx hljs&quot;&gt;import { useState } from &quot;react&quot;;
import { useLayout } from &quot;skybridge/web&quot;;
import { useToolInfo, useCallTool } from &quot;../helpers.js&quot;;
import { Calendar, Users, BedDouble, Ticket, ArrowRight, Waves } from &quot;lucide-react&quot;;

export default function Booking() {
  const { theme } = useLayout();
  const { input, output } = useToolInfo&amp;lt;&quot;book-hotel&quot;&amp;gt;();
  const hotelName = output?.hotelName || input?.hotelName;

  const [state, setState] = useState({
    step: 0,
    checkIn: &quot;&quot;,
    checkOut: &quot;&quot;,
    llmStatus: &quot;Dates pending&quot;,
  });

  const { callTool, isPending } = useCallTool&amp;lt;&quot;confirm-booking&quot;&amp;gt;(&quot;confirm-booking&quot;);

  if (!hotelName) return null;

  const nextStep = () =&amp;gt; setState(s =&amp;gt; ({ ...s, step: s.step + 1 }));

  const progressSteps = [&quot;Dates&quot;, &quot;Guests&quot;, &quot;Room&quot;, &quot;Review&quot;];

  return (
    &amp;lt;div className={`app-shell ${theme === &quot;dark&quot; ? &quot;dark&quot; : &quot;&quot;}`}&amp;gt;
      &amp;lt;div className=&quot;mx-auto w-full max-w-2xl px-4 py-6 sm:px-6 sm:py-8&quot;&amp;gt;
        &amp;lt;div className=&quot;glass-panel space-y-6 p-5 sm:p-7&quot;&amp;gt;
          &amp;lt;header className=&quot;space-y-3&quot;&amp;gt;
            &amp;lt;p className=&quot;pill bg-teal-100 text-teal-700 dark:bg-teal-900/50 dark:text-teal-200&quot;&amp;gt;
              &amp;lt;Waves className=&quot;h-3.5 w-3.5&quot; /&amp;gt; Booking Flow
            &amp;lt;/p&amp;gt;
            &amp;lt;h2 className=&quot;font-mozilla text-3xl leading-tight text-deep dark:text-sky-100&quot;&amp;gt;
              Complete your stay at {hotelName}
            &amp;lt;/h2&amp;gt;
            &amp;lt;div className=&quot;flex flex-wrap gap-2&quot;&amp;gt;
              {progressSteps.map((label, idx) =&amp;gt; (
                &amp;lt;span key={label} className={`step-chip ${idx === state.step ? &quot;step-chip-active&quot; : &quot;&quot;}`}&amp;gt;
                  {idx + 1}. {label}
                &amp;lt;/span&amp;gt;
              ))}
            &amp;lt;/div&amp;gt;
          &amp;lt;/header&amp;gt;

          {state.step === 0 &amp;amp;&amp;amp; (
            &amp;lt;section className=&quot;space-y-4 rounded-2xl border border-sky-100 bg-white/70 p-4 dark:border-slate-700 dark:bg-slate-900/55&quot;&amp;gt;
              &amp;lt;div className=&quot;flex items-center gap-2 text-deep dark:text-sky-200&quot;&amp;gt;
                &amp;lt;Calendar className=&quot;w-5 h-5&quot; /&amp;gt;
                &amp;lt;span className=&quot;font-semibold&quot;&amp;gt;Select Dates&amp;lt;/span&amp;gt;
              &amp;lt;/div&amp;gt;
              {/* form fields */}
            &amp;lt;/section&amp;gt;
          )}

          {state.step === 1 &amp;amp;&amp;amp; (
            &amp;lt;section className=&quot;space-y-4 rounded-2xl border border-sky-100 bg-white/70 p-4 dark:border-slate-700 dark:bg-slate-900/55&quot;&amp;gt;
              &amp;lt;div className=&quot;flex items-center gap-2 text-deep dark:text-sky-200&quot;&amp;gt;
                &amp;lt;Users className=&quot;w-5 h-5&quot; /&amp;gt;
                &amp;lt;span className=&quot;font-semibold&quot;&amp;gt;Guest Details&amp;lt;/span&amp;gt;
              &amp;lt;/div&amp;gt;
            &amp;lt;/section&amp;gt;
          )}

          {state.step === 2 &amp;amp;&amp;amp; (
            &amp;lt;section className=&quot;space-y-4 rounded-2xl border border-sky-100 bg-white/70 p-4 dark:border-slate-700 dark:bg-slate-900/55&quot;&amp;gt;
              &amp;lt;div className=&quot;flex items-center gap-2 text-deep dark:text-sky-200&quot;&amp;gt;
                &amp;lt;BedDouble className=&quot;w-5 h-5&quot; /&amp;gt;
                &amp;lt;span className=&quot;font-semibold&quot;&amp;gt;Select Room Type&amp;lt;/span&amp;gt;
              &amp;lt;/div&amp;gt;
            &amp;lt;/section&amp;gt;
          )}

          {state.step === 3 &amp;amp;&amp;amp; (
            &amp;lt;section className=&quot;space-y-4 rounded-2xl border border-sky-100 bg-white/70 p-4 dark:border-slate-700 dark:bg-slate-900/55&quot;&amp;gt;
              &amp;lt;div className=&quot;flex items-center gap-2 text-deep dark:text-sky-200&quot;&amp;gt;
                &amp;lt;Ticket className=&quot;w-5 h-5&quot; /&amp;gt;
                &amp;lt;span className=&quot;font-semibold&quot;&amp;gt;Review and Confirm&amp;lt;/span&amp;gt;
              &amp;lt;/div&amp;gt;
              &amp;lt;button
                className=&quot;primary-action&quot;
                onClick={() =&amp;gt; callTool({
                  hotelId: output?.hotelId || input?.hotelId || &quot;&quot;,
                  hotelName,
                  checkIn: state.checkIn,
                  checkOut: state.checkOut,
                  adults: 2,
                  children: 0,
                  guestNames: [&quot;Primary Guest&quot;],
                  roomType: &quot;Standard&quot;,
                })}
                disabled={isPending}
              &amp;gt;
                Confirm Booking &amp;lt;ArrowRight className=&quot;h-4 w-4&quot; /&amp;gt;
              &amp;lt;/button&amp;gt;
            &amp;lt;/section&amp;gt;
          )}
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;div
        className=&quot;hidden&quot;
        data-llm={`Booking status: ${state.llmStatus}. User is at step ${state.step + 1} for ${hotelName}. Current data: ${JSON.stringify(state)}`}
      /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;The hidden &lt;code&gt;data-llm&lt;/code&gt; block keeps the model aware of current form progress and entered values, which helps it decide when to call &lt;code&gt;confirm-booking&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;With both views and tools wired together, let’s go ahead and test it.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-test-a-skybridge-app&quot;&gt;How do you test a Skybridge app?&lt;/h2&gt;
&lt;p&gt;I will first go ahead and test it out in the developer dashboard:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;alignnone size-full wp-image-214629&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/UykZUbF1.gif&quot; alt=&quot;&quot; width=&quot;800&quot; height=&quot;450&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;This is the result of my test in Claude Desktop:&lt;/p&gt;
&lt;p&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; class=&quot;alignnone size-full wp-image-214630&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/NE2HxxKR.gif&quot; alt=&quot;&quot; width=&quot;800&quot; height=&quot;450&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;To test it in Claude, do the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start the development server&lt;/li&gt;
&lt;li&gt;Open the development dashboard&lt;/li&gt;
&lt;li&gt;Start the tunnel&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Thereafter, copy the MCP URL and add it to the connectors in Claude Desktop&lt;/p&gt;
&lt;p&gt;The final implementation is available in the &lt;a href=&quot;https://github.com/emmanuelhashy/hotel_booking_skybridge&quot;&gt;hotel booking GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-are-skybridges-limitations&quot;&gt;What are Skybridge’s limitations?&lt;/h2&gt;
&lt;p&gt;Skybridge simplifies MCP development, but a few constraints still matter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;React-Focused&lt;/strong&gt;: Currently, the frontend view layer requires React&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://blog.logrocket.com/node-js-project-architecture-best-practices/&quot;&gt;Node.js&lt;/a&gt; Environment&lt;/strong&gt;: The local server component requires a Node.js runtime&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provider Support&lt;/strong&gt;: While compatible with the MCP standard, some advanced host-specific UI capabilities may still be under development&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Developing for the agentic web should not require duplicate integrations or deep protocol expertise. Skybridge gives you one path for building interactive, state-synchronized apps that work across major chatbot platforms. By centering development on shared tools and views, you can ship richer AI experiences with lower maintenance costs.&lt;/p&gt;


&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/mcp/&quot;&gt;#mcp&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/</link><guid isPermaLink="false">https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/</guid><pubDate>Invalid Date</pubDate><author>Emmanuel John</author></item><item><title>Getting started with Meilisearch: A complete guide</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#whats-changed-in-meilisearch&quot;&gt;What&#39;s changed in Meilisearch?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;whats-changed-in-meilisearch&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-you-run-meilisearch&quot;&gt;How do you run Meilisearch?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-run-meilisearch&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-you-set-up-a-meilisearch-project&quot;&gt;How do you set up a Meilisearch project?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-set-up-a-meilisearch-project&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-you-create-an-index-and-add-documents-in-meilisearch&quot;&gt;How do you create an index and add documents in Meilisearch?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-create-an-index-and-add-documents-in-meilisearch&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-you-search-documents-in-meilisearch&quot;&gt;How do you search documents in Meilisearch?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-search-documents-in-meilisearch&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-you-update-documents-in-meilisearch&quot;&gt;How do you update documents in Meilisearch?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-update-documents-in-meilisearch&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-meilisearch-ranking-rules-work&quot;&gt;How do Meilisearch ranking rules work?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-meilisearch-ranking-rules-work&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-does-hybrid-and-semantic-search-work-in-meilisearch&quot;&gt;How does hybrid and semantic search work in Meilisearch?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-does-hybrid-and-semantic-search-work-in-meilisearch&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-you-build-conversational-search-and-rag-with-meilisearch&quot;&gt;How do you build conversational search and RAG with Meilisearch?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-build-conversational-search-and-rag-with-meilisearch&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-you-use-meilisearch-for-merchandising-and-personalization&quot;&gt;How do you use Meilisearch for merchandising and personalization?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-use-meilisearch-for-merchandising-and-personalization&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#where-does-meilisearch-fit-in-an-ai-stack&quot;&gt;Where does Meilisearch fit in an AI stack?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;where-does-meilisearch-fit-in-an-ai-stack&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;&lt;em&gt;&lt;b&gt;Editor’s note:&lt;/b&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; This article was updated in July 2026 to reflect the latest Meilisearch features and APIs, including hybrid and semantic search, conversational search and RAG, AI-powered retrieval, revised setup and authentication workflows, updated JavaScript SDK usage, and current best practices.&lt;/span&gt;&lt;/em&gt;&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;895&quot; height=&quot;597&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2021/01/Getting-started-with-Meilisearch-A-complete-guide.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2021/01/Getting-started-with-Meilisearch-A-complete-guide.png 895w, https://blog.logrocket.com/wp-content/uploads/2021/01/Getting-started-with-Meilisearch-A-complete-guide-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2021/01/Getting-started-with-Meilisearch-A-complete-guide-768x512.png 768w&quot; sizes=&quot;(max-width: 895px) 100vw, 895px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;Meilisearch is a superfast search engine that supports keyword, semantic, and hybrid search through a single API. In this tutorial, you’ll learn how to set up Meilisearch, index and search documents, customize ranking rules, and explore its AI-powered retrieval capabilities.&lt;/p&gt;
&lt;p&gt;Meilisearch began as a fast, typo-tolerant, open-source search engine you ran from a single binary. It still is. But it has grown into a full retrieval engine: keyword, semantic, and hybrid search from one API, on the same Rust core.&lt;/p&gt;
&lt;p&gt;That evolution matters now that search has become a core part of AI infrastructure, powering &lt;a href=&quot;https://blog.logrocket.com/local-rag-with-small-language-models/&quot;&gt;retrieval-augmented generation&lt;/a&gt; (RAG), copilots, and other AI applications. Rather than combining a traditional search engine, a vector database, and a separate RAG framework, Meilisearch brings these capabilities together through a single API. The feel hasn’t changed, though: one Cloud project (or a single binary), one API, and results in milliseconds.&lt;/p&gt;
&lt;p&gt;In this tutorial, we’ll cover everything from setting up Meilisearch and indexing documents to customizing ranking rules and exploring newer capabilities like hybrid search, conversational search, and AI-powered retrieval.&lt;/p&gt;
&lt;h2 id=&quot;whats-changed-in-meilisearch&quot;&gt;What’s changed in Meilisearch?&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Past&lt;/th&gt;
&lt;th&gt;Present&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primarily a fast, typo-tolerant full-text search engine&lt;/td&gt;
&lt;td&gt;Supports keyword, semantic, and hybrid search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Focused on traditional keyword search&lt;/td&gt;
&lt;td&gt;Built-in embeddings and semantic retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search API only&lt;/td&gt;
&lt;td&gt;Conversational search and RAG through the &lt;code&gt;/chats&lt;/code&gt; endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic search relevance tuning&lt;/td&gt;
&lt;td&gt;Expanded ranking rules and customizable relevance pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Separate tooling often needed for recommendations and AI retrieval&lt;/td&gt;
&lt;td&gt;Built-in recommendations (&lt;code&gt;/similar&lt;/code&gt;) and AI retrieval capabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single binary or self-hosted deployment&lt;/td&gt;
&lt;td&gt;Self-hosted or Meilisearch Cloud with the same API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;how-do-you-run-meilisearch&quot;&gt;How do you run Meilisearch?&lt;/h2&gt;
&lt;p&gt;The quickest way to get started is with Meilisearch Cloud, the managed version of Meilisearch. It provides a &lt;a href=&quot;https://cloud.meilisearch.com/login&quot;&gt;14-day free trial&lt;/a&gt;, a web dashboard, and a search preview, so you can start building without managing any infrastructure. Create a project at &lt;a href=&quot;https://www.meilisearch.com/cloud&quot;&gt;cloud.meilisearch.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once your project is ready, copy the project host URL and API key from the overview page. Authentication now uses a standard &lt;code&gt;Authorization: Bearer&lt;/code&gt; header (the old &lt;code&gt;X-Meili-API-Key&lt;/code&gt; header is no longer used). You can verify that your project is reachable by listing its indexes:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;curl &#39;MEILISEARCH_URL/indexes&#39; \
  -H &#39;Authorization: Bearer MEILISEARCH_API_KEY&#39;&lt;/pre&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;# =&amp;gt; { &quot;results&quot;: [], &quot;total&quot;: 0, &quot;offset&quot;: 0, &quot;limit&quot;: 20 }&lt;/pre&gt;
&lt;p&gt;If the request succeeds, you’re ready to start indexing documents.&lt;/p&gt;
&lt;p&gt;Prefer to run Meilisearch yourself? Since it’s fully open source, you can self-host it with a single command, and the API remains identical from this point onward.&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;# Install and launch (a master key is mandatory in production)
curl -L https://install.meilisearch.com | sh
./meilisearch --master-key=&quot;aSampleMasterKey&quot;&lt;/pre&gt;
&lt;p&gt;By default, a self-hosted instance runs on &lt;code&gt;localhost:7700&lt;/code&gt;. You can also install Meilisearch using Docker, Homebrew, or APT; see the installation guide for additional options. Your master key serves the same purpose as the Cloud API key used earlier.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-set-up-a-meilisearch-project&quot;&gt;How do you set up a Meilisearch project?&lt;/h2&gt;
&lt;p&gt;Once your Meilisearch instance is running, install the JavaScript client and initialize it with your project URL and API key:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npm install meilisearch&lt;/pre&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;import { Meilisearch } from &#39;meilisearch&#39;

const client = new Meilisearch({
  host: &#39;MEILISEARCH_URL&#39;,
  apiKey: &#39;MEILISEARCH_API_KEY&#39;
})&lt;/pre&gt;
&lt;p&gt;If you’re updating code from an older Meilisearch project, note that the JavaScript client has changed slightly. The client is now a named export, and the class is &lt;code&gt;Meilisearch&lt;/code&gt; (&lt;code&gt;MeiliSearch&lt;/code&gt; still works as an alias). However, the older CommonJS syntax:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const MeiliSearch = require(&#39;meilisearch&#39;)&lt;/pre&gt;
&lt;p&gt;is no longer supported.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-create-an-index-and-add-documents-in-meilisearch&quot;&gt;How do you create an index and add documents in Meilisearch?&lt;/h2&gt;
&lt;p&gt;In Meilisearch, an index is a collection of documents. Each index has a primary key, which uniquely identifies every document. If your data already contains an &lt;code&gt;id&lt;/code&gt; field, Meilisearch detects and uses it automatically.&lt;/p&gt;
&lt;p&gt;We’ll use a sample &lt;a href=&quot;https://www.meilisearch.com/docs/assets/datasets/movies.json&quot;&gt;movies dataset&lt;/a&gt;, where each document is represented as plain JSON:&lt;/p&gt;
&lt;pre class=&quot;language-json hljs&quot;&gt;{
  &quot;id&quot;: 287947,
  &quot;title&quot;: &quot;Shazam!&quot;,
  &quot;overview&quot;: &quot;A boy is given the ability to become an adult superhero...&quot;,
  &quot;genres&quot;: [&quot;Action&quot;, &quot;Comedy&quot;, &quot;Fantasy&quot;],
  &quot;release_date&quot;: &quot;2019-03-23&quot;
}&lt;/pre&gt;
&lt;p&gt;You can add the documents to an index like this:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;import movies from &#39;./movies.json&#39;

const task = await client.index(&#39;movies&#39;).addDocuments(movies)
console.log(task) // { taskUid: 0, status: &#39;enqueued&#39;, ... }&lt;/pre&gt;
&lt;p&gt;Indexing is asynchronous, so instead of returning a completed result, Meilisearch returns a task (called an &lt;code&gt;updateId&lt;/code&gt; in older versions). You can monitor its progress using the returned &lt;code&gt;taskUid&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;await client.tasks.getTask(0)

// status moves enqueued -&amp;gt; processing -&amp;gt; succeeded&lt;/pre&gt;
&lt;p&gt;Calling &lt;code&gt;client.index(&#39;movies&#39;)&lt;/code&gt; automatically creates the index the first time you write to it, so you rarely need to call &lt;code&gt;createIndex()&lt;/code&gt; yourself. If you’re updating older code, note that &lt;code&gt;listIndexes()&lt;/code&gt; has been renamed to &lt;code&gt;getIndexes()&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-search-documents-in-meilisearch&quot;&gt;How do you search documents in Meilisearch?&lt;/h2&gt;
&lt;p&gt;Once your documents are indexed, you can search them using the &lt;code&gt;search()&lt;/code&gt; method:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const results = await client.index(&#39;movies&#39;).search(&#39;botman&#39;)

/*
{
  hits: [
    {
      id: 29751,
      title: &#39;Batman Unmasked: ...&#39;
    },
    ...
  ],
  query: &#39;botman&#39;,
  processingTimeMs: 12,
  limit: 20,
  offset: 0,
  estimatedTotalHits: 66
}
*/&lt;/pre&gt;
&lt;p&gt;Even though the query contains a typo, &lt;code&gt;botman&lt;/code&gt; still returns results for Batman because typo tolerance is enabled by default. In this example, the search also completes in just 12 ms without any additional tuning.&lt;/p&gt;
&lt;p&gt;The response includes several useful fields you’ll use frequently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;hits&lt;/code&gt;&lt;/strong&gt;: The matching documents&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;estimatedTotalHits&lt;/code&gt;&lt;/strong&gt;: An approximate count of matching documents, replacing the older &lt;code&gt;nbHits&lt;/code&gt; field&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;processingTimeMs&lt;/code&gt;&lt;/strong&gt;: The time taken to process the search request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;limit&lt;/code&gt; and &lt;code&gt;offset&lt;/code&gt;&lt;/strong&gt;: Values used for pagination&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;how-do-you-update-documents-in-meilisearch&quot;&gt;How do you update documents in Meilisearch?&lt;/h2&gt;
&lt;p&gt;Updating documents works much like adding them. If you send a document with an existing primary key, Meilisearch updates only the fields you provide, leaving the remaining fields unchanged.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;await client.index(&#39;movies&#39;).updateDocuments([
  {
    id: 287947,
    overview: &#39;Corrected synopsis text.&#39;
  }
])&lt;/pre&gt;
&lt;p&gt;Like indexing, updating documents is an asynchronous operation, so the request returns a task that you can track using its &lt;code&gt;taskUid&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;how-do-meilisearch-ranking-rules-work&quot;&gt;How do Meilisearch ranking rules work?&lt;/h2&gt;
&lt;p&gt;Ranking rules determine the order in which Meilisearch returns search results. You can reorder the built-in rules, remove them, or define custom ranking rules for each index to better match your application’s search experience.&lt;/p&gt;
&lt;p&gt;The current default ranking rules are:&lt;/p&gt;
&lt;pre class=&quot;language-json hljs&quot;&gt;[
  &quot;words&quot;,
  &quot;typo&quot;,
  &quot;proximity&quot;,
  &quot;attributeRank&quot;,
  &quot;sort&quot;,
  &quot;wordPosition&quot;,
  &quot;exactness&quot;
]&lt;/pre&gt;
&lt;p&gt;If you’re updating an older project, note that the defaults have changed since 2021. The &lt;code&gt;sort&lt;/code&gt; rule was added, &lt;code&gt;attribute&lt;/code&gt; was split into &lt;code&gt;attributeRank&lt;/code&gt; and &lt;code&gt;wordPosition&lt;/code&gt;, and the overall order was updated.&lt;/p&gt;
&lt;p&gt;You can reorder the rules like any other Meilisearch setting:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;await client.index(&#39;movies&#39;).updateRankingRules([
  &#39;words&#39;,
  &#39;typo&#39;,
  &#39;proximity&#39;,
  &#39;attributeRank&#39;,
  &#39;sort&#39;,
  &#39;wordPosition&#39;,
  &#39;exactness&#39;
])&lt;/pre&gt;
&lt;p&gt;For example, moving &lt;code&gt;exactness&lt;/code&gt; higher in the list prioritizes precise matches, while moving it lower favors broader recall. Because the rules are evaluated in order, even small changes to their sequence can noticeably affect the relevance of your search results. See the &lt;a href=&quot;https://www.meilisearch.com/docs/capabilities/full_text_search/relevancy/ranking_rules&quot;&gt;relevancy documentation&lt;/a&gt; for more details on each ranking rule.&lt;/p&gt;
&lt;h2 id=&quot;how-does-hybrid-and-semantic-search-work-in-meilisearch&quot;&gt;How does hybrid and semantic search work in Meilisearch?&lt;/h2&gt;
&lt;p&gt;This is one of the biggest additions since 2021. In addition to traditional keyword search, Meilisearch now supports semantic search using embeddings and can combine both approaches in a single query.&lt;/p&gt;
&lt;p&gt;To enable semantic search, configure an embedder for your index. Meilisearch generates and caches embeddings for your documents automatically, with support for providers including &lt;a href=&quot;https://blog.logrocket.com/openai-vs-open-source-llm/&quot;&gt;OpenAI&lt;/a&gt;, Cohere, Mistral, &lt;a href=&quot;https://blog.logrocket.com/gemini-2-5-future-of-ai-reasoning/&quot;&gt;Gemini&lt;/a&gt;, Voyage, Bedrock, Hugging Face, Ollama, or any compatible &lt;a href=&quot;https://blog.logrocket.com/stop-writing-rest-apis-from-scratch/&quot;&gt;REST API&lt;/a&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;curl -X PATCH &#39;MEILISEARCH_URL/indexes/movies/settings/embedders&#39; \
  -H &#39;Authorization: Bearer MEILISEARCH_API_KEY&#39; \
  -H &#39;Content-Type: application/json&#39; \
  --data-binary &#39;{
    &quot;movies-openai&quot;: {
      &quot;source&quot;: &quot;openAi&quot;,
      &quot;model&quot;: &quot;text-embedding-3-small&quot;,
      &quot;apiKey&quot;: &quot;OPEN_AI_API_KEY&quot;,
      &quot;documentTemplate&quot;: &quot;A movie titled {{doc.title}}: {{doc.overview}}&quot;
    }
  }&#39;&lt;/pre&gt;
&lt;p&gt;Once the embedder is configured, you can perform a hybrid search. The hybrid option’s &lt;code&gt;semanticRatio&lt;/code&gt; parameter controls the balance between keyword matching (&lt;code&gt;0&lt;/code&gt;) and semantic search (&lt;code&gt;1&lt;/code&gt;):&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;await client.index(&#39;movies&#39;).search(&#39;space adventure with a robot&#39;, {
  hybrid: {
    embedder: &#39;movies-openai&#39;,
    semanticRatio: 0.5
  }
})&lt;/pre&gt;
&lt;p&gt;With a hybrid search, Meilisearch can return relevant results even when the query terms don’t appear exactly in your documents, while still preserving the precision of keyword matching.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-build-conversational-search-and-rag-with-meilisearch&quot;&gt;How do you build conversational search and RAG with Meilisearch?&lt;/h2&gt;
&lt;p&gt;Meilisearch also includes built-in support for retrieval-augmented generation (RAG) through a single &lt;code&gt;/chats&lt;/code&gt; endpoint. When a user asks a question in natural language, Meilisearch retrieves relevant documents from your indexes and passes them to the LLM you configure, which generates a response grounded in those documents with source citations. That means you don’t need to wire together a separate vector database or orchestration layer yourself.&lt;/p&gt;
&lt;p&gt;The endpoint is OpenAI-compatible, so you can use the OpenAI SDK, the Vercel AI SDK, or any similar client by pointing it directly at your Meilisearch instance (streaming is required):&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;import OpenAI from &#39;openai&#39;

const openai = new OpenAI({
  baseURL: &#39;MEILISEARCH_URL/chats/cloud&#39;,
  apiKey: &#39;MEILISEARCH_API_KEY&#39;
})

const stream = await openai.chat.completions.create({
  model: &#39;PROVIDER_MODEL_UID&#39;,
  stream: true,
  messages: [
    {
      role: &#39;user&#39;,
      content: &#39;What movies are about artificial intelligence?&#39;
    }
  ]
})

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content || &#39;&#39;)
}&lt;/pre&gt;
&lt;p&gt;Beyond question answering, the same endpoint also supports multi-turn conversations, one-shot summaries, and retrieval-only workflows that you can integrate into your own applications. Since LLMs can still hallucinate, Meilisearch includes guardrails and system prompt controls to help keep responses grounded in your indexed data. See the &lt;a href=&quot;https://www.meilisearch.com/docs/capabilities/conversational_search/overview&quot;&gt;conversational search documentation&lt;/a&gt; for more details.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-use-meilisearch-for-merchandising-and-personalization&quot;&gt;How do you use Meilisearch for merchandising and personalization?&lt;/h2&gt;
&lt;p&gt;Search relevance isn’t always enough. In some cases, you may want to promote specific results for business reasons, such as featuring a product during a sale or surfacing a help article for a common support query.&lt;/p&gt;
&lt;p&gt;Meilisearch’s &lt;a href=&quot;https://www.meilisearch.com/docs/capabilities/search_rules/overview&quot;&gt;dynamic search rules&lt;/a&gt; let you pin selected documents to fixed positions whenever a query or time-based condition matches, without changing the underlying organic ranking. It’s an experimental feature that you enable once before adding rules.&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;# Enable the feature (once per project)
curl -X PATCH &#39;MEILISEARCH_URL/experimental-features&#39; \
  -H &#39;Authorization: Bearer MEILISEARCH_API_KEY&#39; \
  -H &#39;Content-Type: application/json&#39; \
  --data-binary &#39;{ &quot;dynamicSearchRules&quot;: true }&#39;&lt;/pre&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;# Pin a document to the top when a query contains &quot;invoice&quot;
curl -X PATCH &#39;MEILISEARCH_URL/dynamic-search-rules/invoice-help&#39; \
  -H &#39;Authorization: Bearer MEILISEARCH_API_KEY&#39; \
  -H &#39;Content-Type: application/json&#39; \
  --data-binary &#39;{
    &quot;active&quot;: true,
    &quot;conditions&quot;: [
      {
        &quot;scope&quot;: &quot;query&quot;,
        &quot;contains&quot;: &quot;invoice&quot;
      }
    ],
    &quot;actions&quot;: [
      {
        &quot;selector&quot;: {
          &quot;indexUid&quot;: &quot;support&quot;,
          &quot;id&quot;: &quot;billing-workspace-overview&quot;
        },
        &quot;action&quot;: {
          &quot;type&quot;: &quot;pin&quot;,
          &quot;position&quot;: 0
        }
      }
    ]
  }&#39;&lt;/pre&gt;
&lt;p&gt;The embedders used for hybrid search can also power recommendations. For example, the &lt;code&gt;/similar&lt;/code&gt; &lt;a href=&quot;https://www.meilisearch.com/docs/capabilities/personalization/getting_started/recommendations&quot;&gt;endpoint&lt;/a&gt; returns documents that are semantically similar to a given item, making it useful for features such as “More like this” or “Customers also viewed.”&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;curl -X POST &#39;MEILISEARCH_URL/indexes/movies/similar&#39; \
  -H &#39;Authorization: Bearer MEILISEARCH_API_KEY&#39; \
  -H &#39;Content-Type: application/json&#39; \
  --data-binary &#39;{ &quot;id&quot;: 192, &quot;embedder&quot;: &quot;movies-openai&quot; }&#39;&lt;/pre&gt;
&lt;p&gt;Meilisearch also supports &lt;a href=&quot;https://www.meilisearch.com/docs/resources/demos/personalized_search&quot;&gt;personalization&lt;/a&gt; by reranking results in real time using a short preference prompt, allowing different users searching for the same term to see results ordered according to their individual preferences.&lt;/p&gt;
&lt;h2 id=&quot;where-does-meilisearch-fit-in-an-ai-stack&quot;&gt;Where does Meilisearch fit in an AI stack?&lt;/h2&gt;
&lt;p&gt;One advantage of Meilisearch is that it brings together capabilities that often require multiple systems. Traditional search, vector search, hybrid search, recommendations through &lt;code&gt;/similar&lt;/code&gt;, and conversational retrieval through &lt;code&gt;/chats&lt;/code&gt; all run on the same Rust core, over the same indexes, and behind the same API.&lt;/p&gt;

&lt;p&gt;That means the index powering your application’s search experience can also serve as the retrieval layer for AI applications, reducing the need to maintain separate search and vector infrastructures.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;What made Meilisearch compelling in 2021 still holds true: you can get started in minutes, index your data, return results in milliseconds, and fine-tune relevance when you need to.&lt;/p&gt;
&lt;p&gt;Since then, Meilisearch has expanded well beyond traditional full-text search. Today, it supports semantic and hybrid search, retrieval-augmented generation (RAG), recommendations, and merchandising, all through the same API and on the same engine. Whether you choose Meilisearch Cloud or self-host the open-source version, the development experience remains largely unchanged.&lt;/p&gt;
&lt;p&gt;This tutorial covered the core features to help you get started, but there’s much more to explore. For more advanced capabilities like &lt;a href=&quot;https://www.meilisearch.com/docs/capabilities/hybrid_search/advanced/custom_hybrid_ranking&quot;&gt;custom hybrid ranking&lt;/a&gt;, &lt;a href=&quot;https://www.meilisearch.com/docs/capabilities/geo_search/overview&quot;&gt;geosearch&lt;/a&gt;, &lt;a href=&quot;https://www.meilisearch.com/docs/capabilities/filtering_sorting_faceting/getting_started&quot;&gt;filtering and faceting&lt;/a&gt;, and &lt;a href=&quot;https://www.meilisearch.com/docs/resources/help/language&quot;&gt;multilingual search&lt;/a&gt;, refer to the &lt;a href=&quot;https://www.meilisearch.com/docs/getting_started/overview&quot;&gt;Meilisearch documentation&lt;/a&gt;.&lt;/p&gt;



&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/2026/&quot;&gt;#2026&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/getting-started-with-meilisearch/</link><guid isPermaLink="false">https://blog.logrocket.com/getting-started-with-meilisearch/</guid><pubDate>Invalid Date</pubDate><author>Michiel Mulders</author></item><item><title>pnpm vs. npm: Which package manager should you use?</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#pnpm-vs-npm-short-version&quot;&gt;pnpm vs. npm: The short version&lt;/a&gt;
        &lt;ul data-toc-name=&quot;pnpm-vs-npm-short-version&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#install-pipeline-attack-vector&quot;&gt;The install pipeline as an attack vector&lt;/a&gt;
        &lt;ul data-toc-name=&quot;install-pipeline-attack-vector&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#pnpm-security-model-differs&quot;&gt;Where pnpm&#39;s security model differs&lt;/a&gt;
        &lt;ul data-toc-name=&quot;pnpm-security-model-differs&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#delaying-newly-published-versions-minimum-release-age&quot;&gt;Delaying newly published versions with &lt;code&gt;minimumReleaseAge&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;delaying-newly-published-versions-minimum-release-age&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#requiring-approval-dependency-build-scripts&quot;&gt;Requiring approval for dependency build scripts&lt;/a&gt;
        &lt;ul data-toc-name=&quot;requiring-approval-dependency-build-scripts&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#blocking-exotic-transitive-dependencies&quot;&gt;Blocking exotic transitive dependencies&lt;/a&gt;
        &lt;ul data-toc-name=&quot;blocking-exotic-transitive-dependencies&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#enforcing-trust-policy&quot;&gt;Enforcing trust policy&lt;/a&gt;
        &lt;ul data-toc-name=&quot;enforcing-trust-policy&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#why-pnpm-uses-less-disk-space&quot;&gt;Why pnpm uses less disk space&lt;/a&gt;
        &lt;ul data-toc-name=&quot;why-pnpm-uses-less-disk-space&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#practical-impact-node-modules-bloat&quot;&gt;The practical impact on &lt;code&gt;node_modules&lt;/code&gt; bloat&lt;/a&gt;
        &lt;ul data-toc-name=&quot;practical-impact-node-modules-bloat&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#pnpm-workspace-security-contract&quot;&gt;pnpm as a workspace security contract&lt;/a&gt;
        &lt;ul data-toc-name=&quot;pnpm-workspace-security-contract&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#builtin-sbom-generation&quot;&gt;Built-in SBOM generation&lt;/a&gt;
        &lt;ul data-toc-name=&quot;builtin-sbom-generation&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#native-registry-operations-pnpm&quot;&gt;Native registry operations in pnpm&lt;/a&gt;
        &lt;ul data-toc-name=&quot;native-registry-operations-pnpm&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#how-to-migrate-npm-yarn-to-pnpm&quot;&gt;How to migrate from npm or Yarn to pnpm&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-to-migrate-npm-yarn-to-pnpm&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#when-pnpm-better-default&quot;&gt;When pnpm is the better default&lt;/a&gt;
        &lt;ul data-toc-name=&quot;when-pnpm-better-default&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;JavaScript package managers have evolved far beyond installing dependencies. Today, tools like npm, Yarn, and pnpm also manage workspaces, generate lockfiles, audit vulnerabilities, and shape how teams secure their software supply chain.&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;1536&quot; height=&quot;1024&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-6.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;pnpm vs. npm: The short version&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-6.png 1536w, https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-6-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-6-1024x683.png 1024w, https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-6-768x512.png 768w&quot; sizes=&quot;(max-width: 1536px) 100vw, 1536px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;That last point matters more than ever. The npm ecosystem has become a high-value target for supply chain attacks, from compromised maintainer accounts to malicious lifecycle scripts that run during installation. Recent incidents such as the &lt;a href=&quot;https://www.elastic.co/security-labs/how-we-caught-the-axios-supply-chain-attack&quot;&gt;Axios supply chain attack&lt;/a&gt; and the &lt;a href=&quot;https://labs.cloudsecurityalliance.org/research/csa-research-note-shai-hulud-ai-supply-chain-20260517-csa-st/&quot;&gt;Mini Shai Hulud malware campaign&lt;/a&gt; show how quickly a trusted dependency can become an attack path. OWASP now lists &lt;a href=&quot;https://owasp.org/Top10/2025/A03_2025-Software_Supply_Chain_Failures/&quot;&gt;software supply chain failures&lt;/a&gt; as A03 in its 2025 Top 10, reflecting how central dependency risk has become to application security.&lt;/p&gt;
&lt;p&gt;npm still defines many of the conventions the JavaScript ecosystem uses every day, including &lt;code&gt;package.json&lt;/code&gt;, lockfiles, lifecycle scripts, and registry publishing. But some of npm’s long-standing design choices are showing their age, especially around &lt;code&gt;node_modules&lt;/code&gt; bloat, dependency hoisting, install-time trust, and default security posture.&lt;/p&gt;
&lt;p&gt;pnpm takes a different approach. Instead of copying dependency files into every project, it stores packages once in a content-addressable global store and links them into each project. That architecture reduces disk usage, speeds up repeated installs, and avoids some of the accidental dependency access that npm’s flattened &lt;code&gt;node_modules&lt;/code&gt; layout can allow. More recently, pnpm has also become more opinionated about security by delaying newly published versions, blocking unapproved build scripts, and rejecting certain risky dependency sources by default.&lt;/p&gt;
&lt;p&gt;In this article, we’ll look at where pnpm improves on npm, how its &lt;code&gt;node_modules&lt;/code&gt; structure works, what its newer security defaults protect against, and what to consider before migrating an existing project.&lt;/p&gt;
&lt;h2 id=&quot;pnpm-vs-npm-short-version&quot;&gt;pnpm vs. npm: The short version&lt;/h2&gt;
&lt;p&gt;pnpm is not just “npm, but faster.” Its main advantage is that performance, disk efficiency, and dependency strictness come from the same architectural choice: packages are stored once and linked into projects instead of being duplicated or broadly hoisted.&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;The table below summarizes the key differences:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;npm&lt;/th&gt;
&lt;th&gt;pnpm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dependency storage&lt;/td&gt;
&lt;td&gt;Installs packages into each project’s &lt;code&gt;node_modules&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stores package files once in a global content-addressable store and links them into projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk usage&lt;/td&gt;
&lt;td&gt;Can duplicate package files across projects&lt;/td&gt;
&lt;td&gt;Reuses the same package version across projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency access&lt;/td&gt;
&lt;td&gt;Hoisted dependencies can be imported even when they are not declared directly&lt;/td&gt;
&lt;td&gt;Strict layout makes undeclared dependency imports easier to catch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install scripts&lt;/td&gt;
&lt;td&gt;Lifecycle scripts run by default unless restricted&lt;/td&gt;
&lt;td&gt;Dependency build scripts require explicit approval through &lt;code&gt;allowBuilds&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Newly published versions&lt;/td&gt;
&lt;td&gt;Can be delayed with &lt;code&gt;min-release-age&lt;/code&gt;, but this is opt-in&lt;/td&gt;
&lt;td&gt;Delays newly published versions by default in pnpm 11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workspaces&lt;/td&gt;
&lt;td&gt;Supported through the &lt;code&gt;workspaces&lt;/code&gt; field&lt;/td&gt;
&lt;td&gt;Supported through &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;, with centralized security and version policy settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SBOM support&lt;/td&gt;
&lt;td&gt;Supports &lt;code&gt;npm sbom&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Supports &lt;code&gt;pnpm sbom&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;The trade-off is compatibility. pnpm’s strictness can expose problems that npm’s hoisting hides. That is usually a good thing for long-term correctness, but it can make migration noisier for projects that rely on undeclared dependencies.&lt;/p&gt;
&lt;h2 id=&quot;install-pipeline-attack-vector&quot;&gt;The install pipeline as an attack vector&lt;/h2&gt;
&lt;p&gt;The package install step is one of the most trusted parts of a JavaScript workflow. It is also one of the riskiest.&lt;/p&gt;
&lt;p&gt;When you run &lt;code&gt;npm install&lt;/code&gt;, dependency lifecycle scripts such as &lt;code&gt;preinstall&lt;/code&gt;, &lt;code&gt;install&lt;/code&gt;, and &lt;code&gt;postinstall&lt;/code&gt; can execute automatically. Legitimate packages use these scripts to download binaries, compile native modules, or perform setup work that the package needs in order to function.&lt;/p&gt;
&lt;p&gt;The problem is that install scripts are still third-party code running on your machine or in CI. If an attacker compromises a package, maintainer account, or release process, the install step becomes a direct execution path.&lt;/p&gt;
&lt;p&gt;The Mini Shai Hulud campaign exploited that kind of trust boundary: malicious packages used installation behavior to steal sensitive credentials such as GitHub tokens, API keys, and cloud secrets. Once those credentials were captured, attackers could publish additional poisoned packages and expand the compromise.&lt;/p&gt;
&lt;p&gt;Lockfiles help reduce the risk of silently picking up a malicious patch release. For example, if a project depends on &lt;code&gt;axios&lt;/code&gt; with a semver range such as &lt;code&gt;^1.13.0&lt;/code&gt;, installing without a lockfile can resolve to a newer patch or minor version that satisfies the range. If that newer version is compromised during the window when it is available, your project may install it automatically.&lt;/p&gt;
&lt;p&gt;That is why package manager behavior matters. Security is not only about running &lt;code&gt;npm audit&lt;/code&gt; after installation; it is also about deciding which code is allowed to run during installation and how quickly newly published versions can enter your dependency tree. Understanding &lt;a href=&quot;https://blog.logrocket.com/npm-dependencies-bigger-security-risk-your-code/&quot;&gt;why npm dependencies are a bigger security risk than your code&lt;/a&gt; can help frame just how significant these decisions are.&lt;/p&gt;
&lt;h2 id=&quot;pnpm-security-model-differs&quot;&gt;Where pnpm’s security model differs&lt;/h2&gt;
&lt;p&gt;pnpm’s newer security defaults are designed around a simple idea: the package manager should not assume that every newly published package, transitive dependency, or install script is safe.&lt;/p&gt;
&lt;p&gt;These defaults do not make a project immune to supply chain attacks. You still need lockfiles, code review, two-factor authentication for package publishing, dependency monitoring, and secret scanning. But pnpm shifts more protection into the default install path instead of requiring every team to opt into it manually.&lt;/p&gt;
&lt;h3 id=&quot;delaying-newly-published-versions-minimum-release-age&quot;&gt;Delaying newly published versions with &lt;code&gt;minimumReleaseAge&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;pnpm 11 sets &lt;code&gt;minimumReleaseAge&lt;/code&gt; to &lt;code&gt;1440&lt;/code&gt; minutes by default, which means a newly published package version must be at least 24 hours old before pnpm will resolve it.&lt;/p&gt;

&lt;p&gt;That delay helps with a common supply chain pattern: malicious versions are often discovered and removed shortly after publication. Waiting a day gives maintainers, registries, and security tools time to react before the version reaches your project.&lt;/p&gt;
&lt;p&gt;You can increase the delay in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-yaml hljs&quot;&gt;minimumReleaseAge: 10080 # Wait one week before installing newly published versions
&lt;/pre&gt;
&lt;p&gt;You can also exempt trusted packages, such as internal packages your organization publishes:&lt;/p&gt;
&lt;pre class=&quot;language-yaml hljs&quot;&gt;minimumReleaseAge: 1440
minimumReleaseAgeExclude:
  - &#39;@your-org/*&#39;
&lt;/pre&gt;
&lt;p&gt;npm has added a similar setting, &lt;code&gt;min-release-age&lt;/code&gt;, but it is not enabled by default. That difference matters: with pnpm, the safer behavior is the starting point; with npm, teams must choose to configure it.&lt;/p&gt;
&lt;h3 id=&quot;requiring-approval-dependency-build-scripts&quot;&gt;Requiring approval for dependency build scripts&lt;/h3&gt;
&lt;p&gt;Install scripts are powerful because they run code during dependency installation. That power is also why they are dangerous.&lt;/p&gt;
&lt;p&gt;pnpm’s &lt;code&gt;allowBuilds&lt;/code&gt; setting lets teams approve which dependencies may run lifecycle build scripts. If a package tries to run a build script and is not allowlisted, pnpm reports it instead of silently trusting it.&lt;/p&gt;
&lt;p&gt;A typical workflow looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;pnpm install
pnpm approve-builds
&lt;/pre&gt;
&lt;p&gt;The resulting policy lives in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-yaml hljs&quot;&gt;allowBuilds:
  &#39;@swc/core&#39;: true
  esbuild: true
  sharp: true
&lt;/pre&gt;
&lt;p&gt;This is useful because many legitimate packages need build scripts. Native dependencies such as &lt;code&gt;sharp&lt;/code&gt;, &lt;code&gt;esbuild&lt;/code&gt;, and &lt;code&gt;@swc/core&lt;/code&gt; may require postinstall behavior. The goal is not to ban lifecycle scripts entirely; it is to make script execution explicit and reviewable.&lt;/p&gt;
&lt;p&gt;npm has also introduced an approval workflow through &lt;code&gt;npm approve-scripts&lt;/code&gt;, but in the current npm release this policy is advisory: install scripts still run by default, and npm reports which packages have not been reviewed.&lt;/p&gt;
&lt;h3 id=&quot;blocking-exotic-transitive-dependencies&quot;&gt;Blocking exotic transitive dependencies&lt;/h3&gt;
&lt;p&gt;Another pnpm 11 default, &lt;code&gt;blockExoticSubdeps&lt;/code&gt;, blocks transitive dependencies from resolving through sources such as Git repositories or tarball URLs. These sources can be legitimate, but they also bypass some of the normal expectations teams have around public registry packages, provenance, and review.&lt;/p&gt;
&lt;p&gt;This does not mean you can never install from a Git URL. It means pnpm is stricter about allowing a package deep in your dependency tree to pull code from a less predictable source without you noticing.&lt;/p&gt;
&lt;h3 id=&quot;enforcing-trust-policy&quot;&gt;Enforcing trust policy&lt;/h3&gt;
&lt;p&gt;pnpm 10.21 introduced &lt;code&gt;trustPolicy&lt;/code&gt;, including a &lt;code&gt;no-downgrade&lt;/code&gt; mode that can block a package version if its trust evidence is weaker than a version you previously installed.&lt;/p&gt;
&lt;p&gt;For example, if a package previously shipped with provenance and a later version does not, pnpm can refuse the downgrade in trust. That kind of policy is especially relevant after incidents where compromised package versions lack the provenance signals present in earlier legitimate releases.&lt;/p&gt;

&lt;p&gt;npm can display provenance information and supports signature verification through commands like &lt;code&gt;npm audit signatures&lt;/code&gt;, but it does not currently enforce a comparable trust downgrade policy during normal installation.&lt;/p&gt;
&lt;h2 id=&quot;why-pnpm-uses-less-disk-space&quot;&gt;Why pnpm uses less disk space&lt;/h2&gt;
&lt;p&gt;The other major difference between npm and pnpm is how they store packages on disk.&lt;/p&gt;
&lt;p&gt;A JavaScript dependency rarely arrives alone. Install &lt;code&gt;express&lt;/code&gt;, and you also install the packages &lt;code&gt;express&lt;/code&gt; depends on, the packages those packages depend on, and so on. This transitive tree can grow quickly.&lt;/p&gt;
&lt;p&gt;npm v3 improved disk usage by flattening &lt;code&gt;node_modules&lt;/code&gt; and hoisting dependencies to the project root where possible. That reduced duplication, but it also introduced a long-standing side effect: packages can sometimes be imported by your application even if you never declared them in &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example, suppose &lt;code&gt;express&lt;/code&gt; depends on &lt;code&gt;debug&lt;/code&gt;. With npm’s flattened layout, &lt;code&gt;debug&lt;/code&gt; may appear at the top level of &lt;code&gt;node_modules&lt;/code&gt;. Your application can then accidentally do this:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;import debug from &#39;debug&#39;;
&lt;/pre&gt;
&lt;p&gt;The import works even though &lt;code&gt;debug&lt;/code&gt; is not your direct dependency. Later, if &lt;code&gt;express&lt;/code&gt; removes &lt;code&gt;debug&lt;/code&gt; or changes how it depends on it, your application breaks. This is the “phantom dependency” or “ghost dependency” problem. Tools like &lt;a href=&quot;https://blog.logrocket.com/finding-unused-and-ghost-dependencies-with-knip/&quot;&gt;Knip can help you find unused and ghost dependencies&lt;/a&gt; before they cause issues in production.&lt;/p&gt;
&lt;p&gt;pnpm avoids this by using a stricter &lt;code&gt;node_modules&lt;/code&gt; layout. Package files are stored once in a global content-addressable store. Projects then reference those files through hard links and symlinks.&lt;/p&gt;
&lt;p&gt;A simplified layout looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext hljs&quot;&gt;~/.pnpm-store/
└── v11/
    ├── files/
    │   ├── 4a/1f9c8b...              ← react content, hash-addressed
    │   └── 7e/2d5a91...              ← react-dom content, hash-addressed
    └── index.db

project-a/
└── node_modules/
    ├── react -&amp;gt; .pnpm/&lt;a href=&quot;https://blog.logrocket.com/cdn-cgi/l/email-protection&quot; class=&quot;__cf_email__&quot; data-cfemail=&quot;2f5d4a4e4c5b6f1e17011c011e&quot;&gt;[email&amp;nbsp;protected]&lt;/a&gt;/node_modules/react
    ├── react-dom -&amp;gt; .pnpm/&lt;a href=&quot;https://blog.logrocket.com/cdn-cgi/l/email-protection&quot; class=&quot;__cf_email__&quot; data-cfemail=&quot;3c4e595d5f48115853517c0d04120f120d&quot;&gt;[email&amp;nbsp;protected]&lt;/a&gt;/node_modules/react-dom
    └── .pnpm/
        ├── &lt;a href=&quot;https://blog.logrocket.com/cdn-cgi/l/email-protection&quot; class=&quot;__cf_email__&quot; data-cfemail=&quot;e49681858790a4d5dccad7cad5&quot;&gt;[email&amp;nbsp;protected]&lt;/a&gt;/
        │   └── node_modules/
        │       └── react/            ← hard linked from the store
        └── &lt;a href=&quot;https://blog.logrocket.com/cdn-cgi/l/email-protection&quot; class=&quot;__cf_email__&quot; data-cfemail=&quot;daa8bfbbb9aef7beb5b79aebe2f4e9f4eb&quot;&gt;[email&amp;nbsp;protected]&lt;/a&gt;/
            └── node_modules/
                ├── react-dom/        ← hard linked from the store
                └── react -&amp;gt; ../../&lt;a href=&quot;https://blog.logrocket.com/cdn-cgi/l/email-protection&quot; class=&quot;__cf_email__&quot; data-cfemail=&quot;27554246445367161f09140916&quot;&gt;[email&amp;nbsp;protected]&lt;/a&gt;/node_modules/react
&lt;/pre&gt;
&lt;p&gt;A hard link points another file name at the same underlying data on disk, so the package contents are not duplicated. A symlink works more like a shortcut, pointing one path to another. pnpm combines both: hard links avoid repeated package contents, and symlinks preserve the dependency relationships Node needs for module resolution.&lt;/p&gt;
&lt;p&gt;This is why pnpm can feel dramatically lighter on machines with many projects. If 20 projects all use the same version of React, pnpm stores that version once and links it into each project as needed.&lt;/p&gt;
&lt;p&gt;One caveat: Finder, Windows Explorer, and some disk-usage tools can make pnpm projects look larger than they really are because they count each hard-linked file as if it were a separate copy. To measure actual disk usage, use tools that understand hard links or compare free disk space before and after installation.&lt;/p&gt;
&lt;h2 id=&quot;practical-impact-node-modules-bloat&quot;&gt;The practical impact on &lt;code&gt;node_modules&lt;/code&gt; bloat&lt;/h2&gt;
&lt;p&gt;Early in my career, I dealt with &lt;code&gt;node_modules&lt;/code&gt; bloat before I understood what caused it. I was learning React on a 256GB laptop with limited internet access, so I copied entire &lt;code&gt;node_modules&lt;/code&gt; folders between projects to avoid downloading the same packages again.&lt;/p&gt;
&lt;p&gt;pnpm solves that problem at the package manager level. Whether you have one project or a hundred on the same machine, every package version is stored once and reused.&lt;/p&gt;
&lt;p&gt;I saw this in a large pnpm monorepo with four apps. The root &lt;code&gt;node_modules&lt;/code&gt; appeared to take about &lt;strong&gt;1.87GB&lt;/strong&gt; on disk. After deleting &lt;code&gt;node_modules&lt;/code&gt; and reinstalling, the free storage on my machine barely changed because the package contents were already in pnpm’s store. The reinstall mostly recreated links.&lt;/p&gt;
&lt;p&gt;When I converted the same project to an npm monorepo and installed dependencies, disk usage increased because npm wrote a separate copy of the dependencies into that project.&lt;/p&gt;
&lt;p&gt;The important lesson is not just “pnpm is smaller.” It is that pnpm treats installed packages as reusable content, while npm generally treats each project install as its own dependency tree.&lt;/p&gt;
&lt;h2 id=&quot;pnpm-workspace-security-contract&quot;&gt;pnpm as a workspace security contract&lt;/h2&gt;
&lt;p&gt;pnpm’s workspace configuration has become a central place for dependency policy.&lt;/p&gt;
&lt;p&gt;In pnpm 11, most non-registry settings belong in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; rather than being scattered across &lt;code&gt;.npmrc&lt;/code&gt; files or a &lt;code&gt;pnpm&lt;/code&gt; field in &lt;code&gt;package.json&lt;/code&gt;. That makes settings such as &lt;code&gt;minimumReleaseAge&lt;/code&gt;, &lt;code&gt;allowBuilds&lt;/code&gt;, &lt;code&gt;blockExoticSubdeps&lt;/code&gt;, and &lt;code&gt;trustPolicy&lt;/code&gt; easier to review in one place.&lt;/p&gt;
&lt;p&gt;For a monorepo, that matters. A root workspace file can act as a security contract for every package in the repo:&lt;/p&gt;
&lt;pre class=&quot;language-yaml hljs&quot;&gt;packages:
  - &#39;apps/*&#39;
  - &#39;packages/*&#39;

minimumReleaseAge: 1440
blockExoticSubdeps: true
trustPolicy: no-downgrade
allowBuilds:
  &#39;@swc/core&#39;: true
  esbuild: true
  sharp: true
&lt;/pre&gt;
&lt;p&gt;This file tells reviewers not only which packages exist in the workspace, but also which dependency behaviors are allowed.&lt;/p&gt;
&lt;p&gt;pnpm’s catalogs push this further by centralizing dependency versions across a workspace. With strict catalog mode, &lt;code&gt;pnpm add&lt;/code&gt; can fail when a developer tries to install a version outside the approved catalog. npm’s &lt;code&gt;overrides&lt;/code&gt; can force versions after resolution, but catalogs are more proactive: they prevent unsupported versions from being added in the first place.&lt;/p&gt;
&lt;h2 id=&quot;builtin-sbom-generation&quot;&gt;Built-in SBOM generation&lt;/h2&gt;
&lt;p&gt;A software bill of materials, or SBOM, is a machine-readable inventory of the packages in your application. Teams use SBOMs for audits, compliance, vulnerability scanning, and incident response.&lt;/p&gt;
&lt;p&gt;Before package managers added native SBOM support, teams often reached for tools like Syft or CycloneDX plugins. npm introduced &lt;code&gt;npm sbom&lt;/code&gt; first, and pnpm now includes &lt;code&gt;pnpm sbom&lt;/code&gt; as well.&lt;/p&gt;
&lt;p&gt;That means you can generate CycloneDX or SPDX output directly from your package manager:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;pnpm sbom --format cyclonedx-json &amp;gt; sbom.json
&lt;/pre&gt;
&lt;p&gt;This is useful during incident response. When a new vulnerability or compromise is disclosed, an SBOM helps you answer a basic but urgent question: did we ship the affected package, and in which release?&lt;/p&gt;
&lt;h2 id=&quot;native-registry-operations-pnpm&quot;&gt;Native registry operations in pnpm&lt;/h2&gt;
&lt;p&gt;pnpm 11 also implements registry operations directly. Commands such as &lt;code&gt;pnpm publish&lt;/code&gt;, &lt;code&gt;pnpm login&lt;/code&gt;, &lt;code&gt;pnpm logout&lt;/code&gt;, &lt;code&gt;pnpm view&lt;/code&gt;, &lt;code&gt;pnpm deprecate&lt;/code&gt;, and &lt;code&gt;pnpm unpublish&lt;/code&gt; no longer depend on the npm CLI under the hood.&lt;/p&gt;
&lt;p&gt;This is not a feature that npm lacks; npm is still the native CLI for the npm registry. The point is that pnpm’s workflow no longer has to hand publishing and account operations off to another tool. For teams standardizing on pnpm, that makes the install-to-publish workflow more consistent.&lt;/p&gt;
&lt;h2 id=&quot;how-to-migrate-npm-yarn-to-pnpm&quot;&gt;How to migrate from npm or Yarn to pnpm&lt;/h2&gt;
&lt;p&gt;Migrating to pnpm is usually straightforward, but the strict dependency layout can reveal hidden problems in your project.&lt;/p&gt;
&lt;p&gt;A safe migration looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;# Option 1: Install globally
npm i -g pnpm

# Option 2: Use Corepack
corepack enable
corepack use pnpm@latest

# Convert the existing lockfile
pnpm import

# Remove the old install artifacts
rm -rf node_modules package-lock.json yarn.lock

# Install with pnpm
pnpm install
&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pnpm import&lt;/code&gt; reads your existing &lt;code&gt;package-lock.json&lt;/code&gt; or &lt;code&gt;yarn.lock&lt;/code&gt; and creates a &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; that preserves your current resolutions as closely as possible. That gives you a more controlled starting point than deleting the old lockfile and resolving everything from scratch.&lt;/p&gt;
&lt;p&gt;After the first install, expect to fix a few things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add any dependencies your code imports directly but did not list in &lt;code&gt;package.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Replace npm or Yarn commands in CI workflows and Dockerfiles&lt;/li&gt;
&lt;li&gt;Create a root &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; for monorepos&lt;/li&gt;
&lt;li&gt;Review packages that require lifecycle scripts and approve them with &lt;code&gt;pnpm approve-builds&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Decide whether to keep the default &lt;code&gt;minimumReleaseAge&lt;/code&gt; or tune it for your release workflow&lt;/li&gt;
&lt;li&gt;Update developer onboarding docs so local commands use &lt;code&gt;pnpm&lt;/code&gt; consistently&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most common migration surprise is phantom dependencies. Code that worked under npm may fail under pnpm because it imported a package that was only available through hoisting. Treat those failures as useful signals. They show you where your dependency declarations were incomplete.&lt;/p&gt;
&lt;h2 id=&quot;when-pnpm-better-default&quot;&gt;When pnpm is the better default&lt;/h2&gt;
&lt;p&gt;pnpm is a strong fit when your team cares about reproducible installs, monorepo performance, disk usage, and supply chain hardening. It is especially useful for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Large monorepos with shared packages and many repeated dependencies&lt;/li&gt;
&lt;li&gt;Teams that want stricter dependency declarations&lt;/li&gt;
&lt;li&gt;CI systems where install speed and lockfile consistency matter&lt;/li&gt;
&lt;li&gt;Security-conscious teams that want safer install defaults&lt;/li&gt;
&lt;li&gt;Projects that need centralized workspace dependency policy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;npm is still a reasonable default for small projects, tutorials, and teams that want the least surprising path through the JavaScript ecosystem. It is preinstalled with Node.js, widely documented, and universally supported. If your project is small and your workflow is simple, npm may be enough.&lt;/p&gt;
&lt;p&gt;But for larger applications, pnpm’s trade-offs are increasingly attractive. It keeps the familiar &lt;code&gt;package.json&lt;/code&gt; workflow while solving several pain points that npm either leaves to configuration or exposes through years of backward-compatible behavior. Teams that also want to &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/&quot;&gt;secure their full-stack projects from NPM attacks&lt;/a&gt; will find that pnpm’s stricter defaults complement those broader hardening efforts.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Package managers are now part of the security boundary. They do not just fetch dependencies; they decide which code runs during installation, how quickly new versions enter a project, how strictly dependencies are resolved, and how easily teams can audit what they shipped.&lt;/p&gt;
&lt;p&gt;That is where pnpm has pulled ahead. Its content-addressable store reduces disk usage and repeated downloads. Its strict dependency layout exposes phantom dependencies instead of hiding them. Its workspace configuration centralizes dependency policy. And its newer security defaults make risky installation behavior harder to miss.&lt;/p&gt;
&lt;p&gt;This does not make npm obsolete. npm created the workflow the ecosystem still builds on, and it continues to add security features of its own. The difference is that pnpm is more willing to make stricter behavior the default.&lt;/p&gt;
&lt;p&gt;For teams maintaining large JavaScript apps or monorepos, that default posture matters. pnpm is not only faster and more disk-efficient than npm in many workflows; it is a package manager that treats dependency installation as a security-sensitive operation from the start. If you are also thinking about how your dependencies interact with authentication and access control, reviewing &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/&quot;&gt;JWT authentication best practices&lt;/a&gt; is a natural next step for hardening the full application stack. And if you are adopting AI-assisted development alongside these security improvements, understanding &lt;a href=&quot;https://blog.logrocket.com/ai-development-governance-practical-guide/&quot;&gt;AI-assisted development governance&lt;/a&gt; can help your team maintain control over what gets shipped.&lt;/p&gt;


&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/javascript/&quot;&gt;#javascript&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/</link><guid isPermaLink="false">https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/</guid><pubDate>Invalid Date</pubDate><author>Chinwike Maduabuchi</author></item></channel></rss>