<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Playful Programming</title>
    <description>The latest articles on DEV Community by Playful Programming (playfulprogramming).</description>
    <link>https://dev.to/playfulprogramming</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3314%2Ffd92caab-2014-431e-a19e-8ab47f2bf5ab.png</url>
      <title>DEV Community: Playful Programming</title>
      <link>https://dev.to/playfulprogramming</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/playfulprogramming"/>
    <language>en</language>
    <item>
      <title>Astro Wellness: The First Project Built Start to Finish by My Agent Team 🏋️</title>
      <dc:creator>Domenico Tenace</dc:creator>
      <pubDate>Wed, 28 Oct 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/astro-wellness-the-first-project-built-start-to-finish-by-my-agent-team-3kkn</link>
      <guid>https://dev.to/playfulprogramming/astro-wellness-the-first-project-built-start-to-finish-by-my-agent-team-3kkn</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Hey everyone 👋&lt;/p&gt;

&lt;p&gt;A few days ago I wrote about the agent team I built for OpenCode: a web-developer primary agent and four subagents for UX/UI, code review, SEO and documentation. The obvious question was, does it actually hold up on a real project?&lt;/p&gt;

&lt;p&gt;Meet &lt;a href="https://github.com/DomeT99/astro-wellness" rel="noopener noreferrer"&gt;&lt;strong&gt;Astro Wellness&lt;/strong&gt;&lt;/a&gt;, a modern, dark and energetic gym template built with Astro and Bulma. It's the first project the whole team took from empty folder to finished, and it's now listed among the official Astro templates: &lt;a href="https://portal.astro.build/themes/astro-wellness/" rel="noopener noreferrer"&gt;astro-wellness on the Astro portal&lt;/a&gt;. I'm still a bit surprised typing that.&lt;/p&gt;

&lt;p&gt;Let's dive in! 🤙&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Astro Wellness? 🤔
&lt;/h2&gt;

&lt;p&gt;It's a template for a gym, studio or training brand, ready to adopt and customise. It ships with everything you need to go live:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pages: home (hero, stats, services, testimonials), about, services with pricing plans, blog, contact and a custom 404&lt;/li&gt;
&lt;li&gt;Dark and light mode through Bulma's themes, persisted in &lt;code&gt;localStorage&lt;/code&gt;, following the OS preference on the first visit and applied before first paint&lt;/li&gt;
&lt;li&gt;SPA style page transitions with Astro's &lt;code&gt;ClientRouter&lt;/code&gt;, respecting &lt;code&gt;prefers-reduced-motion&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A Markdown blog on content collections with a typed schema and an auto generated RSS feed&lt;/li&gt;
&lt;li&gt;SEO ready: per page title and description, canonical URLs, Open Graph, JSON-LD (&lt;code&gt;HealthClub&lt;/code&gt; on the home page, &lt;code&gt;BlogPosting&lt;/code&gt; on posts), sitemap and &lt;code&gt;robots.txt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Strict TypeScript, typed props on every component, and Bulma imported module by module so nothing unused ships
The look is a dark theme with an orange accent plus an adaptive light theme, and every design token lives in two clearly documented places, so re-theming the whole site is a quick job. It's MIT licensed, pnpm only, on Astro 7, with a live demo on Netlify.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How the Team Worked on It 👥
&lt;/h2&gt;

&lt;p&gt;Here's the part I wanted to test. If you missed the previous article, the rule in my setup is simple: nobody edits anything in the first pass. Every agent proposes first, I approve, and only then does it apply changes, with each write still behind an &lt;code&gt;ask&lt;/code&gt; gate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;web-developer&lt;/strong&gt; led the build. It explored the project, proposed the structure (pages, layout, data files, components), and implemented it after my OK, then verified with the type check and the build. The strict TypeScript setup and the clean &lt;code&gt;src/data&lt;/code&gt; folder for services, team, pricing and testimonials come from its rules about typed props and keeping pages free of complex logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ux-ui-expert&lt;/strong&gt; handled the design system: the dark and orange palette, the light theme flipping every token automatically, responsive behaviour at Bulma's breakpoints, and the details like focus states and reduced motion support. Its rule of using the framework's primitives before custom CSS is why the project styles stay small and prefixed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;seo-expert&lt;/strong&gt; audited the head, the structured data, the sitemap, the robots file and the social share image. It's also the reason the README tells you to set your real domain before deploying, since the agent flagged it as something only the template user can decide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;code-reviewer&lt;/strong&gt; went over the changes with its checklist and reported by severity, with concrete fixes I could approve one by one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;documentation-updater&lt;/strong&gt; kept the repo honest: README, CHANGELOG, CONTRIBUTING, SECURITY and AGENTS.md. Its rule of never documenting what it can't verify against the code is exactly what you want from docs that will be read by strangers.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Worked 👍
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The approval gate paid off.&lt;/strong&gt; On a template that other people will copy, I didn't want surprise edits. Proposals first meant I made the design and architecture calls, and the agents did the heavy lifting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Specialists beat one big prompt.&lt;/strong&gt; Having the SEO and docs passes happen as separate reviews, by agents with their own skill and checklist, made the final result more complete than anything I'd have remembered to ask for at the end of a long conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistency.&lt;/strong&gt; Same structure rules, same naming, same quality bar across the whole codebase, because the conventions live in the agent files and not in my head.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's reusable.&lt;/strong&gt; The same team can now take on the next template without me rebuilding the workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Was Harder 😬
&lt;/h2&gt;

&lt;p&gt;I won't pretend it was effortless. The approval flow means a lot of reading and clicking, and for small tasks it's slower than just doing it. Delegation costs tokens and time. And the agents don't replace judgment: decisions like the visual direction, the tone of the sample content and which sections a gym site really needs were mine.&lt;/p&gt;

&lt;p&gt;The other lesson is that a template has a different bar than a personal site. People will fork it, so documentation, accessibility and clean defaults matter more than usual, and that's where having dedicated agents for those jobs helped the most.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It, Fork It, Break It 🚀
&lt;/h2&gt;

&lt;p&gt;Astro Wellness is open source and free to use for your own gym, studio or training brand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll need Node 22.22 or newer and pnpm 10+. Edit &lt;code&gt;src/data/site.ts&lt;/code&gt; for your identity, swap the sample content, set your real domain in &lt;code&gt;astro.config.mjs&lt;/code&gt;, and deploy the &lt;code&gt;dist/&lt;/code&gt; folder anywhere static. The live demo is at astro-wellness.netlify.app, and issues and PRs are welcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts 🌟
&lt;/h2&gt;

&lt;p&gt;Having a template accepted among the official Astro ones was a great moment, but the bigger win for me is the proof that the agent setup works on a real deliverable, not just on demos. One primary agent, four specialists, a lot of small approvals, and a project I'm proud to put my name on.&lt;/p&gt;

&lt;p&gt;If you try the template, or build your own agent team for OpenCode, tell me how it goes.&lt;/p&gt;

&lt;p&gt;Happy coding! ✨&lt;/p&gt;




&lt;p&gt;Hi 👋🏻&lt;br&gt;
My name is Domenico, software developer passionate of Open Source, I write article about it for share my knowledge and experience.&lt;br&gt;
Don't forget to visit my social links to discover my links, check out Domenico Tenace Open Labs for my open source projects and subscribe to my newsletter! 🫰🏻&lt;/p&gt;

&lt;p&gt;🌲 Links: &lt;a href="https://links.domenicotenace.dev/" rel="noopener noreferrer"&gt;https://links.domenicotenace.dev/&lt;/a&gt;&lt;br&gt;
🐙 Domenico Tenace Open Labs: &lt;a href="https://github.com/Domenico-Tenace-Open-Labs" rel="noopener noreferrer"&gt;https://github.com/Domenico-Tenace-Open-Labs&lt;/a&gt;&lt;br&gt;
📰 Newsletter: &lt;a href="https://domenicotenace.substack.com/subscribe" rel="noopener noreferrer"&gt;https://domenicotenace.substack.com/subscribe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you like my content or want to support my work, you can support me with a small donation. I would be grateful 🥹&lt;/p&gt;

&lt;p&gt;☕ Buy me a Coffee: &lt;a href="https://buymeacoffee.com/domenicotenace" rel="noopener noreferrer"&gt;https://buymeacoffee.com/domenicotenace&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>astro</category>
      <category>javascript</category>
    </item>
    <item>
      <title>One Dev, Four Specialists: My Agent Team for OpenCode 🧑‍💻</title>
      <dc:creator>Domenico Tenace</dc:creator>
      <pubDate>Thu, 22 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/one-dev-four-specialists-my-agent-team-for-opencode-5f31</link>
      <guid>https://dev.to/playfulprogramming/one-dev-four-specialists-my-agent-team-for-opencode-5f31</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Hey everyone 👋&lt;/p&gt;

&lt;p&gt;For a long time my OpenCode workflow was one agent, one giant conversation, and me asking it to code, then review, then fix the SEO, then update the docs. It works, but the context gets messy, and the agent that just wrote the code is the worst one to judge it.&lt;/p&gt;

&lt;p&gt;So I rebuilt the setup as a small team: one primary agent that builds, and four subagents it delegates to. Each one has its own role, its own boundaries and its own dedicated skill. Every agent is a single markdown file, so the whole thing is easy to read, tweak and version.&lt;/p&gt;

&lt;p&gt;Let's dive in! 🤙&lt;/p&gt;




&lt;h2&gt;
  
  
  The Team 👥
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;web-developer&lt;/strong&gt; is the primary agent, a senior web dev for Astro, Vue, TypeScript, Vite, Bulma and Tailwind. It's the one I talk to, and it owns the implementation.&lt;/p&gt;

&lt;p&gt;Then four subagents, all launched by the web-developer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ux-ui-expert&lt;/strong&gt; designs and reviews UI, UX and graphics, always respecting the project's design system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;code-reviewer&lt;/strong&gt; reviews changes, a feature or the whole codebase, and reports issues by severity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;seo-expert&lt;/strong&gt; audits on-page, technical and content SEO.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;documentation-updater&lt;/strong&gt; keeps README, changelog and guides in sync with the code.&lt;/p&gt;

&lt;p&gt;The flow is natural: I describe a feature, the web-developer proposes a plan, builds it after my OK, then calls the right specialists before we call it done.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Golden Rule: Propose First, Apply After Approval ✋
&lt;/h2&gt;

&lt;p&gt;This is the heart of the setup, and it's the same in every agent. Nobody edits files in the first pass. They present a concrete proposal (files to change, what changes, why) and wait for my explicit approval. Then, when they do apply something, every single write is gated by an &lt;code&gt;ask&lt;/code&gt; permission, so I confirm it again.&lt;/p&gt;

&lt;p&gt;The relay matters too. When a subagent proposes fixes, the web-developer passes the proposal to me instead of applying it on its own. Two checkpoints, no silent changes, and if something unexpected shows up mid-application, the instruction is to stop and report instead of improvising.&lt;/p&gt;




&lt;h2&gt;
  
  
  Anatomy of an Agent File 🔧
&lt;/h2&gt;

&lt;p&gt;OpenCode has primary agents (the ones you talk to, switched with Tab) and subagents (specialists called by a primary agent, or by you with an @ mention). Agents live in &lt;code&gt;.opencode/agents/&lt;/code&gt; and the file name becomes the agent name. Here's the frontmatter of my code-reviewer, trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Expert code reviewer for the current project, proposes fixes and applies them only after approval.&lt;/span&gt;
&lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;subagent&lt;/span&gt;
&lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#f59e0b"&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# load and follow the dedicated skill&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;skill&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code-review&lt;/span&gt;
    &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;
  &lt;span class="c1"&gt;# approval gate: every write asks for confirmation&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;edit&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
    &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ask&lt;/span&gt;
  &lt;span class="c1"&gt;# web access for docs and known issues&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;webfetch&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
    &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;
  &lt;span class="c1"&gt;# connected MCP servers, when necessary&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*_*"&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
    &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;
  &lt;span class="c1"&gt;# sandbox: nothing outside the workspace&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external_directory&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
    &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deny&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details I like. First, the sandbox rule goes last on purpose: for &lt;code&gt;external_directory&lt;/code&gt; the last matching rule wins, so that deny overrides every allow above it, including the MCP one. Agents can't read or write outside the current project, not even through the terminal. Second, web access and MCP servers are always allowed, but the prompt tells the agent not to overuse them and to try project knowledge first.&lt;/p&gt;

&lt;p&gt;Below the frontmatter comes the prompt: role, process, operating boundaries and the report format.&lt;/p&gt;




&lt;h2&gt;
  
  
  One Dedicated Skill Per Subagent 🎯
&lt;/h2&gt;

&lt;p&gt;Each subagent has a global skill it must load first, before doing anything else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;code-reviewer loads &lt;strong&gt;code-review&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;seo-expert loads &lt;strong&gt;seo-audit&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;ux-ui-expert loads &lt;strong&gt;web-design&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;documentation-updater loads &lt;strong&gt;update-documentation&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The split is simple. The agent file says who the agent is and what it's allowed to do. The skill holds the actual checklist and workflow, loaded on demand so it doesn't clutter the context when it isn't needed. For example, the documentation-updater follows a fixed path: establish repository context, understand the change, inspect the implementation, search for documentation drift, plan, edit, validate, review the diff.&lt;/p&gt;

&lt;p&gt;Because the skills are global, I can reuse them across every project and improve them in one place.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Delegation Works 🧭
&lt;/h2&gt;

&lt;p&gt;The web-developer prompt has a dedicated section for each specialist, saying exactly when to call it: code reviews, SEO audits and pre-ship checks, UI work and responsive fixes, documentation and changelog updates. It's explicit about what the specialist will bring back, too.&lt;/p&gt;

&lt;p&gt;The reports are structured and that's what makes them useful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;code-reviewer&lt;/strong&gt; returns findings ordered from critical to info, each with &lt;code&gt;path:line&lt;/code&gt;, why it matters and a concrete fix. It also notes what is good, because a review that is only a list of problems isn't balanced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;seo-expert&lt;/strong&gt; returns findings by severity grouped as on-page, technical and content, with before and after snippets, a separate list of quick wins, and the items that need input from me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ux-ui-expert&lt;/strong&gt; returns design proposals ranked by impact, the files to edit, asset recommendations and anything it can't decide without brand facts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;documentation-updater&lt;/strong&gt; returns a plan first: files to modify, files to add, files to leave alone and why. It only documents what it can verify against the implementation.&lt;/p&gt;

&lt;p&gt;And one rule I really like, shared by all of them: if a fact only I know (brand info, copy, a release version), flag it and ask, never invent it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Beats a Single Agent 💡
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fresh eyes.&lt;/strong&gt; The agent that wrote the code isn't the one reviewing it. That alone catches more than any "now review what you just wrote" prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cleaner context.&lt;/strong&gt; Each subagent works in its own child session, so the review reasoning doesn't pollute the building conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predictable behavior.&lt;/strong&gt; Same approval gate, same sandbox, same reporting style, across the whole team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The routine is built in.&lt;/strong&gt; Review, SEO, design and docs checks are part of the setup, not something I have to remember at the end of every task.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Downsides 😬
&lt;/h2&gt;

&lt;p&gt;It's not free magic. More agents means more tokens and more waiting, so for a one line fix it's overkill and I simply stay in the web-developer. The approval flow adds clicks, and with two checkpoints it can feel heavy on small tasks, though I prefer that to surprise edits. Subagent descriptions need care too, because similar descriptions make the primary agent pick the wrong one. And a review only helps if you actually read it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts 🌟
&lt;/h2&gt;

&lt;p&gt;Splitting one do-everything agent into a team looked like a small change, but it changed how I work. I stopped micromanaging prompts at the end of every task, and I got a setup where nothing touches my code without a proposal and a yes from me.&lt;/p&gt;

&lt;p&gt;If you use OpenCode, start small: one primary agent plus one reviewer with the approval gate. Once you see the difference, you'll want the rest of the team.&lt;/p&gt;

&lt;p&gt;Happy coding! ✨&lt;/p&gt;




&lt;p&gt;Hi 👋🏻&lt;br&gt;
My name is Domenico, software developer passionate of Open Source, I write article about it for share my knowledge and experience.&lt;br&gt;
Don't forget to visit my social links to discover my links, check out Domenico Tenace Open Labs for my open source projects and subscribe to my newsletter! 🫰🏻&lt;/p&gt;

&lt;p&gt;🌲 Links: &lt;a href="https://links.domenicotenace.dev/" rel="noopener noreferrer"&gt;https://links.domenicotenace.dev/&lt;/a&gt;&lt;br&gt;
🐙 Domenico Tenace Open Labs: &lt;a href="https://github.com/Domenico-Tenace-Open-Labs" rel="noopener noreferrer"&gt;https://github.com/Domenico-Tenace-Open-Labs&lt;/a&gt;&lt;br&gt;
📰 Newsletter: &lt;a href="https://domenicotenace.substack.com/subscribe" rel="noopener noreferrer"&gt;https://domenicotenace.substack.com/subscribe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you like my content or want to support my work, you can support me with a small donation. I would be grateful 🥹&lt;/p&gt;

