<?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>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>
    <item>
      <title>Functional languages are heavily imperative.</title>
      <dc:creator>Mike Pearson</dc:creator>
      <pubDate>Mon, 17 Aug 2026 07:39:18 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/functional-languages-are-heavily-imperative-4c1</link>
      <guid>https://dev.to/playfulprogramming/functional-languages-are-heavily-imperative-4c1</guid>
      <description>&lt;p&gt;This article will seem full of contradictions.&lt;/p&gt;

&lt;p&gt;Even writing it feels like a contradiction: Why am I digging through academic Haskell papers from the '90s right in the middle of my AI psychosis moment? Literally, as I write this, I am running my first experiment with an agent delegating tasks to another agent. So why am I spending time on esoteric academic nonsense instead of giving in to the vibes and building my loopy graphy software factory?&lt;/p&gt;

&lt;p&gt;I am writing this because AI is a nuclear amplifier on the patterns we establish in our codebases, and we literally do not have the right terminology to establish scalable patterns. Nobody has the answers we need because nobody even has the right terminology to talk about it. &lt;/p&gt;

&lt;h2&gt;
  
  
  Two Dimensions, Not One
&lt;/h2&gt;

&lt;p&gt;Industry dogma says that functional and declarative are synonyms—that code is either side-effectful &lt;em&gt;and&lt;/em&gt; imperative, or pure &lt;em&gt;and&lt;/em&gt; declarative:&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%2F7durxsp38qjbtmu9xxel.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%2F7durxsp38qjbtmu9xxel.png" alt="Functional - imperative_declarative_binary.svg" width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But functional and declarative are independent dimensions:&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%2Fi8on4tbfkddhezidq8oq.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%2Fi8on4tbfkddhezidq8oq.png" alt="good_quadrants_horizontal_left_labels_gap_tuned.svg" width="800" height="619"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Code that is both functional and imperative debunks this, and so does code that is both side-effectful and declarative.&lt;/p&gt;

&lt;p&gt;Both exist!&lt;/p&gt;

&lt;h2&gt;
  
  
  "Imperative Functional Programming"
&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%2Fm1ti1r4jqetdlqwb17so.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%2Fm1ti1r4jqetdlqwb17so.png" alt="quadrants_tl_br_questions_other_three_70pct_transparent.svg" width="800" height="617"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Simon L Peyton Jones and Philip Wadler helped create Haskell, which is a beautiful, pure functional programming language.&lt;/p&gt;

&lt;p&gt;In 1993 they wrote a paper called &lt;a href="https://dl.acm.org/doi/epdf/10.1145/158511.158524" rel="noopener noreferrer"&gt;"Imperative Functional Programming."&lt;/a&gt; The title may sound like an oxymoron, but it isn't. &lt;/p&gt;

&lt;p&gt;The functional programming community had been struggling with how to manage side-effects like I/O, and these co-creators of Haskell proposed a way to decouple the &lt;em&gt;expression&lt;/em&gt; of I/O commands from their &lt;em&gt;execution&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;But they noticed something:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It will not have escaped the reader’s notice that programs written in the monadic style look rather similar to imperative programs. For example, the echo program in C:&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;echo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nl"&gt;loop:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&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="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;eof&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;putchar&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;loop&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;In Haskell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="n"&gt;echo&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;echo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getcIO&lt;/span&gt; &lt;span class="n"&gt;'bindI0'&lt;/span&gt; &lt;span class="nf"&gt;\&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;eof&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
        &lt;span class="n"&gt;doneIO&lt;/span&gt;
    &lt;span class="kr"&gt;else&lt;/span&gt; &lt;span class="n"&gt;putcI0&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;'seqI0'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Does the monadic style force one, in effect, to write a functional facsimile of an imperative program, thereby losing any advantages of writing in x functional language?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nope! Jones and Wadler list two advantages that remain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;List operations (like &lt;code&gt;map&lt;/code&gt;and  &lt;code&gt;append&lt;/code&gt;) can still operate on monadic commands&lt;/li&gt;
&lt;li&gt;Effects can be expressed without immediately executing them. "It's a bit like being able to define your own control structures in an imperative language."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Recognizing the imperative structure of the Haskell monadic pattern does not mean we have to abandon the advantages that are &lt;em&gt;inherent&lt;/em&gt; in pure functional programming.&lt;/p&gt;

&lt;p&gt;But it &lt;em&gt;is&lt;/em&gt; still imperative structure. &lt;/p&gt;

&lt;h3&gt;
  
  
  "Lazy Imperative Programming"
&lt;/h3&gt;

&lt;p&gt;John Launchbury, another creator of Haskell, wrote a paper the following year called &lt;a href="https://www.researchgate.net/publication/2248360_Lazy_Imperative_Programming" rel="noopener noreferrer"&gt;Lazy Imperative Programming.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In it he says,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Imperative features were introduced to Glasgow Haskell for expressing input and output.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Could this be plainer? Haskell has imperative features. &lt;/p&gt;

&lt;p&gt;And the Haskell code that uses them is imperative. Also from his paper, an "Imperative Scan Left":&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%2F9tecj6o9rjt7h7g2zfg1.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%2F9tecj6o9rjt7h7g2zfg1.png" alt="Haskell - John Launchbury - Imperative Scan Left - Pasted image 20260811005543.png" width="799" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;writeVar&lt;/code&gt; is an imperative statement, which is why this is an example of imperative code—written entirely with pure functions!!!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.researchgate.net/publication/2248360_Lazy_Imperative_Programming" rel="noopener noreferrer"&gt;The paper&lt;/a&gt; has many other references to imperative stuff, including this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In a strict imperative framework such as the IO monad (and most imperative languages), no value could be returned until the whole of the list was traversed. Using lazy sequences, however, this is not the case. If only the head of the list is required then very little of the computation is performed: the variable is allocated and initialised, it is read, and the list returned with that value in the head. If even less is required, merely whether the final list is empty for example, then the variable is not even allocated as only xs needs to be examined in order to give the structure of ys.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🤯&lt;br&gt;
This is insanely cool.&lt;/p&gt;

&lt;p&gt;Again, not everything in Haskell &lt;em&gt;has&lt;/em&gt; to be declarative for it to still allow for incredible &lt;em&gt;execution&lt;/em&gt; properties!&lt;/p&gt;
&lt;h3&gt;
  
  
  The "Imperative Feel" of Concurrent Haskell
&lt;/h3&gt;

&lt;p&gt;Paul Hudak (another creator of Haskell) and Conal Elliot wrote &lt;a href="https://dl.acm.org/doi/pdf/10.1145/258948.258973" rel="noopener noreferrer"&gt;a paper&lt;/a&gt; in 1997 where they weren't happy with the "strongly imperative feel" of Concurrent Haskell—even though it was 100% purely functional.&lt;/p&gt;

&lt;p&gt;(Side note: I owe my passion for programming to this paper. More on that later.)&lt;/p&gt;

&lt;p&gt;Here's an example of what that pure functional approach with an "strongly imperative feel" looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;newEmptyMVar&lt;/span&gt;

&lt;span class="n"&gt;forkIO&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;threadDelay&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;
  &lt;span class="n"&gt;putMVar&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;takeMVar&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;puTMVar box 42&lt;/code&gt; expresses a command to set &lt;code&gt;box&lt;/code&gt;'s value to 42. &lt;/p&gt;

&lt;p&gt;In the authors' own words,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;While this system is purely functional in the technical sense, its semantics has a strongly imperative feel&lt;/strong&gt;. That is, expressions are evaluated without side-effects to yield concurrent, imperative computations, which are executed to perform the &lt;strong&gt;implied side effects&lt;/strong&gt;. In contrast, modeling entire behaviors as implicitly concurrent functions of continuous time yields what we consider &lt;strong&gt;a more declarative feel&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is not a coincidence that the definition of "imperative" in English is "expresses a command" and that a line of code that literally "expresses a command" had an "strongly imperative feel"—even in Haskell.&lt;/p&gt;