&lt;p&gt;☕ Buy me a Coffee: &lt;a href="https://buymeacoffee.com/domenicotenace" rel="noopener noreferrer"&gt;https://buymeacoffee.com/domenicotenace&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>programming</category>
    </item>
    <item>
      <title>Closed vs Open Source AI: Who Really Owns Your Data? 🔓</title>
      <dc:creator>Domenico Tenace</dc:creator>
      <pubDate>Mon, 12 Oct 2026 08:18:12 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/closed-vs-open-source-ai-who-really-owns-your-data-1jd3</link>
      <guid>https://dev.to/playfulprogramming/closed-vs-open-source-ai-who-really-owns-your-data-1jd3</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Hey everyone 👋&lt;/p&gt;

&lt;p&gt;When we pick an AI model, we usually look at benchmarks, speed and price. Fair enough. But there's a question most teams skip until it's too late: who owns the data, and who holds the controls?&lt;/p&gt;

&lt;p&gt;Right now the AI world is split in two. On one side, proprietary models, the classic "black boxes". On the other, open weights models you can download and run yourself. Let me walk you through why this choice matters more than it looks. Let's dive in! 🤙&lt;/p&gt;




&lt;h2&gt;
  
  
  Closed vs Open: The Quick Version
&lt;/h2&gt;

&lt;p&gt;Closed models live behind an API. You send text, you get text back, and what happens in between is not your business. They're easy to start with, and that's why so many companies pick them.&lt;/p&gt;

&lt;p&gt;Open weights models (think Llama, Mistral, Qwen, DeepSeek) are different. You get the model itself, so you can run it on your own server, in a private cloud, or even on a beefy laptop. One note: "open weights" is not always the same as "open source" in the strict sense, so always read the license before building a product on top of one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Open Source Is Worth a Look
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Your Data Stays at Home 🔒
&lt;/h3&gt;

&lt;p&gt;With a self-hosted model, nothing leaves your infrastructure. No customer data, no internal documents, no proprietary code sent to someone else's servers. If you work in healthcare, finance, or you just have a nervous legal team, this alone can settle the discussion.&lt;/p&gt;

&lt;h3&gt;
  
  
  No Vendor Lock-in 🔓
&lt;/h3&gt;

&lt;p&gt;Prices change. APIs get deprecated. Terms of service get rewritten on a random Tuesday. If your whole product depends on one provider, every one of those changes is your problem, and you get no vote.&lt;/p&gt;

&lt;p&gt;With open models you control the infrastructure, so you can move, switch or fork whenever you want.&lt;/p&gt;

&lt;h3&gt;
  
  
  You Can Look Inside and Make It Yours 🛠️
&lt;/h3&gt;

&lt;p&gt;No black box means you can inspect what you're running. You can also fine-tune a model for your own domain and keep full ownership of the result. Try doing that with a closed API, and you'll quickly find where the rules stop.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Catch
&lt;/h2&gt;

&lt;p&gt;I'm not here to sell you a dream, so let's be honest about the downsides.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Running models costs real money, in GPUs or cloud instances.&lt;/li&gt;
&lt;li&gt;Someone has to maintain it: updates, scaling, monitoring, security. That someone is you.&lt;/li&gt;
&lt;li&gt;The very top closed models still tend to lead on the hardest tasks, even if the gap keeps shrinking.&lt;/li&gt;
&lt;li&gt;Licenses vary a lot, and some come with restrictions you won't like.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're a small team with no infra experience, a managed API might be the smarter call, at least at the start.&lt;/p&gt;




&lt;h2&gt;
  
  
  So What Should You Do? 🎯
&lt;/h2&gt;

&lt;p&gt;For most teams, the answer isn't "pick a side", it's "keep the exit open".&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with a closed API if you need to move fast.&lt;/li&gt;
&lt;li&gt;Put a thin abstraction layer between your code and the model, so swapping providers is a config change, not a rewrite.&lt;/li&gt;
&lt;li&gt;Use open models for the sensitive stuff, or for tasks where a smaller model is good enough and much cheaper.&lt;/li&gt;
&lt;li&gt;Test open alternatives regularly. The landscape moves every few weeks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open source fits best when privacy is non negotiable, when you have enough volume to justify your own infra, or when you need deep customization. Closed fits best when you want zero ops and the strongest raw performance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts 🌟
&lt;/h2&gt;

&lt;p&gt;This isn't a philosophical debate, it's a practical one. The more AI becomes the core of your product, the more it matters who controls it. Open source is also what keeps a handful of companies from owning the entire cognitive infrastructure we all rely on.&lt;/p&gt;

&lt;p&gt;You don't have to go all in. Just make sure that if the rules change tomorrow, you have somewhere to go.&lt;/p&gt;

&lt;p&gt;Happy coding! ✨&lt;/p&gt;




&lt;p&gt;Hi 👋🏻&lt;br&gt;
My name is Domenico, software developer passionate of Open Source, I write article about it for share my knowledge and experience.&lt;br&gt;
Don't forget to visit my social links to discover my links, check out Domenico Tenace Open Labs for my open source projects and subscribe to my newsletter! 🫰🏻&lt;/p&gt;

&lt;p&gt;🌲 Links: &lt;a href="https://links.domenicotenace.dev/" rel="noopener noreferrer"&gt;https://links.domenicotenace.dev/&lt;/a&gt;&lt;br&gt;
🐙 Domenico Tenace Open Labs: &lt;a href="https://github.com/Domenico-Tenace-Open-Labs" rel="noopener noreferrer"&gt;https://github.com/Domenico-Tenace-Open-Labs&lt;/a&gt;&lt;br&gt;
📰 Newsletter: &lt;a href="https://domenicotenace.substack.com/subscribe" rel="noopener noreferrer"&gt;https://domenicotenace.substack.com/subscribe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you like my content or want to support my work, you can support me with a small donation. I would be grateful 🥹&lt;/p&gt;

&lt;p&gt;☕ Buy me a Coffee: &lt;a href="https://buymeacoffee.com/domenicotenace" rel="noopener noreferrer"&gt;https://buymeacoffee.com/domenicotenace&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>llm</category>
      <category>programming</category>
    </item>
    <item>
      <title>Seasoned Developer Advice after 25 years in IT</title>
      <dc:creator>Emanuele Bartolesi</dc:creator>
      <pubDate>Fri, 09 Oct 2026 10:09:24 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/seasoned-developer-advice-after-25-years-in-it-5dpg</link>
      <guid>https://dev.to/playfulprogramming/seasoned-developer-advice-after-25-years-in-it-5dpg</guid>
      <description>&lt;p&gt;I've now been working in IT for exactly 25 years.&lt;/p&gt;

&lt;p&gt;My impostor syndrome is still very much present. Writing an article called "seasoned developer advice" makes me hesitate a little. There is always something I don't know, a technology I haven't used, or someone who understands a subject much better than I do.&lt;/p&gt;

&lt;p&gt;But after 25 years, I think I can start sharing a few thoughts.&lt;/p&gt;

&lt;p&gt;These are opinions about how to work, build software, and stay in this profession. Take what helps, question the rest, and adapt it to the team and situation you have in front of you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask the question you are embarrassed to ask
&lt;/h2&gt;

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

&lt;p&gt;If a requirement is unclear, ask about it before you turn it into code.&lt;/p&gt;

&lt;p&gt;What does "active customer" mean? Who can approve this operation? What should happen when the external service is unavailable? These questions can feel basic, especially when everyone else in the meeting appears to understand everything.&lt;/p&gt;

&lt;p&gt;I would rather admit that I don't understand a business rule than build an entire feature around my interpretation of it. The same applies to an unfamiliar piece of code. Ask someone to walk you through it, then explain it back in your own words.&lt;br&gt;
This step is even easier with AI nowdays, but asking a colleague still an easier thing to understand maybe something that AI doesn't know behind a business choice or something like that.&lt;/p&gt;

&lt;p&gt;Experience should make it easier to say "I don't know yet." I still have to remind myself of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give coordination an owner
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwvipn9z9svvlcr92489k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwvipn9z9svvlcr92489k.png" alt="Plush Emanuele arranging connected task cards on a felt board." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Developers need time to develop. If the lead developer also has to chase every dependency, negotiate the scope, update stakeholders, and organize the release, account for that work in the plan.&lt;/p&gt;

&lt;p&gt;On a larger project, I would want someone explicitly responsible for coordination. That might be a project manager, a delivery lead, or another role that fits the organization. On a small team, it may be a shared responsibility, but the decisions still need an owner.&lt;/p&gt;

&lt;p&gt;A date written in a ticket does not resolve a blocked dependency. Someone has to talk to the other team, agree on what happens next, and adjust the plan when the answer changes.&lt;/p&gt;

&lt;p&gt;When estimating a feature, include that work. A week full of meetings leaves less time for implementation than a week with clear decisions and uninterrupted time.&lt;br&gt;
Actually we are still doing this mistake to "overmeetings" but I hope it will change soon or later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make meetings earn their place
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feyio1e0zl6lpq0bp99up.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feyio1e0zl6lpq0bp99up.png" alt="Plush Emanuele at a meeting table with an hourglass and a decision board." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I don't think every team needs the same meeting schedule.&lt;/p&gt;

&lt;p&gt;A short conversation with the person who understands the business process can be useful while a feature is taking shape. Showing a working screen may reveal that the approval flow is wrong before you spend another week building it.&lt;/p&gt;

&lt;p&gt;A daily meeting where everyone reads out tickets already visible on the board is harder to justify.&lt;/p&gt;

&lt;p&gt;For me, a useful meeting resolves a question, exposes a dependency, or produces a decision. If an update can be written down and read when people have time, try that. If a disagreement needs a conversation, have the conversation.&lt;/p&gt;

&lt;p&gt;Record the outcome where the work lives. A decision made on a call is easy to miss if the developer implementing it was somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write code for the person who has to change it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbwve3fp0ewbg2ayoawrc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbwve3fp0ewbg2ayoawrc.png" alt="Plush Emanuele showing readable code on a laptop beside connected modules." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Readable code matters most when someone needs to fix it under pressure.&lt;/p&gt;

&lt;p&gt;I prefer a method with a clear name and an obvious flow over an abstraction that takes three files to understand. There are good reasons for abstraction, but I want to know which problem it solves before adding another layer.&lt;/p&gt;

&lt;p&gt;The same caution applies to shared code. Two methods that look similar may represent business rules that will evolve independently. Putting them in one common library creates a relationship you will have to maintain.&lt;/p&gt;

&lt;p&gt;Extract code when the shared behavior is understood. If several applications depend on it, think about compatibility, ownership, and how updates will reach them. Publishing a package creates a responsibility beyond the original project.&lt;br&gt;
Still a valid advice also for we use a lot of AI for writing code.&lt;br&gt;
We need anywayt to review what is written to understand if we can read and understand the code anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leave the explanation you would want to find later
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu66h82yrr9a52g029e9l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu66h82yrr9a52g029e9l.png" alt="Plush Emanuele writing diagrams in a notebook beside a laptop." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Code can show what happens without explaining why it must happen that way.&lt;/p&gt;

&lt;p&gt;A comment explaining that a file format is required by an external system is useful. A comment saying that the next line increments a counter usually adds little.&lt;/p&gt;

&lt;p&gt;I want documentation that helps someone do the work: how to run the application, which configuration is needed, how to execute the tests, and what to check when a deployment fails. Keep those instructions close to the repository and update them when the workflow changes.&lt;/p&gt;

&lt;p&gt;There is also value in a short decision note. If the team rejected a queue because the current workload did not justify operating one, record that reasoning and the condition that would make you reconsider.&lt;/p&gt;

&lt;p&gt;AI can help draft these explanations. Check them against the code and the actual setup before treating them as documentation. A plausible installation guide that cannot be followed is another problem for the next developer to debug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Think about the application after it leaves your machine
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff01lcapyywv10288tljt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff01lcapyywv10288tljt.png" alt="Plush Emanuele inspecting a server with a recovery arrow and a shield." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A feature needs more than a successful local run.&lt;/p&gt;

&lt;p&gt;Before shipping, I want to know how we will recognize a failure and what we can do about it. A useful log entry should give enough context to trace an operation without exposing credentials or customer data. An exception with no context can leave you searching through several unrelated requests.&lt;/p&gt;

&lt;p&gt;Be deliberate about tests, too. If a feature moves data between customers, test that boundary. If it sends notifications, consider what happens when an operation is repeated. The failure case should influence the test, rather than becoming an afterthought once the happy path passes.&lt;/p&gt;

&lt;p&gt;And discuss recovery. Reverting code will not undo every effect of that code. A database change or an email already sent needs its own consideration.&lt;/p&gt;

&lt;p&gt;These questions belong in development, while you can still change the design without an incident forcing the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review the code without making it personal
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7xn1p41u494hx4q59xzb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7xn1p41u494hx4q59xzb.png" alt="Plush Emanuele reviewing a laptop with a magnifying glass and a friendly feedback bubble." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Having someone question your implementation can be uncomfortable. With impostor syndrome already in the room, a review comment can feel larger than it is.&lt;/p&gt;

&lt;p&gt;Try to keep the conversation specific. Explain which behavior concerns you, point to the assumption, and suggest a way to verify it. "What happens if this request is retried?" is a useful opening for a discussion.&lt;/p&gt;

&lt;p&gt;As the author, explain your reasoning and be willing to change it. As the reviewer, distinguish a defect from a preference. A naming choice does not deserve the same treatment as a missing authorization check.&lt;/p&gt;

&lt;p&gt;I also like reviews small enough that a colleague can understand the change properly. If a bug fix includes a folder reorganization and a new framework, consider separating those decisions.&lt;/p&gt;

&lt;p&gt;The goal is a change the team understands well enough to maintain. Being the person who wins the argument contributes very little to that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make maintenance part of the plan
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F80apjn13cfirdcuue8ov.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F80apjn13cfirdcuue8ov.png" alt="Plush Emanuele maintaining a software box beside a calendar." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Framework upgrades, dependency updates, and refactoring need time on the calendar.&lt;/p&gt;

&lt;p&gt;It is easy to postpone them because the application still works. When you do, be explicit about what you are accepting and when you will revisit it. Otherwise, postponement becomes the plan by default.&lt;/p&gt;

&lt;p&gt;A refactoring ticket should name the problem it will address. Perhaps adding a validation rule currently requires changing five different places. That gives the team a concrete improvement to assess and a way to tell whether the work helped.&lt;/p&gt;

&lt;p&gt;I would apply the same discipline to modernization. Explain what the upgrade enables, which risk it addresses, and how you will validate the change. The age of a framework alone is a weak explanation for rewriting a working application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep learning through things you can use
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F01fymquljz45tenzs45n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F01fymquljz45tenzs45n.png" alt="Plush Emanuele learning from a book and experimenting with a laptop." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You cannot study every language, framework, cloud service, and AI tool that appears.&lt;/p&gt;

&lt;p&gt;Choose something relevant and spend enough time with it to understand where it helps. Build a small application, add a feature to a side project, or investigate a part of your production system that you usually avoid.&lt;/p&gt;

&lt;p&gt;A course can give you structure. Applying the lesson gives you questions the course may never have covered.&lt;/p&gt;

&lt;p&gt;I would also leave room to learn from colleagues. Someone with less experience may know a tool or technique you have never used. Twenty-five years in IT should not make that conversation awkward.&lt;/p&gt;

&lt;p&gt;Your main language is useful expertise. Stay curious about the parts around it: the database query, the deployment pipeline, or the interface the user has to navigate. A problem does not stop at the boundary of your preferred stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose tools you can trust and understand
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsruu8qfgvavlyu41xan2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsruu8qfgvavlyu41xan2.png" alt="Plush Emanuele choosing tools beside a laptop and a small AI robot." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use an editor that lets you work comfortably. Learn its debugger, navigation, and refactoring tools before assuming a different editor will solve your frustration.&lt;/p&gt;

&lt;p&gt;I would take the same approach to AI. Start with work where you can judge the result: exploring unfamiliar code, preparing a first test draft, or checking an implementation for missed cases.&lt;/p&gt;

&lt;p&gt;Read the output and run the checks. When the tool proposes a design, ask what assumptions it made. You remain responsible for the change you submit.&lt;/p&gt;

&lt;p&gt;Trying another tool or model can be useful when it answers a specific question about your workflow. You don't need a collection of subscriptions just to feel current.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a way of working you can keep doing
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxd8nq266pz72kx45ggv0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxd8nq266pz72kx45ggv0.png" alt="Plush Emanuele taking a coffee break beside a closed laptop." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After 25 years, I want my advice to include what happens away from the keyboard.&lt;/p&gt;

&lt;p&gt;Take breaks. Get up, walk around, do sports, and give yourself permission to stop staring at the same problem. If you are stuck, ask for help rather than making another exhausted hour the price of proving that you can solve it alone.&lt;/p&gt;

&lt;p&gt;Talk to teammates sometimes without a ticket as the agenda. It is easier to ask someone a difficult question when your only interaction with them has not been a review disagreement.&lt;/p&gt;

&lt;p&gt;And keep something outside work. There will always be another release, article, framework, or project. Closing the laptop does not mean you stopped caring about your profession.&lt;/p&gt;

&lt;p&gt;I still don't feel that 25 years gives me an answer to everything. It does give me enough reason to share these thoughts, even while the impostor syndrome tells me to wait a little longer.&lt;/p&gt;




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