&lt;p&gt;But the authors were dissatisfied with that "imperative feel"—so they invented functional reactive programming (FRP):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;&lt;span class="n"&gt;untilB&lt;/span&gt;&lt;span class="p"&gt;`&lt;/span&gt;
    &lt;span class="n"&gt;predicate&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=*&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;
      &lt;span class="o"&gt;-=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just a single declaration. Nothing here expresses a command at all. &lt;code&gt;putMVar&lt;/code&gt; is 100% imperative, while this is 100% declarative and 0% imperative.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Note on "more declarative" and monoparadigmatic dogma
  &lt;h4&gt;
  
  
  Note on "more declarative" and monoparadigmatic dogma
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;Since the industry dogma in the 1990s was "functional = declarative", they had to call FRP &lt;strong&gt;more declarative&lt;/strong&gt;. Concurrent Haskell was already declarative, supposedly, by virtue of being in Haskell—so this had to be &lt;em&gt;extra&lt;/em&gt; declarative 🤷&lt;/p&gt;

&lt;p&gt;And I say that with great admiration for these authors, because I owe my passion for programming to this paper. This was the origin of both RxJS and signals, which I love... very much. Too much for many people.&lt;/p&gt;

&lt;p&gt;The "Lazy Imperative Programming" paper was more contradictory. The author often contrasted Haskell with "imperative languages," while dozens of times referring to features &lt;em&gt;within&lt;/em&gt; Haskell as imperative. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On the one hand, functional languages are commonly more expressive and easier to reason about than imperative languages,&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After reading 20 or so papers from the 1990s talking about programming paradigms, you will understand that the terminology was extremely entrenched. Academics classified programming languages into rigid categories to help explain the programming patterns of the time.&lt;/p&gt;

&lt;p&gt;Functional programmers were especially guilty of reinforcing this reductionism, since they were a minority of programmers and had to fight against the grain with every rhetorical tool they could. Branding functional programming languages as automatically 100% declarative was an enticing selling point—even though we have many Haskell creators very plainly referring to features &lt;em&gt;within&lt;/em&gt; Haskell as imperative.&lt;/p&gt;

&lt;p&gt;Also, in the 1990s very few "imperative languages" had added much functional features, having not had to deal with much concurrent processing yet, which meant languages really did mostly fall into neat categories; multi-paradigmatic languages were not the norm like they are now. The concept of entire languages as "imperative" or "declarative" somewhat applied back then, but today is a nothing but a leftover from decades of synchronous code that all matched the same 2-3 structural patterns.&lt;/p&gt;

&lt;p&gt;We have to make a mental note of this history, but if we do not push to correct the terminology, it will continue to confuse developers and AI. Many developers think that stuffing imperative features between &lt;code&gt;&amp;lt; &amp;gt;&lt;/code&gt; characters magically makes them declarative because it's part of HTML, a "declarative language." Some are attempting this and creating masses of spaghetti code that only cosmetically looks different from the imperative JavaScript spaghetti they were writing last year.&lt;/p&gt;

&lt;p&gt;There are no "declarative languages" and "imperative languages", as much as it would simplify selling them. There are commands expressed in code, which are &lt;strong&gt;imperative&lt;/strong&gt; by the purest definition; and there are features and behaviors &lt;strong&gt;declared&lt;/strong&gt; without scattered commands controlling them from elsewhere.&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;/p&gt;
&lt;br&gt;


&lt;h3&gt;
  
  
  Conclusion (Imperative Functional Programming)
&lt;/h3&gt;

&lt;p&gt;The literal precise English definition of "imperative" is something that expresses a command. That is what these pure functions are doing in Haskell and other functional languages. This should already rest the argument.&lt;/p&gt;

&lt;p&gt;But even the less precise definitions of imperative programming focus on &lt;strong&gt;structural descriptions written in code&lt;/strong&gt; rather than execution details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Imperative programming involves &lt;strong&gt;writing programs as sequences of explicit commands&lt;/strong&gt; that are executed in order from top to bottom." &lt;a href="https://builtin.com/articles/imperative-programming" rel="noopener noreferrer"&gt;https://builtin.com/articles/imperative-programming&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;"logic &lt;strong&gt;encoded&lt;/strong&gt; as a sequence of ordered operations" &lt;a href="https://en.wikipedia.org/wiki/Programming_language" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/Programming_language&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;"Imperative programming &lt;strong&gt;focuses on describing&lt;/strong&gt; how a program operates step by step, rather than on high-level &lt;strong&gt;descriptions&lt;/strong&gt; of its expected results" &lt;a href="https://en.wikipedia.org/w/index.php?title=Imperative_programming" rel="noopener noreferrer"&gt;https://en.wikipedia.org/w/index.php?title=Imperative_programming&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;"With an imperative approach, a developer &lt;strong&gt;writes code that specifies the steps&lt;/strong&gt; that the computer must take to accomplish the goal." &lt;a href="https://learn.microsoft.com/en-us/dotnet/standard/linq/functional-vs-imperative-programming" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/dotnet/standard/linq/functional-vs-imperative-programming&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;"Im­per­a­tive pro­gram­ming languages are &lt;strong&gt;composed of step-by-step in­struc­tions (how)&lt;/strong&gt;... By contrast, in de­clar­a­tive pro­gram­ming, the desired result (what) is &lt;strong&gt;described&lt;/strong&gt; directly." &lt;a href="https://www.ionos.com/digitalguide/websites/web-development/imperative-programming/" rel="noopener noreferrer"&gt;https://www.ionos.com/digitalguide/websites/web-development/imperative-programming/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Imperative programming is a paradigm where you &lt;strong&gt;explicitly state&lt;/strong&gt; how the program should achieve the desired result. &lt;a href="https://octopus.com/devops/infrastructure-as-code/declarative-vs-imperative-programming/" rel="noopener noreferrer"&gt;https://octopus.com/devops/infrastructure-as-code/declarative-vs-imperative-programming/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's very clear that Haskell and functional programming languages are extremely cool with the flexibility they afford for all kinds of things, including the execution of side effects.&lt;/p&gt;

&lt;p&gt;But Haskell has features that enable writing literal imperative commands even with pure functions. This results in code that is structured imperatively: Explicit commands incrementally adding to behavior described elsewhere. The instant a single command appears, it completely takes away the declarative quality of describing the final result of something up-front. The imperative features of Haskell are not just &lt;em&gt;less&lt;/em&gt; declarative; they are completely anti-declarative.&lt;/p&gt;

&lt;p&gt;Functional programming &lt;em&gt;can&lt;/em&gt; be imperative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Declarative Side-Effects
&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%2Fae9pvmixf8vw5k9gb2jt.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%2Fae9pvmixf8vw5k9gb2jt.png" alt="quadrants_only_br_opaque_br_question.svg" width="800" height="616"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  RxJS
&lt;/h3&gt;

&lt;p&gt;RxJS enables fully declarative code, despite latent side-effects.&lt;/p&gt;

&lt;p&gt;RxJS actually came from that Fran paper we looked at earlier, which proposed a way to turn this kind of Concurrent Haskell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;newEmptyMVar&lt;/span&gt;

&lt;span class="n"&gt;forkIO&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;threadDelay&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;
  &lt;span class="n"&gt;putMVar&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;takeMVar&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;&lt;span class="n"&gt;untilB&lt;/span&gt;&lt;span class="p"&gt;`&lt;/span&gt;
    &lt;span class="n"&gt;predicate&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=*&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;
      &lt;span class="o"&gt;-=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither of these trigger side-effects, but the declarative version structures the code as a single description of a complete, final logical result (a behavior over time). The thing that made the first imperative and the second declarative is the &lt;em&gt;structure&lt;/em&gt; of the code, not the side-effect execution model. &lt;/p&gt;

&lt;p&gt;The same structural move can be made in side-effectful code:&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;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withResolvers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;setTimeout&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;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;lastValueFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&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;map&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;42&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;Both of these trigger side-effects when executed, but the declarative version structures the code as a single description of a complete, final logical result. The thing that made the first imperative and the second declarative is the &lt;em&gt;structure&lt;/em&gt; of the code, not the side-effect execution model. &lt;/p&gt;

&lt;p&gt;These four code snippets alone are enough to complete our four quadrants:&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%2F5wsk1y18dfeav7hbx88o.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%2F5wsk1y18dfeav7hbx88o.png" alt="good_quadrants_large_labels_no_axes_even_offset_code_centered_with_titles_rxjs_2021.svg" width="800" height="796"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But let's look at some more common declarative side-effects.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTML
&lt;/h3&gt;

&lt;p&gt;"HTML is declarative" is often said. And it's mostly true (see above callout "Note on "more declarative" and monoparadigmatic dogma").&lt;/p&gt;

&lt;p&gt;Yet the entire point of HTML is a side-effect: You being able to look at beautiful interfaces like this:&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%2F13yt6viayccxy1mkr6k1.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%2F13yt6viayccxy1mkr6k1.png" alt="Old Webpage" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's not just the DOM that's a side effect though. The HTML rendered in the page can itself trigger other kinds of side effects, like network calls:&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="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"&lt;/span&gt; &lt;span class="nt"&gt;/&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;"https://cdn.jsdelivr.net/npm/axios/dist/axios.min.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;!-- ... --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://storage.googleapis.com/blog-images-backup/1*tY3o3UFtBaMQ103en48qxA.png"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://www.youtube.com/embed/dQw4w9WgXcQ"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Nondeterministic Declarations
&lt;/h3&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;Math.random&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;Math.random()&lt;/code&gt; technically produces a side effect because it relies on and mutates a hidden global internal state (the seed or PRNG algorithm pointer) inside the runtime environment to generate its next value.&lt;/p&gt;