&lt;p&gt;If you enjoyed this blog post and want to learn more about C# development, you might be interested in subscribing to my bi-weekly newsletter called Dev Dispatch. By subscribing, you will get access to exclusive content, tips, and tricks, as well as updates on the latest news and trends in the development world. You will also be able to interact with me, and share your feedback and suggestions. To subscribe, simply navigate to &lt;a href="https://buttondown.email/kasuken?tag=devto" rel="noopener noreferrer"&gt;https://buttondown.email/kasuken?tag=devto&lt;/a&gt;, enter your email address and click on the Subscribe button. You can unsubscribe at any time. Thank you for your support!&lt;/p&gt;

</description>
      <category>career</category>
      <category>careerdevelopment</category>
      <category>weeklyretro</category>
    </item>
    <item>
      <title>My Windows 11 Laptop Was Struggling with Claude and Codex. Defender Exclusions Helped.</title>
      <dc:creator>Emanuele Bartolesi</dc:creator>
      <pubDate>Tue, 06 Oct 2026 15:01:47 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/my-windows-11-laptop-was-struggling-with-claude-and-codex-defender-exclusions-helped-2iln</link>
      <guid>https://dev.to/playfulprogramming/my-windows-11-laptop-was-struggling-with-claude-and-codex-defender-exclusions-helped-2iln</guid>
      <description>&lt;p&gt;My Windows 11 laptop sometimes struggled while I was working with Claude and Codex or GitHub Copilot. CPU usage was high, and the fans were running like crazy.&lt;br&gt;
It means also that my battery runs out in 1 hour and 30 minutes 😀&lt;/p&gt;

&lt;p&gt;When I checked Task Manager, Antimalware Service Executable stood out. Disk usage was around 90%, with heavy CPU activity as well, while the two tools were running.&lt;/p&gt;

&lt;p&gt;That process belongs to Microsoft Defender Antivirus. You can find it as &lt;code&gt;Antimalware Service Executable&lt;/code&gt; in Task Manager's Processes tab, or &lt;code&gt;MsMpEng.exe&lt;/code&gt; in the Details tab. &lt;a href="https://learn.microsoft.com/en-us/defender-endpoint/microsoft-defender-antivirus-windows" rel="noopener noreferrer"&gt;Microsoft documents both names here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I suspected Defender's scanning of the tools' local folders was contributing to the slowdown. Adding folder exclusions solved the problem I was seeing on my laptop. This is my experience with one machine, rather than proof that Claude or Codex always causes this behavior.&lt;/p&gt;
&lt;h2&gt;
  
  
  The folders I excluded
&lt;/h2&gt;

&lt;p&gt;These are the four entries in my Windows Security exclusion list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\Users\barto\.claude
C:\Users\barto\.codex
C:\Users\barto\.copilot
C:\Users\barto\.github-copilot-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fta5jqbgk3owcewv1jx8o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fta5jqbgk3owcewv1jx8o.png" alt="Windows Security exclusion list showing the .claude, .codex, .copilot, and .github-copilot-cli folders under my Windows user profile." width="375" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The last two entries are for my Copilot tooling. Their presence in this list does not establish that they contributed to the Claude and Codex slowdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is a security tradeoff:&lt;/strong&gt; a folder exclusion removes Defender antivirus scanning coverage for that folder and its contents. Files placed there later are also covered by the exclusion. Trusting the tool does not guarantee that every file it writes or downloads is safe. &lt;a href="https://support.microsoft.com/en-us/windows/security/threat-malware-protection/virus-and-threat-protection-in-the-windows-security-app" rel="noopener noreferrer"&gt;Microsoft explains the exclusion options and risks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Treat this list as a record of my setup. For your machine, identify the affected folder first and keep any exclusion as narrow as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to add a folder exclusion in Windows 11
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Start&lt;/strong&gt;, search for &lt;strong&gt;Windows Security&lt;/strong&gt;, and open it.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Virus &amp;amp; threat protection&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Virus &amp;amp; threat protection settings&lt;/strong&gt;, select &lt;strong&gt;Manage settings&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Scroll to &lt;strong&gt;Exclusions&lt;/strong&gt; and select &lt;strong&gt;Add or remove exclusions&lt;/strong&gt;. Approve the administrator prompt if it appears.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Add an exclusion&lt;/strong&gt;, then &lt;strong&gt;Folder&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Browse to the folder you want to exclude, for example &lt;code&gt;C:\Users\YOUR_USERNAME\.claude&lt;/code&gt;, and select &lt;strong&gt;Select Folder&lt;/strong&gt;. Replace &lt;code&gt;YOUR_USERNAME&lt;/code&gt; with your Windows user folder name.&lt;/li&gt;
&lt;li&gt;Confirm that the folder appears in the exclusion list. Repeat for other folders only where needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These steps follow &lt;a href="https://support.microsoft.com/en-us/windows/security/threat-malware-protection/virus-and-threat-protection-in-the-windows-security-app" rel="noopener noreferrer"&gt;Microsoft's Windows Security guidance&lt;/a&gt;. On a company-managed laptop, your IT team may control these settings.&lt;/p&gt;




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

&lt;p&gt;If you enjoyed this blog post and want to learn more about C# development, you might be interested in subscribing to my bi-weekly newsletter called Dev Dispatch. By subscribing, you will get access to exclusive content, tips, and tricks, as well as updates on the latest news and trends in the development world. You will also be able to interact with me, and share your feedback and suggestions. To subscribe, simply navigate to &lt;a href="https://buttondown.email/kasuken?tag=devto" rel="noopener noreferrer"&gt;https://buttondown.email/kasuken?tag=devto&lt;/a&gt;, enter your email address and click on the Subscribe button. You can unsubscribe at any time. Thank you for your support!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>githubcopilot</category>
    </item>
    <item>
      <title>Moving to European Digital Services, One Decision at a Time</title>
      <dc:creator>Emanuele Bartolesi</dc:creator>
      <pubDate>Thu, 24 Sep 2026 16:13:19 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/moving-to-european-digital-services-one-decision-at-a-time-3j68</link>
      <guid>https://dev.to/playfulprogramming/moving-to-european-digital-services-one-decision-at-a-time-3j68</guid>
      <description>&lt;p&gt;My move to European digital services includes Proton, Qwant, and DeepL. When it came to Notion, I went further and replaced it with &lt;a href="https://github.com/kasuken/Brainy" rel="noopener noreferrer"&gt;Brainy&lt;/a&gt;, the product I built for my own notes, projects, and tasks.&lt;/p&gt;

&lt;p&gt;That sounds like a migration, but the word creates the wrong expectation. I did not spend a weekend exporting every account, cancelling every subscription, and declaring myself independent from American technology on Monday morning.&lt;/p&gt;

&lt;p&gt;I am changing one decision at a time.&lt;/p&gt;

&lt;p&gt;I do not want a US provider to be the automatic answer for every digital need simply because it is the most visible option. Europe has credible products. Some are good enough to compete directly, and some fit my needs better. That does not mean I will replace a working service because a company has the wrong flag beside its logo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proton made the change visible
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://proton.me/" rel="noopener noreferrer"&gt;Proton&lt;/a&gt; is where the migration became a system rather than a few unrelated experiments.&lt;/p&gt;

&lt;p&gt;Proton Mail replaced my previous email setup. Proton VPN replaced NordVPN. Proton Pass replaced NordPass. I also use &lt;a href="https://proton.me/lumo" rel="noopener noreferrer"&gt;Lumo&lt;/a&gt; for selected private AI conversations rather than treating every chat as suitable for any general-purpose assistant.&lt;/p&gt;

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

&lt;p&gt;The immediate benefit was consolidation. One privacy-focused Swiss ecosystem replaced several separate accounts and subscriptions. I did not need to find a different European company for email, another for passwords, and another for the VPN before making progress.&lt;/p&gt;

&lt;p&gt;There is an obvious trade-off: consolidation reduces vendor sprawl while increasing dependence on Proton. Moving three services into one company is not diversification. I still prefer the current setup, but it would be dishonest to describe concentration as independence.&lt;/p&gt;

&lt;p&gt;Lumo has a similar boundary. Proton says stored conversations use zero-access encryption, which is why it has a place in my private workflow. I do not pretend that it replaces every AI service or every model. The choice depends on the conversation and the job I need to complete.&lt;br&gt;
For the moment I am using Lumo only for questions related my health and other sensitive information.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqxcbgks6wztacu5edrya.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqxcbgks6wztacu5edrya.webp" alt="Lumo" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The corporate map is more complicated than the logos
&lt;/h2&gt;

&lt;p&gt;Once I started checking where services were based, the simple Europe-versus-USA story stopped working.&lt;/p&gt;

&lt;p&gt;NordVPN is not an American service. &lt;a href="https://support.nordvpn.com/hc/en-us/articles/19441152966161-Where-is-NordVPN-based" rel="noopener noreferrer"&gt;NordVPN says it is based in and operates under Panama's jurisdiction&lt;/a&gt;, while its parent company, Nord Security, is based in the Netherlands. Moving to Proton VPN is a Panama-to-Switzerland change at the service-jurisdiction level, with a European parent company also involved in the old setup.&lt;/p&gt;

&lt;p&gt;WeTransfer is European too. &lt;a href="https://wetransfer.com/documents/WeTransfer_Data_Processing_Agreement.pdf" rel="noopener noreferrer"&gt;WeTransfer B.V. is based in Amsterdam&lt;/a&gt;, so my switch to SwissTransfer was not about escaping an American company. It was about the product.&lt;/p&gt;

&lt;p&gt;For my file-sharing needs, &lt;a href="https://www.swisstransfer.com/" rel="noopener noreferrer"&gt;SwissTransfer&lt;/a&gt; has a privacy model I prefer and a much more useful free allowance for large files. Infomaniak says SwissTransfer accepts transfers up to 50 GB, stores the files in Switzerland, supports password protection, and deletes the files after the transfer period. &lt;a href="https://help.wetransfer.com/hc/en-us/articles/360036942752-What-does-a-free-WeTransfer-account-offer-me" rel="noopener noreferrer"&gt;WeTransfer's current free-account documentation&lt;/a&gt; describes an allowance of up to ten transfers or 3 GB in total over a rolling 30-day period.&lt;/p&gt;

&lt;p&gt;That is a concrete difference. I am not using SwissTransfer because the landing page has a Swiss flag. I am using it because privacy and larger transfers matter in this workflow.&lt;/p&gt;

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

&lt;p&gt;This distinction changed how I evaluate alternatives. I now look separately at the service jurisdiction, parent company, data location, underlying infrastructure, business model, portability, and actual product quality. Those facts do not always point to the same country.&lt;/p&gt;
&lt;h2&gt;
  
  
  Some changes need almost no migration
&lt;/h2&gt;

&lt;p&gt;Qwant and DeepL were easier decisions.&lt;/p&gt;

&lt;p&gt;I use &lt;a href="https://www.qwant.com/" rel="noopener noreferrer"&gt;Qwant&lt;/a&gt; for search. Qwant is a French company, and its current privacy policy says it does not use targeted advertising or retain search history by default. Search is also easy to test: use it, compare the results, and keep another engine available when it does not find what you need.&lt;/p&gt;

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

&lt;p&gt;I use &lt;a href="https://www.deepl.com/" rel="noopener noreferrer"&gt;DeepL&lt;/a&gt; for translation. It was founded in Cologne, Germany, and it is a good example of why the word "alternative" can be misleading. I do not need to treat a European translation service as a political compromise. I can use it because it does the work.&lt;/p&gt;

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

&lt;p&gt;Neither choice required a dramatic data migration. There was no archive to restructure and no DNS record that could break a production system. They simply became part of the workflow.&lt;/p&gt;

&lt;p&gt;New domains are another low-risk entry point. I now use &lt;a href="https://www.infomaniak.com/en/domains" rel="noopener noreferrer"&gt;Infomaniak Domains&lt;/a&gt; instead of GoDaddy for new registrations. I am not forcing every existing domain through an immediate transfer. When I need a new domain, the new default is a Swiss, ICANN-accredited registrar. That is progress without creating unnecessary operational risk.&lt;/p&gt;

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

&lt;p&gt;DNS is an even quieter change. I use &lt;a href="https://www.quad9.net/" rel="noopener noreferrer"&gt;Quad9&lt;/a&gt; as my resolver. Quad9 is a Swiss nonprofit, and its documentation says the recommended &lt;code&gt;9.9.9.9&lt;/code&gt; service blocks lookups for malicious domains without logging end-user IP addresses. It also supports encrypted DNS protocols. This is closer to changing an infrastructure setting than migrating an account, but it moves a foundational part of normal browsing away from whichever resolver happened to be the default.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Falb2sxzm6wglp80afe39.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Falb2sxzm6wglp80afe39.png" alt="Quad9" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Replacing Notion meant building the alternative
&lt;/h2&gt;

&lt;p&gt;Notion was different because a knowledge system accumulates structure as well as data. Notes connect to projects, tasks carry context, and the way information is captured affects whether it becomes useful work or remains another page in an archive.&lt;/p&gt;

&lt;p&gt;I replaced Notion with &lt;a href="https://www.brainy-me.com" rel="noopener noreferrer"&gt;Brainy&lt;/a&gt;, my own product.&lt;/p&gt;

&lt;p&gt;Brainy is a practical second brain organized around the PARA method. It captures notes and ideas, connects them to projects and tasks, and focuses on turning stored knowledge into action. The goal was not to reproduce every Notion feature. It was to build a workflow that matches how I move from an idea to a project, from a project to the next task, and from planning to today's work.&lt;/p&gt;

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

&lt;p&gt;Owning the product gives me control over that workflow, the data model, portability, and the roadmap, but it also gives me the maintenance work. I own the deployments, bugs, database changes, security decisions, and migration logic.&lt;/p&gt;

&lt;p&gt;This is the clearest reason the entire transition cannot happen in two hours. Replacing a service can mean changing a setting or moving years of habits and information. In this case, it also meant building and operating a product.&lt;/p&gt;

&lt;p&gt;For the moment, Brainy is free and you can create your own account here: &lt;a href="https://www.brainy-me.com" rel="noopener noreferrer"&gt;www.brainy-me.com&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sometimes the correct migration is no migration
&lt;/h2&gt;

&lt;p&gt;I still use Spotify.&lt;/p&gt;

&lt;p&gt;The reason is simple: Spotify is already European and it works for me. Its consumer terms identify Spotify AB in Stockholm, Sweden. Replacing it with another service only to increase the number of changed logos would add work without moving me closer to the actual goal.&lt;/p&gt;

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

&lt;p&gt;A useful migration should leave some rows unchanged. Otherwise the process is driven by ideology rather than requirements.&lt;/p&gt;

&lt;p&gt;Here is the current map:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workflow&lt;/th&gt;
&lt;th&gt;Current choice&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;Proton Mail&lt;/td&gt;
&lt;td&gt;Moved into the Proton ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPN&lt;/td&gt;
&lt;td&gt;Proton VPN&lt;/td&gt;
&lt;td&gt;Replaced NordVPN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passwords&lt;/td&gt;
&lt;td&gt;Proton Pass&lt;/td&gt;
&lt;td&gt;Replaced NordPass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private AI chats&lt;/td&gt;
&lt;td&gt;Lumo&lt;/td&gt;
&lt;td&gt;Used selectively for private conversations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large file transfers&lt;/td&gt;
&lt;td&gt;SwissTransfer&lt;/td&gt;
&lt;td&gt;Chosen for privacy and transfers up to 50 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New domain registrations&lt;/td&gt;
&lt;td&gt;Infomaniak&lt;/td&gt;
&lt;td&gt;Replaced GoDaddy as the default for new domains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;Quad9&lt;/td&gt;
&lt;td&gt;Uses a Swiss nonprofit resolver focused on privacy and security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search&lt;/td&gt;
&lt;td&gt;Qwant&lt;/td&gt;
&lt;td&gt;In active use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Translation&lt;/td&gt;
&lt;td&gt;DeepL&lt;/td&gt;
&lt;td&gt;In active use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knowledge and planning&lt;/td&gt;
&lt;td&gt;Brainy&lt;/td&gt;
&lt;td&gt;Replaced Notion with my own product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Music&lt;/td&gt;
&lt;td&gt;Spotify&lt;/td&gt;
&lt;td&gt;Kept because the European service already fits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  The process I use before changing a service
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://european-alternatives.eu/" rel="noopener noreferrer"&gt;European Alternatives directory&lt;/a&gt; is useful for discovery, but inclusion in a directory is the beginning of the evaluation, not the conclusion.&lt;/p&gt;

&lt;p&gt;Before changing a service, I work through a small set of questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What job does the current product perform?&lt;/li&gt;
&lt;li&gt;Which features are genuinely non-negotiable, and which ones are habits?&lt;/li&gt;
&lt;li&gt;Where is the service legally based, where is the data processed, and which external platforms does it still depend on?&lt;/li&gt;
&lt;li&gt;Can I export the data in a useful format and move again later?&lt;/li&gt;
&lt;li&gt;Can I run the old and new services in parallel before cancelling anything?&lt;/li&gt;
&lt;li&gt;Is the alternative better, equivalent, or merely acceptable for this specific job?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last question matters. I am willing to accept a small difference when privacy, control, or supporting a European provider makes the trade worthwhile. I am not willing to ignore a product that fails the workflow simply because it is European.&lt;/p&gt;

&lt;p&gt;The safest changes happen at natural decision points. Register the next domain with a different registrar. Test another search engine without removing the old one. Import a copy of a password vault before cancelling the existing subscription. Send the next large file with a service that supports the size you need.&lt;/p&gt;

&lt;p&gt;Moving all of them at once would turn a preference into an outage plan.&lt;/p&gt;
&lt;h2&gt;
  
  
  Change the next decision