&lt;p&gt;But this is still totally declarative:&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;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;code&gt;Date.now()&lt;/code&gt;, &lt;code&gt;crypto.randomUUID()&lt;/code&gt;, &lt;code&gt;process.memoryUsage()&lt;/code&gt;, etc...
&lt;/h4&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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;date&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;perfNow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;randomBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&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;Uint32Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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;hrtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hrtime&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;uptime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uptime&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;memoryUsage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memoryUsage&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;freeMemory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freemem&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;battery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBattery&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;windowWidth&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;innerWidth&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;visibility&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="nx"&gt;visibilityState&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;online&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onLine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of these are completely declarative. They are declarations of final results, and no subsequent step-by-step (or any) imperative code is involved.&lt;/p&gt;

&lt;p&gt;But they are not pure functions. They don't have side effects, but they have side causes, so they still are not functional. So while they aren't examples of declarative code that causes side effects, they are examples of declarative code that break rules of functional programming, showing that these are independent dimensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Declarative "Pure" Functions with Side-Effects
&lt;/h3&gt;

&lt;h4&gt;
  
  
  CPU
&lt;/h4&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;fib&lt;/span&gt; &lt;span class="o"&gt;=&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&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="nf"&gt;fib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;fib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything here looks pure as the driven snow, and it's definitely declarative.&lt;/p&gt;

&lt;p&gt;No side effects at all, right?&lt;/p&gt;

&lt;p&gt;In fact, it triggers 40,730,022,147 calls, which means that its side effects are so severe that I really don't recommend pasting that into dev tools and running it. You think jQuery has interesting side effects by reaching into the DOM and changing the behavior of random things, but this will change the behavior of everything on the page. Namely, it eliminates it because it locks up the CPU. &lt;/p&gt;

&lt;h4&gt;
  
  
  Memory
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No side effects here, right? It's declarative, at least.&lt;/p&gt;

&lt;p&gt;However, this might get compiled into assembly code that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mov     rdi, 8589934592
call    malloc

mov     rcx, 1073741824
mov     rdi, rax
xor     eax, eax
rep stosq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a huge side-effect in this code: &lt;code&gt;call malloc&lt;/code&gt; requests &lt;code&gt;8589934592&lt;/code&gt; = (2 ** 30) × 8 bytes = 1,073,741,824 × 8 bytes ≈ &lt;strong&gt;8 GiB&lt;/strong&gt; of memory.&lt;/p&gt;

&lt;p&gt;There may be guardrails against creating arrays this big. I didn't feel like testing it. But there is a variation that may not be prevented:&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="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="p"&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time each inner array has only 1 million elements, but there are 1000 of them, so 1,000,000,000 total elements.&lt;/p&gt;

&lt;p&gt;Here are some possible side-effects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The page or Node.js process becomes unresponsive while the runtime tries to allocate and initialize the array&lt;/li&gt;
&lt;li&gt;In a severe case, the browser tab crashes because it exceeds its memory limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And yet, it's still declarative.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion (Declarative Side-Effects)
&lt;/h3&gt;

&lt;p&gt;Declarative and functional are not synonyms. Declarative code is about &lt;em&gt;how a computation is structured&lt;/em&gt;: describing a result rather than spelling out a sequence of commands to get there.&lt;/p&gt;

&lt;p&gt;Functional purity is about &lt;em&gt;what that computation depends on and affects&lt;/em&gt;. A declaration can trigger network requests, consume CPU and memory, observe clocks and runtime state, or depend on hidden mutable state without becoming imperative.&lt;/p&gt;

&lt;p&gt;These are separate dimensions.&lt;/p&gt;

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

&lt;p&gt;The point of separating these dimensions is not to win a pointless terminology argument. It is to make better engineering decisions while AI is applying incredible pressure on our architectural patterns.&lt;/p&gt;

&lt;p&gt;If we collapse &lt;strong&gt;functional&lt;/strong&gt; into &lt;strong&gt;declarative&lt;/strong&gt;, we lose the ability to talk precisely about what code is doing. Pure code can still be structured imperatively. Declarative code can still observe mutable state, trigger I/O, allocate absurd amounts of memory, or otherwise affect the world.&lt;/p&gt;

&lt;p&gt;Structure and purity are independent properties of code that sometimes align, but &lt;em&gt;DO NOT HAVE TO&lt;/em&gt;!&lt;/p&gt;

&lt;p&gt;If the problem is hidden state, surprising effects, or difficulty reasoning about dependencies, functional programming can help with its explicit inputs, immutable values, pure transformations, effect isolation. And although I love functional programming, in my experience, most apps genuinely will never see significant consequences for not being purely functional. &lt;/p&gt;

&lt;p&gt;However, most apps I have seen have suffered severely from being imperative: It always scatters control, with commands reaching across a system (spaghetti code), or behavior that can only be understood by mentally executing twenty steps across multiple contexts. Declarative code fixes this. Functional programming does not.&lt;/p&gt;

&lt;p&gt;I am sick of saying I like declarative code and being told to juST UsE ELm or HASkElL.&lt;/p&gt;

&lt;p&gt;People can't even understand the problem because they don't have the words to discuss it properly.&lt;/p&gt;

&lt;p&gt;Now throw AI into this cross-talking mix of contradictions, and this is what we can expect:&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%2Fljikqwosmna3pdofj63o.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%2Fljikqwosmna3pdofj63o.png" alt="Spaghetti Nuclear Explosion.png" width="800" height="641"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This looks delicious, but can we please try to avoid it anyway?&lt;/p&gt;

&lt;p&gt;Let's fix our vocabulary so we can learn real things about imperative code and &lt;a href="https://dev.to/playfulprogramming/5-reasons-to-avoid-imperative-code-e09"&gt;steer humans and AI away from it.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>imperative</category>
      <category>declarative</category>
      <category>haskell</category>
    </item>
    <item>
      <title>OpenRouter: One API Key to Rule Them All 🔑</title>
      <dc:creator>Domenico Tenace</dc:creator>
      <pubDate>Wed, 12 Aug 2026 07:20:51 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/openrouter-one-api-key-to-rule-them-all-304b</link>
      <guid>https://dev.to/playfulprogramming/openrouter-one-api-key-to-rule-them-all-304b</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

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

&lt;p&gt;Managing AI models in 2026 has gotten ridiculous. Anthropic key here, OpenAI key there, a separate billing account for Google, another for Mistral, and if you want to try DeepSeek you're adding yet another dashboard to the pile. It's chaos.&lt;/p&gt;

&lt;p&gt;OpenRouter fixes this. One API key, one credit balance, 300+ models. I've been using it as the backbone of my AI setup for a while now, and recently I wired it directly into OpenCode, which made the whole workflow click into place.&lt;/p&gt;