&lt;/h2&gt;

&lt;p&gt;I still use services from American companies, and I expect that to continue. Work, collaboration, audience, and network effects do not disappear because a European alternative exists.&lt;/p&gt;

&lt;p&gt;The difference is that I no longer want the American option to win by default. When a subscription renews, a new need appears, or a current product creates friction, I check what exists in Europe before repeating the old decision.&lt;/p&gt;

&lt;p&gt;The outcomes vary. A European product may be good enough, SwissTransfer may be better for large files, or Spotify may already be the European answer. Brainy sits at the other end of the range because I had to build the alternative around the workflow I wanted.&lt;/p&gt;

&lt;p&gt;This process does not need a dramatic finish. The next decision is enough.&lt;/p&gt;



&lt;p&gt;Thanks for reading this post, I hope you found it interesting!&lt;/p&gt;

&lt;p&gt;Feel free to follow me to get notified when new articles are out 🙂&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__79932"&gt;
    &lt;a href="/kasuken" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F79932%2Fbe7bcd76-1f69-4336-9155-6017aebb2c53.png" alt="kasuken image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/kasuken"&gt;Emanuele Bartolesi&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/kasuken"&gt;🏆 Microsoft MVP (12x) &amp;amp; GitHub Star (4x) ⭐
👨‍🎓 LinkedIn Technical Instructor 🎓&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>productivity</category>
    </item>
    <item>
      <title>INP and Partytown: give the main thread back to your users</title>
      <dc:creator>Nicolas Frizzarin</dc:creator>
      <pubDate>Wed, 23 Sep 2026 08:18:37 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/inp-and-partytown-give-the-main-thread-back-to-your-users-o9m</link>
      <guid>https://dev.to/playfulprogramming/inp-and-partytown-give-the-main-thread-back-to-your-users-o9m</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;You click on a button. Nothing happens.&lt;br&gt;
You click again. Still nothing.&lt;br&gt;
Then the menu opens... and closes right away.&lt;/p&gt;

&lt;p&gt;We have all lived this. It's a responsiveness problem. And since March 12, 2024, Google measures it with a Core Web Vital: &lt;strong&gt;INP&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Very often, the problem is not your code. It's the code you didn't write: analytics, tag managers, pixels, A/B testing tools...&lt;/p&gt;

&lt;p&gt;This article explains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What INP really measures&lt;/li&gt;
&lt;li&gt;Why third-party scripts hurt it so much&lt;/li&gt;
&lt;li&gt;How Partytown works&lt;/li&gt;
&lt;li&gt;How to use it in &lt;strong&gt;React&lt;/strong&gt; and &lt;strong&gt;Angular&lt;/strong&gt;, with real examples&lt;/li&gt;
&lt;li&gt;When you should &lt;strong&gt;not&lt;/strong&gt; use it&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  INP in a few words
&lt;/h2&gt;

&lt;p&gt;INP means &lt;strong&gt;Interaction to Next Paint&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It measures the time between a user interaction and the next frame painted by the browser. In other words: how long does the user wait before seeing that something happened?&lt;/p&gt;

&lt;p&gt;Only three types of interaction count:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a click with a mouse&lt;/li&gt;
&lt;li&gt;a tap on a touchscreen&lt;/li&gt;
&lt;li&gt;a key press&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scroll, hover and zoom are &lt;strong&gt;not&lt;/strong&gt; measured.&lt;/p&gt;

&lt;p&gt;INP looks at &lt;strong&gt;all&lt;/strong&gt; the interactions during the visit, and reports the worst one. If there are a lot of interactions, the browser ignores the highest one for every 50 interactions. That way, one random hiccup does not ruin your score.&lt;/p&gt;

&lt;p&gt;The thresholds, at the 75th percentile of your real users:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;INP&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;≤ 200 ms&lt;/td&gt;
&lt;td&gt;🟢 Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 ms to 500 ms&lt;/td&gt;
&lt;td&gt;🟠 Needs improvement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt; 500 ms&lt;/td&gt;
&lt;td&gt;🔴 Poor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Just a reminder: INP replaced FID (First Input Delay). FID only measured the waiting time of the &lt;strong&gt;first&lt;/strong&gt; interaction. INP measures &lt;strong&gt;every&lt;/strong&gt; interaction, from start to paint. Much stricter.&lt;/p&gt;
&lt;h2&gt;
  
  
  What's inside an interaction?
&lt;/h2&gt;

&lt;p&gt;An interaction has three phases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; user clicks                                              next frame
     │                                                        │
     ▼                                                        ▼
     ├────────────────┬──────────────────────┬────────────────┤
     │  Input delay   │ Processing duration  │ Presentation   │
     │                │                      │ delay          │
     └────────────────┴──────────────────────┴────────────────┘
     │◄───────────────── interaction latency ───────────────►│
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input delay&lt;/strong&gt;: the time before your event handlers start. The browser is busy with something else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processing duration&lt;/strong&gt;: the time to run all your event handlers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presentation delay&lt;/strong&gt;: the time to compute style, layout and paint the next frame.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep the first one in mind. This is where third-party scripts hurt the most.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main thread: a one lane road
&lt;/h2&gt;

&lt;p&gt;The browser has one main thread. It does everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;runs JavaScript&lt;/li&gt;
&lt;li&gt;handles events&lt;/li&gt;
&lt;li&gt;computes style and layout&lt;/li&gt;
&lt;li&gt;paints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And it does &lt;strong&gt;one thing at a time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A task longer than 50 ms is a &lt;strong&gt;long task&lt;/strong&gt;. During a long task, the browser can't respond to the user.&lt;/p&gt;

&lt;p&gt;Now imagine your page with Google Tag Manager, a marketing pixel and an analytics script. The user clicks at the wrong moment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Main thread, without Partytown

──[ your app ]──[ tag manager 180ms ]──[ pixel 120ms ]──[ click handler ]──[ paint ]──
                      ▲
                user clicks here
                      │◄──────── input delay ────────►│
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your click handler is fast. Your code is clean. And your INP is still bad.&lt;/p&gt;

&lt;p&gt;This is the key point: &lt;strong&gt;you can write perfect code and still fail INP&lt;/strong&gt;, because you share the road with scripts you don't control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Partytown: move the party somewhere else
&lt;/h2&gt;

&lt;p&gt;Partytown is a small library, lazy-loaded, maintained by the QwikDev team. Its goal is simple: move third-party scripts from the main thread into a &lt;strong&gt;web worker&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The philosophy: the main thread is for &lt;strong&gt;your&lt;/strong&gt; code. Everything that is not in the critical path can go elsewhere.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  subgraph Before["Without Partytown"]
    MT1["Main thread&amp;lt;br/&amp;gt;your app + GTM + pixel + analytics"]
  end
  subgraph After["With Partytown"]
    MT2["Main thread&amp;lt;br/&amp;gt;your app"]
    WW["Web worker&amp;lt;br/&amp;gt;GTM + pixel + analytics"]
    WW -. "DOM calls through a proxy" .-&amp;gt; MT2
  end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;And the same timeline as before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Main thread, with Partytown

──[ your app ]──[ click handler ]──[ paint ]──
                ▲
          user clicks here: almost no input delay

Web worker

──[ tag manager 180ms ]──[ pixel 120ms ]──   ← nobody waits for this
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The long tasks still exist. They just don't block the user anymore.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Partytown is still in &lt;strong&gt;beta&lt;/strong&gt;. It's not guaranteed to work for every script. Test before you ship.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;p&gt;Here comes the tricky part.&lt;/p&gt;

&lt;p&gt;A web worker has no DOM. No &lt;code&gt;document&lt;/code&gt;, no real &lt;code&gt;window&lt;/code&gt;. But third-party scripts expect them. They call &lt;code&gt;document.cookie&lt;/code&gt;, &lt;code&gt;getBoundingClientRect()&lt;/code&gt;, &lt;code&gt;addEventListener&lt;/code&gt;...&lt;/p&gt;

&lt;p&gt;On top of that, communication between a worker and the main thread is &lt;strong&gt;asynchronous&lt;/strong&gt;. Third-party scripts are written in a &lt;strong&gt;synchronous&lt;/strong&gt; way. No &lt;code&gt;await&lt;/code&gt;, no callback.&lt;/p&gt;

&lt;p&gt;So Partytown does two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the worker, it creates &lt;strong&gt;JavaScript Proxies&lt;/strong&gt; that look like &lt;code&gt;window&lt;/code&gt; and &lt;code&gt;document&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Each call to these proxies is sent to the main thread through a &lt;strong&gt;synchronous&lt;/strong&gt; channel, and the worker waits for the answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From the script's point of view, nothing changed. This code works as is inside the worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// blocking call, like on the main thread&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two ways to get this synchronous channel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Service Worker (the fallback)
&lt;/h3&gt;

&lt;p&gt;The worker sends a &lt;strong&gt;synchronous XHR&lt;/strong&gt;. A service worker intercepts it, asks the main thread, and answers.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
  participant W as Web worker (3rd-party script)
  participant SW as Service worker
  participant M as Main thread (DOM)
  W-&amp;gt;&amp;gt;SW: sync XHR "getBoundingClientRect()"
  SW-&amp;gt;&amp;gt;M: postMessage
  M--&amp;gt;&amp;gt;SW: result
  SW--&amp;gt;&amp;gt;W: response { x, y }
  Note over W: for the script, it was a normal blocking call&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;That's why you'll see a lot of &lt;code&gt;proxytown&lt;/code&gt; requests in the network tab. They are not real HTTP requests. They're handled locally. You can hide them with the &lt;code&gt;-url:proxytown&lt;/code&gt; filter in Chrome DevTools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Atomics (the fast one)
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;Atomics&lt;/code&gt; and &lt;code&gt;SharedArrayBuffer&lt;/code&gt;, the worker writes the request, calls &lt;code&gt;Atomics.wait()&lt;/code&gt;, and reads the result when the main thread answers. No service worker needed.&lt;/p&gt;

&lt;p&gt;It's about &lt;strong&gt;10x faster&lt;/strong&gt; to transfer data between threads.&lt;/p&gt;

&lt;p&gt;But there's a condition: the page must be &lt;strong&gt;cross-origin isolated&lt;/strong&gt;. You need these response headers on your document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;Cross&lt;/span&gt;-&lt;span class="n"&gt;Origin&lt;/span&gt;-&lt;span class="n"&gt;Embedder&lt;/span&gt;-&lt;span class="n"&gt;Policy&lt;/span&gt;: &lt;span class="n"&gt;credentialless&lt;/span&gt;
&lt;span class="n"&gt;Cross&lt;/span&gt;-&lt;span class="n"&gt;Origin&lt;/span&gt;-&lt;span class="n"&gt;Opener&lt;/span&gt;-&lt;span class="n"&gt;Policy&lt;/span&gt;: &lt;span class="n"&gt;same&lt;/span&gt;-&lt;span class="n"&gt;origin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Safari doesn't support &lt;code&gt;credentialless&lt;/code&gt;. So Safari falls back to the service worker.&lt;/li&gt;
&lt;li&gt;You can use &lt;code&gt;require-corp&lt;/code&gt; instead, which works in Safari. But it blocks every cross-origin image, script or video that doesn't have a &lt;code&gt;crossorigin&lt;/code&gt; attribute. Be careful with your CDN.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the headers are not there, no problem: Partytown falls back to the service worker automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two rules to remember
&lt;/h2&gt;

&lt;p&gt;Before the code, two rules. They are the same for every framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 1: &lt;code&gt;type="text/partytown"&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The browser doesn't know this type, so it doesn't run the script. Partytown finds it with a selector and runs it in the worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- &amp;lt;script src="https://third-party.com/script.js"&amp;gt;&amp;lt;/script&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ &amp;lt;script type="text/partytown" src="https://third-party.com/script.js"&amp;gt;&amp;lt;/script&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Partytown is &lt;strong&gt;opt-in&lt;/strong&gt;. Only the scripts with this type move. All the others stay where they are. You choose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 2: &lt;code&gt;forward&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your own code still calls &lt;code&gt;dataLayer.push(...)&lt;/code&gt; on the main thread. For example, when a user adds a product to the cart.&lt;/p&gt;