&lt;p&gt;Let me explain how it all works. Let's dive in! 🤙&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is OpenRouter? 🤔
&lt;/h2&gt;

&lt;p&gt;OpenRouter is a unified API gateway that sits between your application and the underlying LLM providers. You send a request to OpenRouter, it forwards it to the model you specified, and returns the response in a normalized OpenAI-compatible format.&lt;/p&gt;

&lt;p&gt;The catalog currently covers 300+ models from every major provider: Anthropic, OpenAI, Google, Meta, Mistral, xAI, DeepSeek, Qwen, NVIDIA, and dozens of smaller ones. You switch models by changing a single parameter in your request. No code changes, no new integrations, no new billing accounts.&lt;/p&gt;

&lt;p&gt;It's also OpenAI SDK-compatible, which means if you're already using the OpenAI client in your code, you change the base URL and the API key, and everything else works unchanged.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works 🔧
&lt;/h2&gt;

&lt;p&gt;The setup is three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Create a free account at openrouter.ai&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add credits via credit card or crypto (no minimum purchase, no expiration)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generate an API key and use it across all your tools&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From that point on, switching models is just changing a string. If you were calling Claude Sonnet directly through Anthropic and want to test Gemini Flash or DeepSeek for the same task, you change one line. That's it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Provider Routing and Fallbacks
&lt;/h3&gt;

&lt;p&gt;One of OpenRouter's strongest practical features is automatic provider fallback. Many popular models are served by multiple infrastructure providers. If one returns an error or hits capacity, OpenRouter silently routes to an alternative and your request succeeds. You don't write retry logic for this, it just works.&lt;/p&gt;

&lt;p&gt;There's also an auto-router mode: set the model to &lt;code&gt;openrouter/free&lt;/code&gt; and OpenRouter selects the best available free model for your request based on what it needs, structured output, tool calling, image understanding, and so on.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Free Tier
&lt;/h3&gt;

&lt;p&gt;OpenRouter offers around 29 free models at any given time, including Llama 3, Gemma 4, DeepSeek Flash, Qwen3, and others. Free accounts get 50 requests per day without adding credits, and 1,000 per day after a first top-up.&lt;/p&gt;

&lt;p&gt;The free model lineup shifts as providers change their policies, so treat it as a useful resource for prototyping, not a guaranteed production tier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pricing: What You Actually Pay 💰
&lt;/h2&gt;

&lt;p&gt;This changed significantly in 2025. OpenRouter used to add per-token markups on top of provider pricing. That model is gone.&lt;/p&gt;

&lt;p&gt;The current structure is straightforward: a flat 5.5% fee on credit purchases, and provider token prices passed through at cost. So if Claude Sonnet 4.6 is $3/$15 per million tokens at Anthropic, you pay $3/$15 on OpenRouter. The only overhead is the 5.5% you paid when you loaded credits.&lt;/p&gt;

&lt;p&gt;The one additional charge: if you bring your own provider API keys (BYOK) and exceed 1 million requests per month, there's a 5% fee on usage beyond that threshold. Below 1M requests it's free to route through your own keys.&lt;/p&gt;

&lt;p&gt;To put it concretely: loading $100 in credits costs $105.50. Everything you buy with those credits is at direct provider rates.&lt;/p&gt;

&lt;p&gt;For hobbyists and developers running multiple models, this is genuinely competitive. For enterprises burning millions of tokens per day, negotiated direct rates might beat it, but you'd have to be at serious scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  OpenRouter + OpenCode 🔗
&lt;/h2&gt;

&lt;p&gt;This is the combination I've been running, and it's become my default setup.&lt;/p&gt;

&lt;p&gt;OpenCode supports any OpenAI-compatible endpoint, which OpenRouter is. You point OpenCode at OpenRouter, set your API key, and from that moment you can use any model in OpenCode's model selector without managing separate credentials for each provider.&lt;/p&gt;

&lt;p&gt;The practical benefit is flexibility without friction. When I want to try Qwen3 Coder for a refactoring task, I switch the model in OpenCode. When I need Claude Opus for something that requires stronger reasoning, I switch back. One balance covers everything.&lt;/p&gt;