&lt;p&gt;But GTM now lives in the worker. So Partytown needs to know which &lt;code&gt;window&lt;/code&gt; functions to patch and send to the worker. That's the role of &lt;code&gt;forward&lt;/code&gt;. It even queues the calls made before Partytown is ready.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  A["Your code&amp;lt;br/&amp;gt;dataLayer.push(...)"] --&amp;gt;|"patched by forward"| B["Partytown&amp;lt;br/&amp;gt;(main thread side)"]
  B --&amp;gt;|"serialized message"| C["GTM&amp;lt;br/&amp;gt;in the web worker"]
  C --&amp;gt;|"network"| D["Google servers"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;By default, a forwarded call &lt;strong&gt;only&lt;/strong&gt; runs in the worker. If you need the original function to also run on the main thread, use &lt;code&gt;preserveBehavior&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;partytown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dataLayer.push&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preserveBehavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="c1"&gt;// runs on both sides&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fbq&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;                                           &lt;span class="c1"&gt;// worker only (default)&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's have some code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Angular
&lt;/h2&gt;

&lt;p&gt;Our goal: run Google Tag Manager in the worker, and track page views and a click from our Angular app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @qwik.dev/partytown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: serve the Partytown files
&lt;/h3&gt;

&lt;p&gt;The worker and the service worker are real files. They must be served by your app. In &lt;code&gt;angular.json&lt;/code&gt;, add them to the &lt;code&gt;assets&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"assets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"glob"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"**/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node_modules/@qwik.dev/partytown/lib"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/~partytown"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a build, you should see a &lt;code&gt;~partytown/&lt;/code&gt; folder in your output, with &lt;code&gt;partytown.js&lt;/code&gt;, &lt;code&gt;partytown-sw.js&lt;/code&gt;, &lt;code&gt;partytown-atomics.js&lt;/code&gt;...&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: &lt;code&gt;index.html&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Third-party scripts must be there early, before Angular starts. So we put everything in &lt;code&gt;src/index.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;My Shop&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- 1. Partytown configuration: BEFORE partytown.js --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;partytown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dataLayer.push&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- 2. Partytown itself: no async, no defer --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/~partytown/partytown.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- 3. Google Tag Manager, moved to the worker --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/partytown"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gtm.start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gtm.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementsByTagName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="nx"&gt;dl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dataLayer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;l=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.googletagmanager.com/gtm.js?id=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;dl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertBefore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dataLayer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GTM-XXXXXXX&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;app-root&amp;gt;&amp;lt;/app-root&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 While you set things up, use &lt;code&gt;/~partytown/debug/partytown.js&lt;/code&gt; and add &lt;code&gt;debug: true&lt;/code&gt; to the config. You get logs in the console. Don't ship the debug build to production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 4: an analytics service
&lt;/h3&gt;

&lt;p&gt;Now, our Angular code must talk to GTM. We only push to &lt;code&gt;dataLayer&lt;/code&gt;. Thanks to &lt;code&gt;forward&lt;/code&gt;, Partytown sends it to the worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// analytics.service.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DOCUMENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inject&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NavigationEnd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Router&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;takeUntilDestroyed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core/rxjs-interop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Window&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;dataLayer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;providedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AnalyticsService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DOCUMENT&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;defaultView&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// null on the server (SSR)&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// An SPA has no real page load after the first one.&lt;/span&gt;
    &lt;span class="c1"&gt;// So we send a page view on each navigation.&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;NavigationEnd&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;NavigationEnd&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;takeUntilDestroyed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page_view&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;page_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urlAfterRedirects&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// nothing to do on the server&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataLayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataLayer&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="c1"&gt;// This call is patched by Partytown: it's serialized and sent to the worker.&lt;/span&gt;
    &lt;span class="c1"&gt;// GTM does its heavy work there, not on the main thread.&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataLayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it with the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ApplicationConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;provideAppInitializer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;provideRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app.routes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AnalyticsService&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./analytics.service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApplicationConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;provideRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;provideAppInitializer&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AnalyticsService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// create the service, so it listens to the router&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: track a click
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// product-card.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AnalyticsService&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./analytics.service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CartStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./cart.store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Product&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-product-card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;h3&amp;gt;{{ product().name }}&amp;lt;/h3&amp;gt;
    &amp;lt;button (click)="addToCart()"&amp;gt;Add to cart&amp;lt;/button&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductCard&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;analytics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AnalyticsService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CartStore&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;addToCart&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. What the user wants to see: update the UI first&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;product&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Tracking: cheap on the main thread, GTM works in the worker&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;add_to_cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;product&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;product&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the click handler. Without Partytown, &lt;code&gt;dataLayer.push&lt;/code&gt; wakes up GTM &lt;strong&gt;on the main thread&lt;/strong&gt;, in the same frame as your click. With Partytown, the push is sent to the worker, and GTM runs its tags there. Your handler stays small. Your processing duration stays small.&lt;/p&gt;

&lt;h2&gt;
  
  
  React
&lt;/h2&gt;

&lt;p&gt;Same goal, same app: GTM in the worker, page views and a click.&lt;/p&gt;

&lt;p&gt;There are two ways to do it in React:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Option A: &lt;code&gt;index.html&lt;/code&gt;&lt;/strong&gt;, for a client-side app (Vite, Create React App...). The simplest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Option B: the &lt;code&gt;&amp;lt;Partytown /&amp;gt;&lt;/code&gt; component&lt;/strong&gt;, for apps rendered on the server (React Router framework mode, Remix, Gatsby...).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: install and copy the files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @qwik.dev/partytown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Partytown comes with a small CLI to copy its files into your &lt;code&gt;public&lt;/code&gt; folder. Run it before &lt;code&gt;dev&lt;/code&gt; and &lt;code&gt;build&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"partytown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"partytown copylib public/~partytown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run partytown &amp;amp;&amp;amp; vite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run partytown &amp;amp;&amp;amp; vite build"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;public/~partytown&lt;/code&gt; to your &lt;code&gt;.gitignore&lt;/code&gt;. It's generated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: client-side app (Vite)
&lt;/h3&gt;

&lt;p&gt;Everything goes in &lt;code&gt;index.html&lt;/code&gt;, like in Angular:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;My Shop&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;partytown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dataLayer.push&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/~partytown/partytown.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/partytown"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gtm.start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gtm.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementsByTagName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="nx"&gt;dl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dataLayer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;l=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.googletagmanager.com/gtm.js?id=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;dl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertBefore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dataLayer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GTM-XXXXXXX&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"root"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/src/main.tsx"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why not in a component? Because a client-side app renders late. The third-party scripts would wait for React to boot. In &lt;code&gt;index.html&lt;/code&gt;, they're found as soon as Partytown starts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: server-rendered app (the &lt;code&gt;&amp;lt;Partytown /&amp;gt;&lt;/code&gt; component)
&lt;/h3&gt;

&lt;p&gt;The package ships a React component: &lt;code&gt;@qwik.dev/partytown/react&lt;/code&gt;. It's a thin wrapper around the Partytown snippet. The config becomes JSX props. And the snippet is &lt;strong&gt;inlined&lt;/strong&gt; in the HTML, so there's no extra request for &lt;code&gt;partytown.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example with a root layout (React Router framework mode / Remix style):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/root.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Partytown&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@qwik.dev/partytown/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Links&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Outlet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Scripts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GTM_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GTM-XXXXXXX&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gtmSnippet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&amp;amp;l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;GTM_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;');
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;head&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Meta&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Links&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* 1. Partytown: config as props */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Partytown&lt;/span&gt; &lt;span class="na"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dataLayer.push&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* 2. GTM in the worker */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text/partytown"&lt;/span&gt;
          &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gtmSnippet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;head&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Outlet&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Scripts&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 Add &lt;code&gt;debug={true}&lt;/code&gt; on &lt;code&gt;&amp;lt;Partytown /&amp;gt;&lt;/code&gt; while you set things up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Scripts added later
&lt;/h3&gt;

&lt;p&gt;If a &lt;code&gt;text/partytown&lt;/code&gt; script is added to the page &lt;strong&gt;after&lt;/strong&gt; Partytown started (lazy loading, route change...), recent versions of Partytown (0.14.3 and later) find it automatically. On older versions, you have to tell Partytown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/partytown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// set the type BEFORE adding it to the DOM&lt;/span&gt;
&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://third-party.com/widget.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ptupdate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// only needed before 0.14.3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Track page views and clicks
&lt;/h3&gt;

&lt;p&gt;A small module, no library needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// analytics.ts&lt;/span&gt;
&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Window&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;dataLayer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// nothing to do on the server&lt;/span&gt;

  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataLayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataLayer&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="c1"&gt;// Patched by Partytown: serialized and sent to the worker&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataLayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A hook for page views:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// usePageView.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useLocation&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./analytics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;usePageView&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLocation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page_view&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;page_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call it once, in your layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Layout.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Outlet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;usePageView&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./usePageView&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;usePageView&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Outlet&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the click:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ProductCard.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./analytics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useCart&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./useCart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Product&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Product&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;add&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleAddToCart&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. UI first: this is what the user is waiting for&lt;/span&gt;
    &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Tracking: GTM does the heavy work in the worker&lt;/span&gt;
    &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;add_to_cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleAddToCart&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Add to cart&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Using Next.js? Partytown has its own Next.js integration page. Check it, the setup is a bit different.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Check that it works
&lt;/h2&gt;

&lt;p&gt;Don't trust, verify. Three quick checks in Chrome DevTools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Console&lt;/strong&gt;: with the debug build, Partytown logs what runs in the worker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sources panel&lt;/strong&gt;: look at the threads. GTM should run in the Partytown web worker, not in the main one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance panel&lt;/strong&gt;: record a page load and a few clicks. The long tasks from &lt;code&gt;googletagmanager.com&lt;/code&gt; should be gone from the main thread track.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then check in GTM &lt;strong&gt;Preview mode&lt;/strong&gt; that your &lt;code&gt;page_view&lt;/code&gt; and &lt;code&gt;add_to_cart&lt;/code&gt; events arrive. If nothing arrives, check your &lt;code&gt;forward&lt;/code&gt; config first. It's the most common mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where INP really wins
&lt;/h2&gt;

&lt;p&gt;Let's go back to our three phases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;               Input delay     Processing      Presentation
               ───────────     ──────────      ────────────
Partytown:     big impact      some impact     small impact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input delay&lt;/strong&gt;: the big win. Third-party long tasks no longer block the main thread, so the click is handled right away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processing duration&lt;/strong&gt;: the work done by third-party scripts when you push an event (like in &lt;code&gt;addToCart&lt;/code&gt;) happens in the worker, not in your handler's frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presentation delay&lt;/strong&gt;: Partytown batches DOM reads and writes into group updates. Less layout thrashing from third-party code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Trade-offs: no magic here
&lt;/h2&gt;

&lt;p&gt;Partytown is a great tool. It's not a silver bullet. Here is what you need to know before using it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;preventDefault()&lt;/code&gt; has no effect.&lt;/strong&gt; When the worker receives the event, it's already too late. A script that needs to block a link or a form submit won't work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DOM operations are throttled.&lt;/strong&gt; Each call crosses threads. That's fine for analytics, not for scripts that build UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI-heavy scripts are a bad fit.&lt;/strong&gt; A chat widget, a cookie banner with a dialog, anything that injects a lot of DOM nodes: keep them on the main thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORS.&lt;/strong&gt; Some third-party scripts don't send the right CORS headers. You'll need to proxy them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-origin iframes&lt;/strong&gt; created from a Partytown script can't persist cookies or storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bad scripts stay bad.&lt;/strong&gt; A script that reads the whole &lt;code&gt;document&lt;/code&gt; every X milliseconds with &lt;code&gt;setInterval&lt;/code&gt; will overload Partytown.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A quick rule:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Good fit ✅&lt;/th&gt;
&lt;th&gt;Bad fit ❌&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Tag Manager&lt;/td&gt;
&lt;td&gt;Chat widgets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Analytics 4&lt;/td&gt;
&lt;td&gt;Cookie consent dialogs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Facebook Pixel&lt;/td&gt;
&lt;td&gt;Scripts that call &lt;code&gt;preventDefault()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixpanel, Segment, Amplitude&lt;/td&gt;
&lt;td&gt;Scripts that inject a lot of DOM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Measure, don't guess
&lt;/h2&gt;

&lt;p&gt;Never move a script "because it should be better". Measure before, measure after.&lt;/p&gt;

&lt;p&gt;INP is a &lt;strong&gt;field&lt;/strong&gt; metric. Your real users on real devices are the source of truth. Use the &lt;code&gt;web-vitals&lt;/code&gt; library with the attribution build to see which phase is slow. It works the same in React and Angular:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onINP&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;web-vitals/attribution&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;onINP&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attribution&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INP:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Element:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attribution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interactionTarget&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Input delay:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attribution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputDelay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Processing:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attribution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;processingDuration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Presentation:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attribution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;presentationDelay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// send it to your analytics&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put it in &lt;code&gt;main.ts&lt;/code&gt; (Angular) or &lt;code&gt;main.tsx&lt;/code&gt; (React).&lt;/p&gt;

&lt;p&gt;Here's how to read the result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High &lt;strong&gt;input delay&lt;/strong&gt;? Look at what runs before the click. Third-party scripts are the usual suspects. This is where Partytown shines.&lt;/li&gt;
&lt;li&gt;High &lt;strong&gt;processing duration&lt;/strong&gt;? It's probably your handler. Partytown won't help.&lt;/li&gt;
&lt;li&gt;High &lt;strong&gt;presentation delay&lt;/strong&gt;? Look at your DOM size and your layout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the lab, INP depends on what you click. Two tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Interact with the page &lt;strong&gt;while it loads&lt;/strong&gt;. That's when the main thread is the busiest.&lt;/li&gt;
&lt;li&gt;Use Total Blocking Time (TBT) as a proxy. It's not INP, but it tells you if the main thread is crowded.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Partytown won't fix your own code
&lt;/h2&gt;

&lt;p&gt;If your click handler takes 400 ms, moving GTM to a worker changes nothing.&lt;/p&gt;

&lt;p&gt;For your own code, the classic tools still apply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Break long tasks into smaller ones.&lt;/li&gt;
&lt;li&gt;Yield to the main thread (&lt;code&gt;setTimeout&lt;/code&gt;, or &lt;code&gt;scheduler.yield()&lt;/code&gt; where it's supported).&lt;/li&gt;
&lt;li&gt;Show visual feedback &lt;strong&gt;first&lt;/strong&gt;, do the heavy work &lt;strong&gt;after&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Move heavy computation to your own web worker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Partytown handles the code you don't own. You handle the code you own.&lt;/p&gt;

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

&lt;p&gt;INP measures what users feel: "I clicked, did something happen?"&lt;/p&gt;

&lt;p&gt;Third-party scripts are often the hidden reason for a bad INP. They take the main thread, and your users wait.&lt;/p&gt;

&lt;p&gt;Partytown moves them into a web worker, and keeps them working thanks to a clever synchronous bridge (Atomics or a service worker).&lt;/p&gt;

&lt;p&gt;In Angular and React, the setup is small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;serve the Partytown files&lt;/li&gt;
&lt;li&gt;add the config and &lt;code&gt;type="text/partytown"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;forward&lt;/code&gt; the functions your code calls&lt;/li&gt;
&lt;li&gt;keep your tracking code as simple as a &lt;code&gt;dataLayer.push&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not magic. It's in beta, it has trade-offs, and it's not made for UI scripts. But for analytics, tag managers and pixels, it can give the main thread back to the only code that should be there: yours.&lt;/p&gt;

&lt;p&gt;Measure first. Move the right scripts. Measure again. 🎉&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>webperf</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The Grand Unifying Architecture of Frontend</title>
      <dc:creator>Ryan Carniato</dc:creator>
      <pubDate>Tue, 22 Sep 2026 16:43:06 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/the-grand-unifying-architecture-of-frontend-bhk</link>
      <guid>https://dev.to/playfulprogramming/the-grand-unifying-architecture-of-frontend-bhk</guid>
      <description>&lt;p&gt;I used to joke that the history of frontend development could be retraced by following the argument of who owned the state. And the truth is both sides have gone back and forth on this over the years. We started on the document web where state was on the server. Then we added JavaScript and Applets where we could have stateful things in the client. But developers at the time didn't like either much and tried to wire state across both sides in their server languages and we got ASP.NET and the like.&lt;/p&gt;

&lt;p&gt;I sit here over 30 years later and it is still going on. Going to replace your React app with HTMX? Maybe. Maybe it isn't that simple. Although this seems like an endless spiral, I don't think this is unsolvable. Even though it has been going on so long, I actually think the answer is in front of us.&lt;/p&gt;

&lt;p&gt;The unified model comes down to understanding responsibilities. And they are as old as the web itself. Every time we misalign complexity soars. Every time we ignore part of the puzzle it goes on to bite us. Every solution straddles part of this solution space but we've struggled to cover the whole range.&lt;/p&gt;




&lt;h2&gt;
  
  
  Core Responsibilities
&lt;/h2&gt;

&lt;p&gt;My observation is that all frontend architectures come down to layering of three responsibilities. Some are more present than others but they all exist in some form.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Navigation (client)
&lt;/h4&gt;

&lt;p&gt;The URL is the basis of the web experience and it belongs to the browser. Orchestration is a client responsibility and every solution from SPA to HTML partial agrees with this. JavaScript in the browser is the root of our application.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Content (server)
&lt;/h4&gt;

&lt;p&gt;Conversely, content belongs to the server whether it is markup or JSON. The server is the authoritative source. It hangs off of our navigation.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Affordances (client)
&lt;/h4&gt;

&lt;p&gt;Affordances find us back in the browser. Local UI state, in progress work, optimistic updates. We need to provide our user feedback faster than a round-trip to the server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Following the Shape
&lt;/h2&gt;

&lt;p&gt;All web application architectures follow this client -&amp;gt; server -&amp;gt; client layering. A user navigates or performs an action, content comes back from the server, and the client settles it. That's the Single Page Application(SPA) architecture after load, but it's also HTMX. And also LiveView, if the server -&amp;gt; client protocol is powerful enough.&lt;/p&gt;

&lt;p&gt;It feels like I'm stating the obvious here. But the web generally maps writes to navigation, and the server only pushes reads. Mutations travel through layer 1 with forms, actions, invalidation, and requests for the content. Layer 2, content, never writes and only publishes.&lt;/p&gt;

&lt;p&gt;This asymmetry is what makes it composable. Breaking that leads to two stateful systems fighting against themselves. Which is why affordances like optimism(layer 3) fit cleanly on top if viewed as an overlay. The client never writes server content directly as it doesn't own it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Revisiting the Spectrum
&lt;/h2&gt;

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

&lt;p&gt;I've tried many times to map the solution space to a 4 quadrant grid but I never found the right axis. But I'm now seeing how all solutions can sit in the same diagram.&lt;/p&gt;

&lt;p&gt;Obviously this is a rough placement, solutions fit spaces not points, and something like React can be used as a frontend in a sync engine like Zero. Here React represents classic SPAs + JSON API. But most frameworks fix both transport and affordance weight so they don't move much.&lt;/p&gt;

&lt;p&gt;But if you put it under our lens of responsibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;HTMX&lt;/strong&gt;: Minimal layer 1, HTML carries the majority of the work under layer 2, and layer 3 is almost nothing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;LiveView&lt;/strong&gt;: Layer 2 expands its capability, while layer 3 remains nearly non-existent. Same dance with the read window stretched to the length of the session.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DataStar&lt;/strong&gt;: More machinery in layer 2 with the addition of server populated Signals allowing for some closer interaction with the client.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Astro(+ View Transitions)&lt;/strong&gt;: navigation(view transition) -&amp;gt; server(markup) -&amp;gt; client(islands). All 3 parts laid out explicitly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Server Components(Next.js)&lt;/strong&gt;: Like Astro except shared client state is preserved.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SPA + JSON(React)&lt;/strong&gt;: Layer 3 grows until it merges with layer 1. And layer 2 is delegated to JSON APIs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sync Engine(Zero)&lt;/strong&gt;: Layer 2 becomes a persistent read of a replicated store, and layer 3 grows to hold a full local copy of it. Optimism over the whole dataset.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every solution, with a bit of coaxing, is capable of expressing all of these. They differ in transport, window length, and how much affordance code you ship. So it comes down to efficiency and authoring. Which means one framework could cover it all, if each responsibility is adjustable.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Modern Take
&lt;/h2&gt;

&lt;p&gt;Nothing I've laid out is any different than how the base platform has always worked, but can we mirror this in a way that encompasses the full spectrum of the past 30 years?&lt;/p&gt;

&lt;p&gt;I'm going to paint a picture using our 3 responsibilities with Solid 2.0.&lt;/p&gt;




&lt;h3&gt;
  
  
  Navigation: Delegation and progressive enhancement
&lt;/h3&gt;

&lt;p&gt;Solid's router owns nothing in the render tree. There is no &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt;(or &lt;code&gt;&amp;lt;A&amp;gt;&lt;/code&gt;) component. Instead our JSX compiler registers a claim on every &lt;code&gt;a[href]&lt;/code&gt; and &lt;code&gt;form[action]&lt;/code&gt; element it creates, and server-streamed content gets the same claims swept over any subtree the runtime renders.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/users.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;renameUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;updateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// routes/users/[id].tsx — plain HTML. The compiler claims the&lt;/span&gt;
&lt;span class="c1"&gt;// anchor and the form. The router intercepts both. Works before &lt;/span&gt;
&lt;span class="c1"&gt;// hydration and after.&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/a&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;renameUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Rename&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/form&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Note: There are no components in this markup.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A SolidJS router consumes those claims to manage &lt;code&gt;aria-current&lt;/code&gt;, active classes, and interception. This means server-rendered markup with zero client components participates fully in client-side navigation. So it works not only with "No JS" but with JavaScript with no interactive components.&lt;/p&gt;

&lt;p&gt;Forms and buttons delegate to server function actions the same way. You hoist your preloads and invalidation correctly and single-flight mutations mean one round trip settles every piece of UI the mutation invalidated whether that UI is JSON-fed client components or server-owned markup.&lt;/p&gt;




&lt;h3&gt;
  
  
  Content: Reactive reads over the wire
&lt;/h3&gt;

&lt;p&gt;Solid's powerful protocol for layer 2 stands on our &lt;code&gt;"use server"&lt;/code&gt; functions. Solid serializes across the boundary with Seroval, which handles not just plain data but async structures, like Promises and async iterators. The communication between server and client is about the data, including data that hasn't settled yet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// AsyncIterable&amp;lt;string&amp;gt; — crosses the wire as-is&lt;/span&gt;
    &lt;span class="na"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;watchProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// client: the iterable's latest yield is just a reactive read&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;r&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;r&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More than that, the reactive graph propagates over the wire. On the server, every expression feeding the client is an open binding for the life of the response. When a promise settles or iterator yields the server re-runs the expression and sends the new value to the client to be replaced or morphed.&lt;/p&gt;

&lt;p&gt;So far I've showcased this in examples around the original response, ie what happens during hydration. Even if the content chunks stream in as they initially are ready, we can continue to update the stream in place as long as the request stays open.&lt;/p&gt;

&lt;p&gt;But the same can be true of server function requests after the fact. We can send back would-be static markup, "Server Components", and have their reactive graph never ship to the client while still doing these streamed pinpoint updates. We will be announcing the preview for our Server Component feature soon (but if you look you can already see &lt;a href="https://github.com/solidjs/solid/tree/next/examples/hackernews" rel="noopener noreferrer"&gt;fully working&lt;/a&gt; &lt;a href="https://github.com/solidjs/solid/tree/next/examples/notes" rel="noopener noreferrer"&gt;examples&lt;/a&gt; &lt;a href="https://github.com/solidjs/solid/tree/next/examples/chat" rel="noopener noreferrer"&gt;in our repo&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;countdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ticks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// AsyncIterable&amp;lt;number&amp;gt;, one per second&lt;/span&gt;

  &lt;span class="c1"&gt;// returning a function returns a Server Component&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ticks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Counted&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// client — zero component code ships for this&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;countdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Counter&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the communication is one-way with a single authoring model, the transport is pluggable. Today we use Request/Response, so it lives as long as the request is open and you reconnect as needed. Swap in something persistent like SSE or a socket and the same server component becomes a non-terminating live component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// request/response — left side of the grid&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getAuction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// persistent — right side of the grid&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;liveAuction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;live&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// the consumer doesn't care which one it got&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;auction&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createOptimisticStore&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;liveAuction&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What makes this safe is what LiveView's failure mode teaches us: &lt;strong&gt;The server's live graph must be a re-derivable projection of durable state.&lt;/strong&gt; It is never the source of truth. Reconnection doesn't depend on sessions, event replay, or anything that dies with a process.&lt;/p&gt;




&lt;h3&gt;
  
  
  Affordances: An async-aware graph that hydrates once
&lt;/h3&gt;

&lt;p&gt;This is an area SolidJS has excelled in. But in 2.0, async became native to the graph. A value arriving from the server content channel and a value computed locally are indistinguishable to the consumer.&lt;/p&gt;

&lt;p&gt;Optimism is a first-class primitive. It isn't just something bundled with the server functions or query caching library. Optimism represents any uncommitted state predicted over async, whatever the async source. It's a general concept. It doesn't just represent what some might consider "lying to the user", it doesn't have to masquerade as an invisible success. It represents any predictive ephemeral affordance. It either settles eventually as truth or it is reverted, but it never outlives its transaction. It prevents getting out of sync.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;auction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setAuction&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createOptimisticStore&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;liveAuction&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;placeBid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// overlay — reverts on settle&lt;/span&gt;
  &lt;span class="nf"&gt;setAuction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;highBid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// mutation - the write goes up&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;bid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// hold until truth comes back down             &lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;until&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;auction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;highBid&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This all works because we &lt;strong&gt;never hydrate more than once.&lt;/strong&gt; After startup, the server should never render a client component as client state has diverged from anything the server could know. This is a critical naive breaking point in HTML Partial and Islands solutions that share client state. Hydrating after state has updated is a recipe for hydration mismatches. While we can safeguard during the initial streaming progression, that isn't a realistic proposition over the life of an application.&lt;/p&gt;

&lt;p&gt;A rule like that only holds if the system enforces it. Every piece of server content is keyed by the call that produced it, the function and its arguments, the same key a query cache would use. Content arriving on any transport only ever writes to that address's store, and mounted UI pulls from the address it's bound to. There is no direct connection from the network to the DOM. So a hover preload can't clobber the page you're looking at, a refetch morphs in place while the client-owned ranges inside it survive, and a stale response doesn't pass a version check. &lt;/p&gt;

&lt;p&gt;Under this unified model, initial load gets the same treatment without paying serialization twice. Client components claim their server-rendered nodes and boot makes zero requests. The only serialized records are values the client requires. Server-only content ships as HTML or as data, never both. The page markup is the payload. For server components, that means always one copy. View-source and search for any piece of content and you'll find it exactly once.&lt;/p&gt;

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

&lt;p&gt;I once positioned this as "Facing Frontend's Existential Crisis", but now it is a solved problem.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/nzbV0YgSBuo" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Traversing the Spectrum
&lt;/h2&gt;

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

&lt;p&gt;What makes this exciting is with the pieces above you are completely free to move the whole solution space and every point in between.&lt;/p&gt;

&lt;p&gt;If you start in the lower left corner, that would be like a SolidJS app with no client components that just swaps server component chunks on server &lt;code&gt;"use server"&lt;/code&gt; actions bound to &lt;code&gt;&amp;lt;form action&amp;gt;&lt;/code&gt;. Server markup streams and morphs, and navigation is handled without any client components.&lt;/p&gt;

&lt;p&gt;The top left-hand side is when we extend layer 3 until it dominates and you have the SPA and there are no server components or server rendering at all. Just JSON APIs.&lt;/p&gt;

&lt;p&gt;And we can continue to traverse over the top to the right-hand side by the way of persistent &lt;code&gt;"use server"&lt;/code&gt; functions and our granular optimistic layer. Solid's &lt;code&gt;live&lt;/code&gt; and &lt;code&gt;until&lt;/code&gt; operators help close the gap when the client state is streamed in as available. I'd still use a dedicated sync engine, but the base primitives alone do a good impression.&lt;/p&gt;

&lt;p&gt;Finally, the lower right corner, is closer to where we started. Except this time our server components have reactive bindings and their connection is persistent. No client components, just our same "use server" action protocol to write back, while our reads stream fine-grained markup partials over the wire. In this world, even the server components could be optimistic depending on how you write your actions, and &lt;code&gt;until&lt;/code&gt; again gives us a mechanism for completion acknowledgements.&lt;/p&gt;

&lt;p&gt;The incredible part is that this required nothing that users of Solid 2.0 aren't already familiar with. Async Signals and &lt;code&gt;use server&lt;/code&gt;. With that, all these experiences are authorable using basically the same patterns, in the same framework, even in the same application.&lt;/p&gt;




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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fek74ygnjpy4rf4n0gkc3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fek74ygnjpy4rf4n0gkc3.png" alt="A Möbius strip: one surface that appears to have two sides" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It goes back to where we started. A few pieces that have been sitting in front of us this whole time.&lt;/p&gt;

&lt;p&gt;The browser owns navigation and affordances. The server owns content. Communication is a loop with writes travelling as navigation and content travelling as one-way reads. And the transport and the response window should be configurable without changing the shape of authoring.&lt;/p&gt;

&lt;p&gt;Everything from HTMX to LiveView to islands to the SPA is a coordinate on this grid. The differences between them are efficiency and ergonomics rather than expressiveness.&lt;/p&gt;

&lt;p&gt;What makes this more than an exercise in categorization is that all the hard pieces are built. We serialize and hydrate once. The reactive graph serializes its own updates. Content is stored by address, so stale hydration can't be represented.&lt;/p&gt;

&lt;p&gt;It comes as little surprise to me that Signals can act as that common representation on all sides. Sync and async. Server and client. Stateful and stateless.&lt;/p&gt;

&lt;p&gt;This is stupidly ambitious. RSCs only aimed to provide the left half of the grid and many felt they still fell short. The HTML-over-the-wire camp has aimed to provide the bottom half, but ignoring the client is ignoring an entire responsibility.&lt;/p&gt;

&lt;p&gt;And yet, this is a future that I want to live in. One where choosing a framework doesn't require you to choose a religion. And one, from my perspective, that is already here.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>solidjs</category>
      <category>architecture</category>
    </item>
    <item>
      <title>SpellBook of Skill: The Twin Project Nobody Asked For, But I Built Anyway 🧙🏻‍♂️</title>
      <dc:creator>Domenico Tenace</dc:creator>
      <pubDate>Mon, 21 Sep 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/spellbook-of-skill-the-twin-project-nobody-asked-for-but-i-built-anyway-564c</link>
      <guid>https://dev.to/playfulprogramming/spellbook-of-skill-the-twin-project-nobody-asked-for-but-i-built-anyway-564c</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Hey everyone 👋&lt;/p&gt;

&lt;p&gt;A while back I launched Spellbook of Prompt, my curated collection of prompts organized by use case. It's been out there growing steadily, and at some point while adding new entries I kept thinking: prompts are only half the story. Knowing &lt;em&gt;how&lt;/em&gt; to build something matters just as much as knowing what to ask an AI to do.&lt;/p&gt;

&lt;p&gt;So I built the twin project. Meet &lt;a href="https://github.com/Domenico-Tenace-Open-Labs/spellbook-of-skill" rel="noopener noreferrer"&gt;&lt;strong&gt;Spellbook of Skill&lt;/strong&gt;&lt;/a&gt;, a curated collection of development skills, tutorials, and guides to help you level up your craft from fundamentals to advanced techniques.&lt;/p&gt;

&lt;p&gt;Let's dive in! 🤙&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Actually Is 🤔
&lt;/h2&gt;

&lt;p&gt;Where Spellbook of Prompt gives you the incantation, Spellbook of Skill teaches you the magic behind it. It's structured, practical guides organized by category and difficulty, each one built to save time and actually get you somewhere, not just another wall of theory.&lt;/p&gt;

&lt;p&gt;Every guide follows the same shape: a clear learning objective, a working code example, prerequisites listed upfront, and a troubleshooting section for when things inevitably don't work on the first try. The concepts are language and framework agnostic where possible, so what you learn actually transfers instead of being locked to one stack.&lt;/p&gt;

&lt;p&gt;The whole thing runs on Astro with Starlight, same setup as Spellbook of Prompt, so browsing is fast and the docs feel consistent across both projects. It's a genuine sibling project, same philosophy, different content.&lt;/p&gt;




&lt;h2&gt;
  
  
  Built Entirely with OpenCode + OpenRouter + DeepSeek V4 Pro 🛠️
&lt;/h2&gt;

&lt;p&gt;Here's the part that ties back to everything I've been writing about lately. I built Spellbook of Skill end to end using OpenCode wired through OpenRouter, running on DeepSeek V4 Pro 0813.&lt;/p&gt;

&lt;p&gt;No manual scaffolding, no switching to a pricier model halfway through. The project structure, the Astro and Starlight configuration, the content schema, the initial category setup, all of it came out of that same combo I've been using as my daily driver. If you've read my previous pieces on DeepSeek's cost-to-quality ratio or on wiring OpenRouter into OpenCode, this is that setup doing real, shippable work again.&lt;/p&gt;

&lt;p&gt;It's honestly become my proof of concept for "can a budget model stack actually ship a complete open source project." So far the answer keeps being yes.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's In There Right Now 📚
&lt;/h2&gt;

&lt;p&gt;The structure is organized by category, starting with:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content Creation&lt;/strong&gt;, structured prompts and guides for writing clear technical articles, the same kind of workflow I use for these posts.&lt;/p&gt;

&lt;p&gt;More categories are marked as coming soon: coding and development workflows, DevOps and deployment, system design and architecture, design and creative development, career and professional growth.&lt;/p&gt;

&lt;p&gt;It's early. I'd rather be upfront about that than oversell it. The foundation, the docs site, the contribution flow, the conventions, is all solid and ready. The content library is what needs to grow now, and that's exactly where the community comes in.&lt;/p&gt;




&lt;h2&gt;
  
  
  This Is Where You Come In 🤝
&lt;/h2&gt;

&lt;p&gt;Spellbook of Skill is MIT licensed and genuinely open to contributions. Whether you want to add a new guide, improve an existing one, or just fix something that's broken, there's a clear path to do it.&lt;/p&gt;

&lt;p&gt;The process is simple: search existing guides first to avoid duplicates, add or update a guide in the right category folder, include a clear title, description, and at least one complete working example, then open a pull request with a short summary. Guides should stay concise and actionable, list prerequisites and learning objectives, include a troubleshooting section, and obviously never contain real secrets or sensitive data.&lt;/p&gt;

&lt;p&gt;Full details are in the CONTRIBUTING.md file if you want the complete picture before jumping in.&lt;/p&gt;

&lt;p&gt;If you've got a skill you wish someone had written down clearly when you were learning it, that's exactly the kind of guide this project needs. Beginner content is just as valuable as advanced content here.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Out 🚀
&lt;/h2&gt;

&lt;p&gt;The live docs are up at &lt;a href="https://spellbook-of-skill.netlify.app/" rel="noopener noreferrer"&gt;spellbook-of-skill.netlify.app&lt;/a&gt;, or clone it and run it locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Domenico-Tenace-Open-Labs/spellbook-of-skill.git
&lt;span class="nb"&gt;cd &lt;/span&gt;spellbook-of-skill
pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open localhost:4321 and start browsing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts 🌟
&lt;/h2&gt;

&lt;p&gt;Spellbook of Prompt and Spellbook of Skill now sit side by side under Domenico Tenace Open Labs, one teaching you what to ask, the other teaching you how to actually do it. Together they cover more ground than either could alone.&lt;/p&gt;

&lt;p&gt;If you're curious about the project, go star it, and if you've got something worth teaching, open a PR. This is exactly the kind of thing that gets better the more people show up.&lt;/p&gt;

&lt;p&gt;The spellbook grows. 🔥&lt;/p&gt;

&lt;p&gt;Happy coding! ✨&lt;/p&gt;




&lt;p&gt;Hi 👋🏻&lt;br&gt;
My name is Domenico, software developer passionate of Open Source, I write article about it for share my knowledge and experience.&lt;br&gt;
Don't forget to visit my social links to discover my links, check out Domenico Tenace Open Labs for my open source projects and subscribe to my newsletter! 🫰🏻&lt;/p&gt;

&lt;p&gt;🌲 Links: &lt;a href="https://links.domenicotenace.dev/" rel="noopener noreferrer"&gt;https://links.domenicotenace.dev/&lt;/a&gt;&lt;br&gt;
🐙 Domenico Tenace Open Labs: &lt;a href="https://github.com/Domenico-Tenace-Open-Labs" rel="noopener noreferrer"&gt;https://github.com/Domenico-Tenace-Open-Labs&lt;/a&gt;&lt;br&gt;
📰 Newsletter: &lt;a href="https://domenicotenace.substack.com/subscribe" rel="noopener noreferrer"&gt;https://domenicotenace.substack.com/subscribe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you like my content or want to support my work, you can support me with a small donation. I would be grateful 🥹&lt;/p&gt;

&lt;p&gt;☕ Buy me a Coffee: &lt;a href="https://buymeacoffee.com/domenicotenace" rel="noopener noreferrer"&gt;https://buymeacoffee.com/domenicotenace&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>deepseek</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>OpenCode + DeepSeek V4: The Budget Combo That Doesn't Feel Like a Budget Combo 🐋</title>
      <dc:creator>Domenico Tenace</dc:creator>
      <pubDate>Tue, 01 Sep 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/opencode-deepseek-v4-the-budget-combo-that-doesnt-feel-like-a-budget-combo-4kca</link>
      <guid>https://dev.to/playfulprogramming/opencode-deepseek-v4-the-budget-combo-that-doesnt-feel-like-a-budget-combo-4kca</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Hey everyone 👋&lt;/p&gt;

&lt;p&gt;I've spent the last few weeks running most of my daily coding work through OpenCode wired to DeepSeek V4, both Pro and Flash. Not as an experiment, as my actual default setup. And the thing that keeps surprising me is how rarely I miss Claude or GPT for everyday tasks, while paying a fraction of the price.&lt;/p&gt;

&lt;p&gt;I also used DeepSeek exclusively to generate an entire Astro template from scratch, Studio Pulse, which turned out to be a genuinely good test of what these models can actually do unattended.&lt;/p&gt;

&lt;p&gt;Let me walk you through what I found. Let's dive in! 🤙&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Context on DeepSeek V4 🤔
&lt;/h2&gt;

&lt;p&gt;DeepSeek V4 is an open weight, MIT licensed model family that went GA in two waves this year, V4-Flash on July 31 and V4-Pro on August 13. Both are mixture of experts models with a 1M token context window.&lt;/p&gt;

&lt;p&gt;V4-Pro runs 1.6T total parameters with 49B active per token. V4-Flash is leaner, 284B total with 13B active. Both support tool use, function calling, and a thinking mode that effectively replaces the old R1 reasoning line.&lt;/p&gt;

&lt;p&gt;The current API pricing sits at $0.435/$0.87 per million input/output tokens for Pro, and $0.14/$0.28 for Flash. Cache hit input drops even further, down to $0.0036 for Pro and $0.0028 for Flash. For context, Claude Opus output alone costs $25 per million tokens. A dollar buys you roughly 500K off-peak output tokens from V4-Pro, versus 40K from Opus.&lt;/p&gt;

&lt;p&gt;That's not a small gap. That's an order of magnitude.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting It Up with OpenCode 🔧
&lt;/h2&gt;

&lt;p&gt;Since OpenCode is model agnostic, plugging in DeepSeek is just a matter of pointing it at DeepSeek's API endpoint (or routing through OpenRouter, which I also do for some workflows). No special configuration, no compatibility layer, it speaks both OpenAI ChatCompletions and Anthropic API formats natively.&lt;/p&gt;

&lt;p&gt;I keep both models available and switch depending on the task:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DeepSeek V4 Flash&lt;/strong&gt; for quick iterations, small fixes, boilerplate, and anything where speed matters more than depth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DeepSeek V4 Pro&lt;/strong&gt; for anything that needs real reasoning, architecture decisions, or larger refactors.&lt;/p&gt;

&lt;p&gt;Switching between them mid-session in OpenCode takes one command. No restart, no reconfiguration.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Impressed Me 👍
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cost is almost absurd.&lt;/strong&gt; Running a full day of development work, dozens of file edits, multiple refactoring sessions, test generation, through V4-Pro costs me less than a coffee. Through V4-Flash it's closer to pocket change. I stopped thinking about token budgets entirely, which changes how you work. You stop being conservative with your prompts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coding quality holds up.&lt;/strong&gt; On Artificial Analysis benchmarks, V4-Flash scores 69.1 on the coding index, ranking 29th out of nearly 200 models tracked. That's genuinely competitive, not "good for the price," just good. For everyday tasks, boilerplate, CRUD logic, component generation, API routes, I couldn't tell the difference from more expensive models in blind use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 1M token context window is a real advantage.&lt;/strong&gt; Both models default to a million tokens of context. For large codebases or long refactoring sessions where you want the model to see everything at once, this matters more than raw intelligence scores.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool use and function calling are solid.&lt;/strong&gt; Agentic workflows in OpenCode, file edits, shell commands, multi-step tasks, worked reliably. I didn't run into the kind of tool-call confusion I've seen with smaller open models in the past.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache hit pricing rewards good habits.&lt;/strong&gt; If you structure your prompts with static content first and variable content last, DeepSeek's caching drops input costs by roughly 88%. That's a meaningful incentive to write better prompts, and OpenCode's session handling makes this easy to leverage naturally.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where It Falls Short 👎
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Agentic capability trails coding capability.&lt;/strong&gt; DeepSeek themselves and independent reviews are honest about this: the model handles individual coding tasks well, but complex autonomous multi-step processes need tighter task boundaries and more validation than you'd need with Claude Opus or GPT-5.5. Long, loosely defined agent runs are where it's more likely to drift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No image or video input.&lt;/strong&gt; V4 is text and code only. If your workflow involves reviewing screenshots or mockups, you're switching models anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing isn't fully stable.&lt;/strong&gt; DeepSeek has already signaled a broader price increase without publishing new rates yet, and peak/off-peak tiers were introduced in mid-August. The eye-popping pricing I quoted above could shift. Budget for it, but don't assume it's permanent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Less polished on ambiguous instructions.&lt;/strong&gt; When a prompt is vague, DeepSeek tends to make more assumptions than Claude does. Being explicit about what you want pays off more here than with pricier models that are better at inferring intent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reasoning depth on genuinely hard problems.&lt;/strong&gt; For architecture decisions with lots of competing trade-offs, V4-Pro is good but not at the level of the very top tier models. I still occasionally reach for something stronger when the stakes are high enough to justify the cost difference.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Test: Studio Pulse 🚀
&lt;/h2&gt;

&lt;p&gt;To really push this setup, I built &lt;a href="https://github.com/DomeT99/studio-pulse" rel="noopener noreferrer"&gt;Studio Pulse&lt;/a&gt; entirely through DeepSeek, no manual coding, no assist from another model.&lt;/p&gt;

&lt;p&gt;It's a template for a full-service communication agency marketing site: multi-page structure with home, about, blog, and contact, dark and light mode with OS preference detection and persistence, SPA-style page transitions via Astro's ClientRouter, a blog powered by content collections with typed frontmatter, and accessibility handled properly, skip links, semantic landmarks, focus-visible outlines, reduced-motion support.&lt;/p&gt;

&lt;p&gt;Stack is Astro 7 with Bulma 1.0.4, statically generated, dependency-light, pnpm as the package manager. Clean project structure, typed content schema, CSS custom properties for theming.&lt;/p&gt;

&lt;p&gt;What stood out building it: DeepSeek handled the Astro-specific patterns (content collections, the loaders API, client router transitions) correctly on the first or second pass most of the time. The accessibility details weren't an afterthought either, I asked for them once early in the process and the model kept applying them consistently across new pages without being reminded.&lt;/p&gt;

&lt;p&gt;It's live at studio-pulse-demo.netlify.app, MIT licensed, and available as a GitHub template if you want to spin up something similar.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who Should Try This Combo 🎯
&lt;/h2&gt;

&lt;p&gt;If you're cost-conscious and building things where "good enough" coding quality is genuinely enough, which is most day-to-day development, this setup is hard to beat. Side projects, templates, internal tools, prototypes, all of it fits well within DeepSeek's strengths.&lt;/p&gt;

&lt;p&gt;If you're running agent pipelines at scale where token costs actually matter to your bottom line, the math here is compelling enough to at least test against your current model.&lt;/p&gt;

&lt;p&gt;Where I'd still reach for something else: safety-critical code, deeply ambiguous architecture decisions, or long autonomous agent runs with minimal supervision. That's where the extra cost of a stronger model still earns its keep.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts 🌟
&lt;/h2&gt;

&lt;p&gt;I didn't expect to make DeepSeek V4 my daily driver, but that's what's happened. Between OpenCode's flexibility and DeepSeek's pricing, the barrier to just trying things dropped to nearly zero, and that changed how much I experiment.&lt;/p&gt;

&lt;p&gt;Studio Pulse is proof that a fully model-generated project doesn't have to feel like a rough draft. It's not a toy, it's an actual usable template with real attention to detail.&lt;/p&gt;

&lt;p&gt;Is DeepSeek the best model on the market right now? No. Is it the best model per dollar? By a wide margin, yes. And for most of what I build day to day, that's the metric that actually matters.&lt;/p&gt;

&lt;p&gt;Happy coding! ✨&lt;/p&gt;




&lt;p&gt;Hi 👋🏻&lt;br&gt;
My name is Domenico, software developer passionate of Open Source, I write article about it for share my knowledge and experience.&lt;br&gt;
Don't forget to visit my social links to discover my links, check out Domenico Tenace Open Labs for my open source projects and subscribe to my newsletter! 🫰🏻&lt;/p&gt;

&lt;p&gt;🌲 Links: &lt;a href="https://links.domenicotenace.dev/" rel="noopener noreferrer"&gt;https://links.domenicotenace.dev/&lt;/a&gt;&lt;br&gt;
🐙 Domenico Tenace Open Labs: &lt;a href="https://github.com/Domenico-Tenace-Open-Labs" rel="noopener noreferrer"&gt;https://github.com/Domenico-Tenace-Open-Labs&lt;/a&gt;&lt;br&gt;
📰 Newsletter: &lt;a href="https://domenicotenace.substack.com/subscribe" rel="noopener noreferrer"&gt;https://domenicotenace.substack.com/subscribe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you like my content or want to support my work, you can support me with a small donation. I would be grateful 🥹&lt;/p&gt;

&lt;p&gt;☕ Buy me a Coffee: &lt;a href="https://buymeacoffee.com/domenicotenace" rel="noopener noreferrer"&gt;https://buymeacoffee.com/domenicotenace&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>deepseek</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Nobody Argued For Your Stack</title>
      <dc:creator>Ryan Carniato</dc:creator>
      <pubDate>Thu, 27 Aug 2026 18:34:44 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/nobody-argued-for-your-stack-51fj</link>
      <guid>https://dev.to/playfulprogramming/nobody-argued-for-your-stack-51fj</guid>
      <description>&lt;p&gt;Last week, it came to light &lt;a href="https://x.com/poteto/status/2089227731305464150" rel="noopener noreferrer"&gt;Cursor had mostly finished migrating from SolidJS to React&lt;/a&gt;. This migration happened about seven months ago. But it became a central focus of discussion following the &lt;a href="https://www.solidjs.com/blog/solid-2-0-rc-the-big-reveal" rel="noopener noreferrer"&gt;Solid 2.0 RC release&lt;/a&gt;. Then yesterday, a week later, it came to my attention that the Anthropic docs example command for their large-scale migration feature is:&lt;/p&gt;

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

&lt;p&gt;I admit that my &lt;a href="https://x.com/RyanCarniato/status/2092631951530410280" rel="noopener noreferrer"&gt;gut reaction&lt;/a&gt; was not great. Out of all the examples they could have chosen... Years of my work became a canonical example of the thing you migrate away from — in the same week we shipped the biggest release in the project's history — stung in a way I won't pretend it didn't. My second reaction was to assume that, like the &lt;a href="https://bytes.dev/archives/513" rel="noopener noreferrer"&gt;other trickle-down posts&lt;/a&gt; I'd seen this week, this rode the same week-old news cycle.&lt;/p&gt;

&lt;p&gt;Then I checked the Internet Archive and realized this has been there &lt;a href="https://web.archive.org/web/20260412131455/https://support.claude.com/en/articles/14554000-claude-code-power-user-tips" rel="noopener noreferrer"&gt;since at least April 2026&lt;/a&gt;. Four months before the Cursor story broke.  At this point, the whole public footprint was a mention of an experiment sandwiched between bigger updates in a &lt;a href="https://cursor.com/blog/scaling-agents" rel="noopener noreferrer"&gt;Cursor blog post&lt;/a&gt; posted in January. The kind of thing that no one outside the industry would even really pick up on. No reasoning, no benchmarks, no argument.&lt;/p&gt;

&lt;p&gt;Stop to think about what that means. I should be careful here because I can't prove anyone at Anthropic ever read that Cursor post. Nobody can. Maybe a docs writer saw the experiment. Maybe Claude drafted its own example. But think it through. Either it traveled from a buried line in one company's release notes into another company's official docs, or it needed no origin at all. It was already assumed before any public migration existed.&lt;/p&gt;

&lt;p&gt;Our industry has quietly started broadcasting conclusions where it used to transmit arguments. We couldn't have picked a worse time, because — as I'll get to — arguments are the only source that still matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters More Than It Used To
&lt;/h2&gt;

&lt;p&gt;It would be fair to ask, hasn't it always been like this? Teams cargo cult large players. Netflix or Facebook uses this predates AI by decades. Optics and politics have always beaten pure technical merit. The weight of Facebook definitely helped React's early propagation.&lt;/p&gt;

&lt;p&gt;But a narrative used to come with friction. It was always in the race, but it couldn't outrun the argument. This has changed for two reasons:&lt;/p&gt;

&lt;p&gt;First, execution cost has collapsed. &lt;a href="https://bun.com/blog/bun-in-rust" rel="noopener noreferrer"&gt;Bun's rewrite from Zig to Rust&lt;/a&gt; — about a million lines — was executed almost entirely by Claude agents in days, not months. And before you start thinking "lock-in", going the opposite way, while not as easy, has never been easier. Migrating from Rust to Zig or React to Solid has never been easier.&lt;/p&gt;

&lt;p&gt;That sounds like good news. And it is. When migrations took years of human effort, cost put a damper on fashion. Now execution is a lot cheaper in all directions. The only thing left is the reason. Which suggests, on the surface, these decisions should be rooted in technical merit. But when narrative is what sets the direction, you start to see how a line buried in documentation is worth an essay. &lt;/p&gt;

&lt;p&gt;Second, verdict production has been industrialized. &lt;a href="https://x.com/jaredpalmer/status/2089463862085251372" rel="noopener noreferrer"&gt;Cognition just migrated its marketing site from Astro to Next.js&lt;/a&gt;. A content site. Astro's home turf, by near-universal consensus, including from people with no stake in the fight. The migration was performed by Devin itself and published as a case study: the agent made the changes, tested them, recorded its own verification runs. No repository, no before-and-after numbers, no benchmark. The output wasn't the site. It was the story.&lt;/p&gt;

&lt;p&gt;And believe me, there is no shortage of these stories. Migrations are now the demo genre of the agent industry. &lt;a href="https://x.com/theo" rel="noopener noreferrer"&gt;Theo Browne&lt;/a&gt; was trying to tell me this a couple weeks back with some very good advice. But it hits a lot harder when you feel it firsthand. Migrations that demo flawlessly are, by definition, the ones moving toward what the agent writes best today. Marketing departments are generating "X → dominant library" verdicts at a rate organic engineering decisions never would.&lt;/p&gt;

&lt;p&gt;So you see the tension. Stacks have never been more sensitive to circulating verdicts, and verdicts have never been produced faster or with less reasoning attached.&lt;/p&gt;




&lt;h2&gt;
  
  
  What an Argument Looks Like
&lt;/h2&gt;

&lt;p&gt;These aren't unheard of. They are just becoming rarer.&lt;/p&gt;

&lt;p&gt;When &lt;a href="https://bun.com/blog/bun-in-rust" rel="noopener noreferrer"&gt;Bun moved from Zig to Rust&lt;/a&gt;, Jarred Sumner made the case in public, with receipts. He talked about bug classes and memory management issues. You can disagree with the argument. &lt;a href="https://andrewkelley.me/post/my-thoughts-bun-rust-rewrite.html" rel="noopener noreferrer"&gt;People have&lt;/a&gt;, point by point, in public. That's the value. An argument can be engaged, checked, narrowed, refuted, or strengthened. A verdict can only be repeated.&lt;/p&gt;

&lt;p&gt;Or look at TanStack's journey with React Server Components on tanstack.com. They spent the year almost arguing with themselves. When &lt;a href="https://tanstack.com/blog/react-server-components" rel="noopener noreferrer"&gt;tanstack.com adopted React Server Components&lt;/a&gt;, they wrote it up and measured it. When they &lt;a href="https://tanstack.com/blog/we-stopped-using-rsc-on-tanstack-com" rel="noopener noreferrer"&gt;stopped using them&lt;/a&gt;, they did the same: What changed, what replaced it, why the tradeoff flipped for their specific case — while TanStack Start went right on supporting RSC as an opt-in primitive for everyone else. The reversal post calling the decision "uneventful" is a tell. A public reversal on React's flagship architecture, and it generated insight instead of heat. Because they delivered analysis instead of verdict.&lt;/p&gt;

&lt;p&gt;That's what evaluation entering the public record looks like. jQuery didn't lose to React because a training set said so. It lost an argument, in public, on merit over several years. Every library you respect got where it is by winning that kind of fight. The flow of events I described earlier doesn't hold fights. It holds direction, and compounds on it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Argument Nobody Published
&lt;/h2&gt;

&lt;p&gt;In all fairness, Cursor never issued a verdict. An experiment that "still needs careful review", then &lt;a href="https://x.com/poteto/status/2089227731305464150" rel="noopener noreferrer"&gt;a congratulatory post on the Solid 2.0 RC release&lt;/a&gt;. But that post still characterized signals as "perf footguns" and "accidental fan out" — no numbers, nothing to engage — and reached straight for the React Compiler as the cure. No bad faith required. The diagnosis and the cure just came from the same place.&lt;/p&gt;

&lt;p&gt;When I tried to reconstruct the case Cursor could have made, it turned out to be interesting. Far more interesting than "perf footguns."&lt;/p&gt;

&lt;p&gt;The post obviously highlighted agents were "quite bad at writing good Solid code," everything "ended up being accidentally tracked." While my "Solid-brain" struggles a bit to imagine what they were doing to get there, I don't doubt it for a second. An agent that has internalized React's mental model will write Solid like React, and Solid written like React is bad Solid. I've also seen the reverse. The difference is when the agent fumbles React, the training data catches it. When it fumbles Solid, the training data shrugs. This is a pure numbers game.&lt;/p&gt;

&lt;p&gt;But &lt;a href="https://x.com/poteto/status/2089227731305464150" rel="noopener noreferrer"&gt;the same post&lt;/a&gt; contains a decision in the opposite direction. They're also moving from Tailwind to StyleX. This is a migration against volume. What StyleX offers instead is that agent mistakes get caught. Styles are typed with deterministic merge order, so a hallucinated class is a build error.&lt;/p&gt;

&lt;p&gt;Put side by side, one conclusion can be made: make agents' mistakes cheap. Two ways to achieve that. Volume and verification. React (with its compiler) wins on volume and arguably on verification. StyleX loses on volume and wins decisively on verification. I won't pretend under this lens with Solid 1.0 this is a particularly comfortable place for signal-based reactivity. But it's also, not coincidentally, where Solid 2.0's design attention has gone.&lt;/p&gt;

&lt;p&gt;That analysis, volume and verification as two axes, is concrete, checkable, and useful. It implies testable claims and gives every framework author a design target. It might also be wrong. I reconstructed it from a benchmark-free post and my own inferences. Cursor's actual reasoning may be entirely different.&lt;/p&gt;

&lt;p&gt;The StyleX decision is even more interesting. Agent-driven development can move against volume when verification wins. There is a second angle that small technologies can embrace. To me, this isn't a new revelation, but one where practice confirms theory.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Happens If Nothing Changes
&lt;/h2&gt;

&lt;p&gt;If you follow this thinking to its logical conclusion, outside of a few exceptions, each layer collapses on its most popular solution today. Frontend converges on React. Systems converges on Rust. Scripting converges on Python. Even the sites that are just pages converge on Next, fit be damned. That's a monoculture, arrived at not by anyone deciding libraries on their merits, but by a system that can no longer distinguish "abundant in the training data" from "better."&lt;/p&gt;

&lt;p&gt;If you are a React fan, you should still be concerned. React was good because it had competition. Hooks arrived in a world where composition patterns were being explored. The React Compiler exists, in part, as an answer to the question signals-based frameworks spent years forcing: Why should the developer pay for re-rendering the world? React's greatest strength has always been its ability to metabolize pressure from outside. A monoculture doesn't just kill the alternatives. It kills the pressure. An ecosystem without pressure doesn't stay good. It stays stuck.&lt;/p&gt;

&lt;p&gt;There is a path back. Teams publish analysis, not verdict. Model vendors evaluate what they ship in public. Libraries learn from StyleX's example and bank on verification. And the rest of us learn to distinguish output from argument before we amplify it. The last one we've been struggling with since the dawn of social media.&lt;/p&gt;

&lt;p&gt;In the meantime, we can keep generating our rules files, llms.txt, docs over MCP, curated examples. We do all of it for Solid 2.0, and it helps a lot. But context is borrowed, not owned. It has to be injected into every session, by every tool, forever, while the inertia works against you for free.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Does the Next Idea Come From?
&lt;/h2&gt;

&lt;p&gt;I want to leave you with a more probing thought. The question this all comes down to isn't whether Solid survives. We're fine. We are shipping the biggest release in our history, with a community that chose this framework on purpose and, more importantly, knows why.&lt;/p&gt;

&lt;p&gt;The question is where the next idea comes from.&lt;/p&gt;

&lt;p&gt;Every paradigm that has mattered started as something the establishment couldn't write. React was mocked for almost a year over JSX. Markup in JavaScript? Are you insane? What about separation of concerns? If today's machinery had existed in 2013, models trained on a web that was all jQuery and Backbone, docs canonizing migrations everyone was already making, verdicts circulating months ahead of their arguments, do you think it could have gotten past that stage?&lt;/p&gt;

&lt;p&gt;It didn't win because the defaults favored it. It won because people made a verifiable case in public. And I don't think the mechanisms are gone. Migrations have never been cheaper, in every direction. The door out of any monoculture has never been more open. We just need to keep analysis as part of the conversation even as we distance ourselves from the implementation. That's being informed. That's taking responsibility.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>react</category>
      <category>programming</category>
    </item>
    <item>
      <title>My AI Content Journey</title>
      <dc:creator>Ryan Carniato</dc:creator>
      <pubDate>Wed, 19 Aug 2026 16:47:10 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/my-ai-content-journey-1b8p</link>
      <guid>https://dev.to/playfulprogramming/my-ai-content-journey-1b8p</guid>
      <description>&lt;p&gt;&lt;em&gt;I apologize ahead of time, what follows has no tooling applied to it. No grammar checks, no AI, and no edit passes for flow. It might be a bit rough, but that's important.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For those who know me, I've written hundreds of articles over the last 8 years. I realized very early into my journey of creating SolidJS that education was going to be a fundamental barrier. React's rhetoric had consumed the frontend space in such way that the discussion of different ideas was not welcomed warmly. It may have started with the ideas, but the complexity of the move to modern tooling had left many feeling overwhelmed. The term "JavaScript Fatigue" was being thrown around, and those who didn't sneak in before  2016 were subject to it.&lt;/p&gt;

&lt;p&gt;My early writing was rough. I mean really rough. I'd try to articulate my ideas, but they were hard to translate. It wasn't that I didn't have decent command of the English language. I always had done well in school. But the ideas I wanted to articulate extended past code examples. They borrowed from years of observations and cross discipline learning. It would take the span of a whole article for me to get my point across and it was really easy to get lost in the details.&lt;/p&gt;

&lt;p&gt;It didn't help that I wrote like I talked. Which is rather disjointed. When I used to type messages on ICQ and MSN back in the day I'd keep thumbing the "." as a way to convey to the recipient the pacing. The more "..." the longer the pause. I had needed to communicate at work, obviously and had gotten used to rewriting my emails on average three times so that I could cut out things that were unnecessary.&lt;/p&gt;

&lt;p&gt;So I published my earlier articles on Medium in 2018 and fortunately for me, the early Solid community on Gitter and Spectrum started providing me edits. This pattern of reviewing my articles with the community never went away. Most of my articles today still appear in the Solid Discord days before publishing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stepping up my Game
&lt;/h2&gt;

&lt;p&gt;At some point, a community member suggested I pick up Grammarly. Honestly, it was such a huge help. I always skipped words and commas in certain places. Given I had the free tier it would also hint when I phrased something a lot oddly so I could at least give a shot at rewriting it.&lt;/p&gt;

&lt;p&gt;This was better but honestly it fixed less than you would think. I wasn't falling into the simple traps. But the writing wasn't really anymore clear. The big break through there was when I started getting my work published through online publications. Specifically, "Angular in Depth" in 2019 time period. They were really supportive of my content but they saw that it was missing the audience.&lt;/p&gt;

&lt;p&gt;I remember at one point &lt;a class="mentioned-user" href="https://dev.to/layzee"&gt;@layzee&lt;/a&gt; sat down with me and picked through an article with me showing me how to break my writing up into more paragraphs. I was accustomed to having a paragraph express what I considered a complete idea. That meant 6-8 run-on sentences. He was like no more than 3. Give them a break to sit with what you just told them. Add spacing and images to your articles for the same reason.&lt;/p&gt;

&lt;p&gt;I didn't like this much at first because it made my articles longer. Too long as far as I thought. But it also forced me to really focus on what I wanted to say. I'd need to do one editing pass to break things up and another to eliminate the cruft.&lt;/p&gt;

&lt;p&gt;At this point, Solid was picking up steam, and there were others in the community that were interested in seeing us succeed. Some of you may not know but Theo Browne (T3.gg) reviewed many of my articles during the 2020-2022 time period. He had a lot less tolerance for sentences that didn't flow and he would go through and gut all my unnecessary phrasing. All the "And ...", "In fact", "But Honestly", "Actually..", "Simply put,". He emphasized it didn't add anything. By doing that I was able to save those for when I wanted to make a real impact.&lt;/p&gt;

&lt;p&gt;So that gives you some insight into where my writing process was through the end of 2025. I'd write something rough, do multiple passes myself, run it through Grammarly, feed it to the Solid community. Have feedback, then pass it to a few specific reviewers (although some like Theo were getting too busy to look at my work regularly). And half a dozen people and twice as many hours later we had an article.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enter LLMs
&lt;/h2&gt;

&lt;p&gt;Now I never prompted an LLM intentionally before the end of 2025. As I have noted in previous articles, I was an AI laggard. Sure Gemini results would show up on a Google search. And I had dabbled with some image generation for Articles/Streams. But I did not use Chat LLMs directly at all. Funny enough, since late 2023 I had noticed two phenomena.&lt;/p&gt;

&lt;p&gt;First, absolute garbage articles quoting my work in like "Best JavaScript Frameworks to use in 2023". At first I thought these were just low effort marketing articles. Some of them probably were. But after a while I came to recognize these as AI generated.&lt;/p&gt;

&lt;p&gt;Second, people coming on to my articles and accusing them as being AI generated. My response would be, "Did you read the article?" There is no way AI makes that argument. But the general feedback was that the articles were vague because they tried to pack too much into a small space. And the move between macro and micro details was disorienting.  Ironically my trademark writing style because it was born out of necessity.&lt;/p&gt;

&lt;p&gt;I mostly pushed both of these aside. What had always mattered was the content. Those two couldn't be more obviously different. Even if I did my own yearly JavaScript summaries, any one with half a brain could tell the difference. My articles had takes that existed nowhere else and provided a deep insight.&lt;/p&gt;




&lt;h2&gt;
  
  
  Trying LLMs for Myself
&lt;/h2&gt;

&lt;p&gt;I was working on Solid 2.0. High-level design stuff late 2025 and I decided to give LLMs a try. Not for coding just to talk through the problem and see if they had an insight. I believe GPT 5.3 was out but I was just using CoPilot for free which was on 5.1. I found the process very frustrating because it kept losing details. I realized pretty quickly I needed to have it write things down.&lt;/p&gt;

&lt;p&gt;As I got more excited about formalizing these designs I got AI to write short explainers about what we had talked about so that I could share them in Discord. This was very helpful since my HackMDs I had been writing were cryptic and people could immediately understand what I was talking about this way. Maybe not deeply but the gist of it.&lt;/p&gt;

&lt;p&gt;Summaries of really high level stuff. The thing I was particularly bad at writing. It was very common in the Solid community for people to post AI written summaries on my writing since no one seemed to get what I was saying and I didn't have time to clarify while working on cutting edge things.&lt;/p&gt;

&lt;p&gt;But every time I tried to get AI to really capture the real point behind my stuff it missed. Even after moving to Cursor and frontier models. AI could write something coherent but it was never the position or argument the way I'd make it. As before it didn't have my original positioning/takes. Occasionally it would come up with something interesting like: &lt;a href="https://hackmd.io/@0u1u3zEAQAO0iYWVAStEvw/rJM9ws3Kbg" rel="noopener noreferrer"&gt;The Architecture of Temporal Determinism&lt;/a&gt;. But that was a rare occurence.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI Assisted Writing
&lt;/h2&gt;

&lt;p&gt;So at some point, tired of the Yellow squigglies I was getting from Grammarly--the ones that you need to pay for that refer to structural issues--I was like why don't I feed my article to the LLM instead? And I was amazed.&lt;/p&gt;

&lt;p&gt;It took what I wrote and it sounded coherent. No initial 3 passes, no Grammarly, no community pass, no special editors. Yes I still ended up doing all of those but it was like it skipped to the finish line. There was very little feedback down the line.&lt;/p&gt;

&lt;p&gt;The part that really loved was it simultaneously made my writing shorter while bringing back that pacing I heard in my head. The old pauses. The way it used semi-colons and &lt;code&gt;--&lt;/code&gt; was exactly what I intended. No broken sentences. No confusion on flow. It just read perfectly.&lt;/p&gt;

&lt;p&gt;I was so impressed. There were a couple things I thought odd. It really liked sumarizing at the end of each section. I had a habit of connecting sections with a probing sentence for sure, but AI liked to try to tie things in a bow. And those additions were the place that I immediately cut out. Easy to see. But for the most part it was like what I had always been looking for. I'd struggled being terse and clear and it just gave it to me, in mostly my own words.&lt;/p&gt;

&lt;p&gt;I released my first article like this:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/playfulprogramming/two-react-design-choices-developers-dont-like-but-cant-avoid-d6g" class="crayons-story__hidden-navigation-link"&gt;Two React Design Choices Developers Don’t Like—But Can’t Avoid&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/playfulprogramming"&gt;
            &lt;img alt="Playful Programming logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3314%2Ffd92caab-2014-431e-a19e-8ab47f2bf5ab.png" class="crayons-logo__image" width="800" height="800"&gt;
          &lt;/a&gt;

          &lt;a href="/ryansolid" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F186199%2Fa3d1cfed-a1ca-41cd-a146-9db4e65711d4.jpeg" alt="ryansolid profile" class="crayons-avatar__image" width="460" height="460"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/ryansolid" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Ryan Carniato
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Ryan Carniato
                
                
              
              &lt;div id="story-author-preview-content-3344810" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/ryansolid" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F186199%2Fa3d1cfed-a1ca-41cd-a146-9db4e65711d4.jpeg" class="crayons-avatar__image" alt="" width="460" height="460"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Ryan Carniato&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/playfulprogramming" class="crayons-story__secondary fw-medium"&gt;Playful Programming&lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/playfulprogramming/two-react-design-choices-developers-dont-like-but-cant-avoid-d6g" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 13&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/playfulprogramming/two-react-design-choices-developers-dont-like-but-cant-avoid-d6g" id="article-link-3344810"&gt;
          Two React Design Choices Developers Don’t Like—But Can’t Avoid
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/solidjs"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;solidjs&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/react"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;react&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/playfulprogramming/two-react-design-choices-developers-dont-like-but-cant-avoid-d6g" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;94&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/playfulprogramming/two-react-design-choices-developers-dont-like-but-cant-avoid-d6g#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              42&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            9 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;and I was pretty excited. This was an important article. It was where I was going to do the unexpected: express to the world that React, a long time competitor philosophically, was actually completely right about a few things. It was controversial. It was honest. And it needed to be clear more than anything I'd done before.&lt;/p&gt;

&lt;p&gt;In hindsight, the response should have been obvious. People couldn't get over the AI phrasings in places. That's the thing. It only takes a couple of places for the radar to go off. I had the before and after AI for this article I actually showed it on stream. I wrote every bit of the source. The things that people were accusing being AI were as much me. I thought that by taking accountability, owning it would be enough. &lt;/p&gt;

&lt;p&gt;But this kicked up a rage. And I admit I felt a bit resentful. This is critical work. From my perspective earthshaking and all people could seem to talk about was that I used AI to make the message clearer. I had been dismissed in the past for being vague or hard to follow. Now I was being dismissed for the opposite.&lt;/p&gt;

&lt;p&gt;I had also received similar feedback on using AI-generated covers. Previously, I was picking up random free images online. It took some cleverness to choose the image but it wasn't some work that I put in. AI had actually given me images suitable to my content. Again it seemed so much better to me. I'm not an artist. But somehow it wasn't acceptable. It seemed irrational to me. I put more effort in, to come up with a better result. I was clearly missing something. &lt;/p&gt;




&lt;h2&gt;
  
  
  Another Attempt
&lt;/h2&gt;

&lt;p&gt;Last week, I took that lesson and tried something a little different when we dropped the &lt;a href="https://www.solidjs.com/blog/solid-2-0-rc-the-big-reveal" rel="noopener noreferrer"&gt;Solid 2.0 RC release announcement&lt;/a&gt;. I had written release articles before but they were always tough. Short attention spans and a need to get to the point. And Solid 2.0 was proving difficult. It just had too much stuff in it. A point form list undersold it but there was no way I was going to be able to get into all the details.&lt;/p&gt;

&lt;p&gt;I had written an article showed it to AI and it had done some edits. And this time I was more careful to cleanup language. Not because I thought it was bad but because I knew it would trigger people. However, no matter what I did the article was way too long. At some point the agent suggested the obvious. Make a follow-up article series. It would summarize the some of the pieces we would be removing. And we would be set.&lt;/p&gt;

&lt;p&gt;I looked at the work and I was impressed it had kept my opening and sentimental parts and filled in the gap with the type of clear, to-the-point way that I'd expect in a release article. It was way shorter than what I had written and after a quick pass, everyone who reviewed agreed it was much more approachable. The language here was not nearly as AI-sounding because I didn't get it to really rewrite what I had written where it mattered. But obviously, even after human edits on the more marketing side there were still some traits.&lt;/p&gt;

&lt;p&gt;Again, and I imagine it was because people didn't get to the end. The response was half about the use of AI in those point form lists. The general sentiment was that it was subtle but still enough to notice. It was genuinely a better article for it, it fit the purpose of the writing. But they couldn't see past it. Most of the article had the human soul, but a few point form lists ruined it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Today
&lt;/h2&gt;

&lt;p&gt;I had an odd week. It started with seeing an &lt;a href="https://gist.github.com/aleclarson/0f4266d63fd83c7a5ea5512441bbea0c" rel="noopener noreferrer"&gt;AI Gist comparing Solid 2.0 to Octane&lt;/a&gt;. I saw that Dominic had responded with some agent-assisted prose, and I knew there was no way I was going to keep up with that discussion while pushing forward all the post-release things I needed to do without doing the same. We literally had the Agents arguing our points. You can tell they are our points. Our fundamental positioning, but it isn't our voice.&lt;/p&gt;

&lt;p&gt;But the most warmly genuine thing happens at the end. We both remove the Agent and have a real discussion. I think we both realized how ridiculous it had become.&lt;/p&gt;

&lt;p&gt;Now, the problem is that it isn't too far from how we are working today. When I have an agent review and issue or PR even if I'm driving it, reviewing the changes, and setting the response, it no longer sounds like me. It's easy to justify the reasoning.&lt;/p&gt;

&lt;p&gt;Look at how many open issues/PRs are on &lt;a href="https://github.com/solidjs/solid" rel="noopener noreferrer"&gt;SolidJS&lt;/a&gt; right now. There is no way we could be as responsive without it. We've almost closed out every issue. That is between issue submitters using agent-guided PRs and from our own agent work. But a certain human aspect is gone. I used to go on every PR and write a thank you note personally and I know I haven't done that as much the last couple months. I can justify it, but there is a human cost.&lt;/p&gt;

&lt;p&gt;I'm also about to release another article. I'm trying yet another approach to this. Where I write it, feed it to AI, but never let AI edit the actual article. When it identifies something that it rewrites, I take that as a sign my writing isn't the best there and read what the AI wrote, then minimize the window and write back the structure myself using my own voice. We'll see if this lands better.&lt;/p&gt;

&lt;p&gt;It's tricky because I can already hear LLMism in my own writing. According to critics, I was already pretty close years ago. And if I already think it sounds better than what I write I have a bias. But it's the most honest thing I can do to produce good content. And I mean good content. Not whatever this is. So I'm going to try again. I'm still figuring this out.&lt;/p&gt;

&lt;p&gt;That's all we can keep doing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>devjournal</category>
    </item>
  </channel>
</rss>