&lt;p&gt;It also means I'm not locked into any single provider's availability. If Anthropic has downtime, I route through Gemini or DeepSeek without touching my workflow. OpenRouter's provider fallback does the heavy lifting.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pros 👍
&lt;/h2&gt;

&lt;p&gt;One API key and one billing account replace every separate provider integration you'd otherwise manage. For teams or individual developers using more than two models regularly, that alone justifies the overhead.&lt;/p&gt;

&lt;p&gt;The OpenAI-compatible API means zero migration cost if you're already using the OpenAI SDK. Change the base URL, change the key, done.&lt;/p&gt;

&lt;p&gt;Provider fallback is genuinely useful in production. You stop worrying about individual provider outages because OpenRouter handles the retry layer for you.&lt;/p&gt;

&lt;p&gt;The free model catalog gives you a real prototyping environment at zero cost. Qwen3 Coder Free and DeepSeek Flash Free are capable enough for a lot of development tasks.&lt;/p&gt;

&lt;p&gt;Transparent pricing with no hidden per-token markups. The 5.5% credit fee is upfront and easy to account for.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cons 👎
&lt;/h2&gt;

&lt;p&gt;The 5.5% credit fee is real money at scale. If you're running high-volume production workloads, that overhead adds up. Direct provider APIs will be cheaper once you're large enough to negotiate rates.&lt;/p&gt;

&lt;p&gt;Credits don't expire, but they also aren't refundable. You're committing capital upfront even if your usage patterns shift.&lt;/p&gt;

&lt;p&gt;Free model availability is unstable. Providers can remove free-tier access at any time and OpenRouter has no control over it. Don't build a production workflow that depends on a specific free model being there next month.&lt;/p&gt;

&lt;p&gt;Enterprise teams needing RBAC, per-team budgets, audit logs, and VPC-native routing will hit a ceiling. OpenRouter is primarily a routing and billing layer, not a governance platform. For stricter compliance needs, something like LiteLLM self-hosted or Portkey might be more appropriate.&lt;/p&gt;

&lt;p&gt;The auto-router mode (&lt;code&gt;openrouter/free&lt;/code&gt;) is convenient, but the model selection varies between requests. If you need consistent output style across sessions, pin a specific model ID instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who Should Use OpenRouter? 🎯
&lt;/h2&gt;

&lt;p&gt;OpenRouter is the obvious choice if you're using more than one or two AI models regularly and don't want to manage the overhead of multiple accounts and billing setups. For individual developers, it removes a real operational annoyance.&lt;/p&gt;

&lt;p&gt;It's also the right call for tools like OpenCode that support BYOK or custom endpoints but don't manage multi-provider routing themselves. OpenRouter becomes the infrastructure layer your tool sits on top of.&lt;/p&gt;

&lt;p&gt;Where it makes less sense: pure Anthropic shops that only need Claude, or enterprises with compliance requirements that need on-premise routing and audit trails. For those cases, the flexibility isn't worth the trade-offs.&lt;/p&gt;




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

&lt;p&gt;OpenRouter started as a scrappy model aggregator with 30 models. In 2026 it's become the most important middleware layer in the independent developer's AI stack, and the pricing reform that eliminated per-token markups made it significantly more honest to use.&lt;/p&gt;

&lt;p&gt;The combination with OpenCode is what made it click for me. Instead of managing Anthropic and Google credentials separately, I load credits into OpenRouter once and switch models from OpenCode's interface as needed. It's the kind of setup that removes decisions you don't want to be making mid-task.&lt;/p&gt;

&lt;p&gt;One key, one balance, hundreds of models. For how messy the provider landscape has gotten, that's a genuinely useful simplification.&lt;/p&gt;

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




&lt;p&gt;Hi 👋🏻&lt;br&gt;&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;&lt;br&gt;
Don't forget to visit my social links to discover my links and to check out Domenico Tenace Open Labs for my open-source projects! 🫰🏻&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;&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;/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;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/openrouter-one-api-key-to-rule-them-all-917xc" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>developers</category>
    </item>
  </channel>
</rss>
