<?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>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>
    <item>
      <title>OpenCode: The Open Source Coding Agent That Doesn't Lock You In 🔓</title>
      <dc:creator>Domenico Tenace</dc:creator>
      <pubDate>Wed, 05 Aug 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/opencode-the-open-source-coding-agent-that-doesnt-lock-you-in-pn3</link>
      <guid>https://dev.to/playfulprogramming/opencode-the-open-source-coding-agent-that-doesnt-lock-you-in-pn3</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

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

&lt;p&gt;If you've ever hit your Claude Code token limits mid-task, wondered what's happening under the hood of a closed-source agent, or just wanted to plug in a different model without switching tools entirely, OpenCode might be exactly what you were looking for.&lt;/p&gt;

&lt;p&gt;It's the open source alternative that took the terminal agent concept and made it model-agnostic, and by mid-2026 it had already surpassed Claude Code on GitHub stars. That's not a coincidence.&lt;/p&gt;

&lt;p&gt;Let me break it down. Let's dive in! 🤙&lt;/p&gt;




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

&lt;p&gt;OpenCode is an open source AI coding agent built in Go that runs in the terminal, desktop, and IDE. Its thesis is simple: you bring the model, OpenCode brings the agent. It supports 75+ LLM providers, from Claude and GPT to local models via Ollama, doesn't store your code, doesn't charge a subscription, and over 7.5 million developers use it monthly.&lt;/p&gt;

&lt;p&gt;It was built by the SST team, the same people behind the popular serverless framework. The design is a client/server architecture, not a single CLI binary, so one backend drives a terminal TUI, a desktop app, and IDE extensions for VS Code and Cursor.&lt;/p&gt;

&lt;p&gt;OpenCode surpassed Claude Code on GitHub stars (160K+ vs 122K+) and is now the most starred open source coding agent. It does what Claude Code does, but lets you plug in any model from 75+ providers, including local models at zero API cost.&lt;/p&gt;




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

&lt;p&gt;Installing OpenCode takes about 30 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://opencode.ai/install | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You land in a polished TUI with two modes, Build and Plan. Build mode executes tasks directly. Plan mode is read-only, it lets the agent analyze your codebase and propose an approach before touching a single file. This is the separation of intent and execution that Claude Code doesn't make as explicit.&lt;/p&gt;

&lt;p&gt;The design choice that matters most is the one in the name. OpenCode separates the agent harness from the model. Claude Code is tuned end to end around Anthropic's models and talks to them and only them. OpenCode connects to 75+ providers through Models.dev, including Anthropic, OpenAI, Google, Moonshot, Z.ai, local runtimes through Ollama, and any OpenAI-compatible endpoint you hand it.&lt;/p&gt;

&lt;p&gt;You can switch models mid-session without restarting. That alone is a feature Claude Code doesn't have.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Pricing Reality 💰
&lt;/h2&gt;

&lt;p&gt;This is where OpenCode makes its strongest argument. There's no subscription for the software itself, you pay for model access directly.&lt;/p&gt;

&lt;p&gt;The options as of mid-2026:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bring your own key&lt;/strong&gt;: pay the provider directly at their posted rates. Full flexibility, full cost visibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenCode Zen&lt;/strong&gt;: a pay-as-you-go hosted gateway with access to many models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenCode Go&lt;/strong&gt;: $5 for the first month, then $10/month. Includes access to capable open models like GLM, Kimi K2, Qwen, DeepSeek, and MiniMax with generous per-session request limits.&lt;/p&gt;

&lt;p&gt;Compare that to Claude Code's $20/month Pro, $100/month Max 5x, or $200/month Max 20x plans. For individual developers or small teams watching their AI spend, the math is clear.&lt;/p&gt;

&lt;p&gt;The caveat: cost visibility helps when you are counting. Once you settle onto a flat Max plan, the math changes. If you're a power user who hits their limits constantly, Claude Code's flat rate might actually be cheaper per task.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Anthropic Block: What Happened and Where It Stands 🚧
&lt;/h2&gt;

&lt;p&gt;This is the part of the story that redefined what OpenCode actually is.&lt;/p&gt;

&lt;p&gt;On January 9, 2026, Anthropic changed its OAuth policy to block third-party applications from authenticating users through Claude.ai accounts. OpenCode was the primary casualty: users who relied on their Claude Pro login lost access overnight. The official reason was "security and ToS compliance."&lt;/p&gt;

&lt;p&gt;The January block made that choice less about saving money and more about philosophy. You are no longer choosing a cheaper way to run Claude. You are choosing whether you want a managed product or an open one.&lt;/p&gt;

&lt;p&gt;Where it stands today: you can still use Claude models in OpenCode, but only through an Anthropic API key, not your Claude.ai subscription credentials. The more lasting impact was reputational: it accelerated many users migrating to Gemini as their default backend.&lt;/p&gt;


&lt;h2&gt;
  
  
  OpenCode vs Claude Code: The Real Differences 🥊
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Terminal experience
&lt;/h3&gt;

&lt;p&gt;OpenCode tends to win here, and it wins on feel. The TUI is the repeated favorite in nearly every comparison thread. The terminal does not flicker on each update, sections scroll independently, and planning prompts are easy to answer. OpenCode also ships a standalone Tauri desktop app for macOS, Windows, and Linux, something Claude Code doesn't have.&lt;/p&gt;
&lt;h3&gt;
  
  
  Autonomy features
&lt;/h3&gt;

&lt;p&gt;Claude Code has a wider toolset: Agent View for fleet management, /goal for fire-and-forget autonomous runs, instant rewind via double-Esc, and background monitoring. OpenCode has background subagents and sessions that survive a terminal close, but lacks the orchestration depth.&lt;/p&gt;
&lt;h3&gt;
  
  
  Output quality
&lt;/h3&gt;

&lt;p&gt;A controlled benchmark found Claude Code faster and OpenCode more thorough using the same model. The summary: "Claude Code is built for speed. OpenCode is built for thoroughness."&lt;/p&gt;

&lt;p&gt;One practical issue to know about: multiple testers reported that OpenCode, across all tested models, reformats existing code without authorization. On mature codebases with established style guides, this is a trust issue. There's an open GitHub issue tracking this, and it's the most common complaint in real-world use.&lt;/p&gt;


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

&lt;p&gt;&lt;strong&gt;True model freedom.&lt;/strong&gt; No other terminal agent lets you swap between Claude, Gemini, DeepSeek, Qwen, or a local Ollama model mid-session. This is OpenCode's defining advantage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open source and inspectable.&lt;/strong&gt; You can read the codebase, fork it, self-host it, and know exactly what the agent is doing with your code. Claude Code is closed source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better terminal UX.&lt;/strong&gt; The TUI is genuinely nicer. Non-flickering updates, scrollable sections, a dedicated desktop app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost control.&lt;/strong&gt; Bring your own key, use local models at zero cost, or pay $10/month for the Go tier. The ceiling is much lower than Claude Code's $200/month Max plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data compliance.&lt;/strong&gt; Teams with strict data requirements can point OpenCode at a local or self-hosted model so code never leaves their network.&lt;/p&gt;


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

&lt;p&gt;&lt;strong&gt;The code reformatting issue.&lt;/strong&gt; OpenCode has a known tendency to reformat existing code without being asked. On mature codebases this creates noisy diffs and erodes trust in the agent's edits. Not a dealbreaker, but worth knowing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thinner plugin ecosystem.&lt;/strong&gt; Claude Code has a more mature skills, hooks, and plugin marketplace. OpenCode's extension catalog is catching up, but it's not there yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No subscription path for Claude.&lt;/strong&gt; If you're paying for Claude Pro or Max, you can't use that credit in OpenCode. You need a separate API key, which means paying again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fewer autonomous orchestration features.&lt;/strong&gt; /goal mode, Agent View, instant rewind, and worktree isolation are Claude Code exclusives. For complex multi-agent workflows, Claude Code is still ahead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Broad permissions by default.&lt;/strong&gt; Several developers flag that OpenCode's agent permissions are broad by default. Check opencode.json before you point it at anything sensitive.&lt;/p&gt;


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

&lt;p&gt;OpenCode is the right choice if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want model flexibility and don't want to be locked into Anthropic's roadmap&lt;/li&gt;
&lt;li&gt;Budget control matters and you want to use cheaper or local models&lt;/li&gt;
&lt;li&gt;You prefer an open source tool you can inspect, fork, and trust&lt;/li&gt;
&lt;li&gt;Your team has compliance requirements that require on-premise model inference&lt;/li&gt;
&lt;li&gt;You find Claude Code's terminal UX annoying and want something nicer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay with Claude Code if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need the strongest possible instruction-following on hard tasks&lt;/li&gt;
&lt;li&gt;You use Agent View, /goal, or other orchestration features heavily&lt;/li&gt;
&lt;li&gt;You want a managed product that "just works" without configuration&lt;/li&gt;
&lt;li&gt;You're already paying for Max and want to get full value from it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And honestly, plenty of people run both. OpenCode for day-to-day work with flexible models, Claude Code for the heavy debugging sessions where Opus is worth it.&lt;/p&gt;


&lt;h2&gt;
  
  
  My Honest Take 💭
&lt;/h2&gt;

&lt;p&gt;OpenCode is a serious tool, not a hobby alternative. 178,000 GitHub stars and 7.5 million monthly users don't lie. The model-agnostic design is genuinely useful, and the terminal UX is objectively better than Claude Code's.&lt;/p&gt;

&lt;p&gt;But the January block revealed something important: the moment you depend on Claude's quality and want to pay less, you're in Anthropic's crosshairs. OpenCode solved the tooling problem, not the model dependency problem. If you're using it with Claude via API key, you're still paying Anthropic rates.&lt;/p&gt;

&lt;p&gt;The real freedom comes when you combine OpenCode's harness with open or local models. That's the combination that makes the cost argument actually land, and that's where the project's future clearly points.&lt;/p&gt;

&lt;p&gt;Managed product or open tool? The answer depends on who you want to be as a developer.&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 Linktree to discover my links and to check out Domenico Tenace Open Labs for my open-source projects! 🫰🏻&lt;/p&gt;

&lt;p&gt;🌲 Linktree: &lt;a href="https://linktr.ee/domenicotenace" rel="noopener noreferrer"&gt;https://linktr.ee/domenicotenace&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;/p&gt;

&lt;p&gt;Follow me on dev.to for more articles 👇&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__985143"&gt;
    &lt;a href="/dvalin99" 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%2F985143%2Fc4c372a7-0b38-4f9e-b206-7ed65597ea31.png" alt="dvalin99 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="/dvalin99"&gt;Domenico Tenace&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/dvalin99"&gt;Passionate about the IT world and everything related to it ✌🏻
Open Source enthusiastic 🦠&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&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;&lt;a href="https://www.buymeacoffee.com/domenicotenace" rel="noopener noreferrer"&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%2Fb5vrzbmybu3q0sb5bzs1.png" alt="Buy Me A Coffee" width="545" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Introducing Argus: The Code Security Colleague Your Team Doesn't Have</title>
      <dc:creator>Davide Imola</dc:creator>
      <pubDate>Thu, 30 Jul 2026 08:56:00 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/introducing-argus-the-code-security-colleague-your-team-doesnt-have-5g81</link>
      <guid>https://dev.to/playfulprogramming/introducing-argus-the-code-security-colleague-your-team-doesnt-have-5g81</guid>
      <description>&lt;p&gt;Most development teams don't have a security engineer. Not because they don't want one. Security expertise is genuinely hard to find, expensive to keep, and most of us would rather ship features than spend an afternoon reading about deserialization gadget chains. So security becomes the thing you'll get to later, and later never comes.&lt;/p&gt;

&lt;p&gt;I've believed for a while that AI is going to change security, and not as a slogan. The reason is structural: there aren't enough people. The industry is short something like 4.8 million security professionals worldwide,&lt;sup id="fnref1"&gt;1&lt;/sup&gt; which is the gap from that first paragraph counted globally, and no hiring spree closes a hole that size. AI won't close it either, and I want to be honest about that. What it can do is make the gap hurt less: take the routine load off the few people who are there, and keep a small team, or a team of one, from being buried. Same as it did for my coding, where it didn't make me smarter, it just stopped the grunt work from being the bottleneck.&lt;/p&gt;

&lt;p&gt;A good model has read more security than most of us ever will: the best practices, the attack patterns, the bugs that hide in plain sight. Point it at code written by someone who isn't a security expert, which is most of us, and it genuinely lifts what they can ship safely. This isn't a spectator take for me either, it's what I spend my days on, building products that live right at that intersection. And the other side already gets it: attackers are using AI right now, so insisting defenders shouldn't is not caution, it's a handicap.&lt;/p&gt;

&lt;p&gt;Concretely, I already use AI for security review, enough that I'm building a talk around how to do it well: lean on the skills that already exist, write your own, and point them at the real tools you already have. Give a capable model the right context and it quietly does a lot of the work a tired reviewer skips under a deadline. What it doesn't do is stick around. Every session starts from zero, knows nothing about your last review, forgets what you told it yesterday.&lt;/p&gt;

&lt;p&gt;Then I started using &lt;a href="https://github.com/openclaw/openclaw" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt;, and it reframed the problem for me. OpenClaw is an always-running agent with a SOUL, a memory, and enough context about you that talking to it feels less like prompting a chatbot and more like messaging a colleague. It remembers. It knows your setup. I used it for useful things and for completely useless ones, and I had a lot of fun. We've mostly stopped since, if I'm honest, but it worked, and somewhere in there the idea landed: what if that colleague did security?&lt;/p&gt;

&lt;p&gt;That's &lt;a href="https://github.com/argusappsec/argus" rel="noopener noreferrer"&gt;Argus&lt;/a&gt;: an open-source application security agent, written in Go, that I've been building with a few colleagues at &lt;a href="https://redcarbon.ai" rel="noopener noreferrer"&gt;RedCarbon&lt;/a&gt;. It's the project that quietly took the place of &lt;a href="https://dev.to/blog/i-built-a-tool-i-dont-use"&gt;Worky&lt;/a&gt;, the tool I built and then never used, except this one has a mission I actually care about, so I'm not going anywhere.&lt;/p&gt;

&lt;p&gt;Fair warning before you get excited: Argus is v0.2.0, and it's as much an idea as a tool. It's the thing I kept wishing existed, so I built a first version with people who actually do security for a living. I'm not sure I've got the shape right, but the shape is the part worth arguing about, and I'd rather do that in the open than sit on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why security review needs a different shape
&lt;/h2&gt;

&lt;p&gt;I want to be clear here, because this is easy to misread. I'm not saying AI security review is a bad idea. It's a great idea. What I don't believe in is doing it with AI alone. Models hallucinate, and a confident, wrong answer about a vulnerability is worse than no answer at all. Lean the whole thing on a model and you get a tool that sounds authoritative while quietly missing the one finding that mattered.&lt;/p&gt;

&lt;p&gt;So the bet behind Argus isn't "point a model at your code". It's the union of three things that each cover for the others' blind spots: deterministic security tools that have been proven for years, a human kept firmly in the loop, and a model to tie it together and reason across the whole picture. Take any one of the three away and it falls over. Especially the human.&lt;/p&gt;

&lt;p&gt;Recognizing a security problem is not something you wing. Get a code suggestion wrong and it's annoying; get a security call wrong and it's a breach. So an agent you'd actually trust with this has to earn that trust in ways a coding assistant never has to. Its answers need to trace back to something real instead of a confident guess. Its memory, from the moment it has one, becomes a thing attackers will try to lie to, so who gets to shape it suddenly matters. And it has to understand your world in particular, not security in the abstract, because a good share of the problems that actually bite only make sense once you know how your systems fit together.&lt;/p&gt;

&lt;p&gt;None of these choices are settled. They're the ones I'd make today, pulled from the years I've spent around cybersecurity and filed down to the few details I think actually matter. I'm genuinely happy to have every one of them argued with. Here's the reasoning, a piece at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured tools, not a shell
&lt;/h2&gt;

&lt;p&gt;The default way to build a code agent in 2026 is to hand a model a shell and let it run. It works shockingly well for writing code. For security, I didn't want it.&lt;/p&gt;

&lt;p&gt;When an agent has a shell, two things are true at once: it can do anything, and you can't be sure what it did. In a security context that's backwards. You want the opposite. A small, known set of capabilities, each one auditable.&lt;/p&gt;

&lt;p&gt;So Argus doesn't get a shell. It gets tools. Under the hood there are real, boring, deterministic security scanners: &lt;a href="https://semgrep.dev" rel="noopener noreferrer"&gt;Semgrep&lt;/a&gt; for static analysis, &lt;a href="https://github.com/gitleaks/gitleaks" rel="noopener noreferrer"&gt;Gitleaks&lt;/a&gt; for secrets, &lt;a href="https://github.com/google/osv-scanner" rel="noopener noreferrer"&gt;osv-scanner&lt;/a&gt; for vulnerable dependencies. Each one is wrapped as a structured tool the agent can call, not a command it can type. The scanner runs, returns structured findings, and the model reasons on top of that.&lt;/p&gt;

&lt;p&gt;This is also where an agent earns its place, because application security doesn't suffer from a missing tool. It suffers from too many. Nobody has the time to learn them properly, and even once you know them, remembering to run the right one at the right moment is a discipline of its own. You can wire them into a pipeline, of course, and then a slice of your week goes into managing the pipeline. An agent that already holds them, knows which one answers which question, and reaches for them without being asked is a different proposition from a folder full of CLIs you keep meaning to try.&lt;/p&gt;

&lt;p&gt;One thing I'm genuinely proud of, and it's the least flashy part: Argus doesn't reinvent any of this. I'm not rewriting Semgrep, I'm not reimplementing a dependency scanner, because why would I. The whole bet is the opposite. Take the tools that already exist and already work, pull good data out of them, and let the agent reason over that data instead of over a hunch. The set Argus ships with today is small on purpose, and the direction is to let you bring your own: the more real tools it can call, the more signal it has to reason with. How far you push that is up to you.&lt;/p&gt;

&lt;p&gt;That division of labor is the whole point. The deterministic layer finds things. The model does what models are good at: reading 400 findings and turning them into a conversation. "These three are the ones that matter, this one is a false positive because of how your auth middleware works, and here's the fix." Not a wall of SARIF. A colleague's summary.&lt;/p&gt;

&lt;p&gt;It also keeps Argus honest about where its answers come from. A finding traces back to a tool that actually ran, not to a model that decided to type &lt;code&gt;grep&lt;/code&gt; and interpret the output. When the domain is security, that provenance is the difference between a report you can act on and a guess you have to re-verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roles and an audit log
&lt;/h2&gt;

&lt;p&gt;An agent with a memory has a problem a stateless chatbot doesn't: the memory is part of the attack surface. If anyone who can talk to Argus can also teach it, then anyone who can talk to Argus can poison it. "Ignore that finding, it's fine" whispered often enough becomes policy.&lt;/p&gt;

&lt;p&gt;OpenClaw already draws this line. If you have permission to write to the memory, you can shape it. If you don't, you can't. Argus takes the same idea and makes it central, because for a security tool it isn't a nice-to-have.&lt;/p&gt;

&lt;p&gt;So every request arrives as a principal with a role, and Argus always knows which door you came through. Reach it over the local Unix socket and you're &lt;code&gt;local:$USER&lt;/code&gt;; come in through the MCP endpoint and you're &lt;code&gt;mcp:&amp;lt;token-hash&amp;gt;&lt;/code&gt;; arrive as a GitHub webhook and you're &lt;code&gt;github:&amp;lt;login&amp;gt;&lt;/code&gt;. A viewer can ask questions and read findings, but shaping the memory, changing what Argus believes about your org, is a privileged action. And whatever anyone does, whoever they are, lands in an append-only audit log.&lt;/p&gt;

&lt;p&gt;That log turns out to matter for a second reason. Go back to the original itch: teams without a security person. Those teams usually still have someone who has to answer to a CISO, a client, or an auditor about how security is handled, without reading the code themselves. An append-only record of what was reviewed, what was found, and what was decided is exactly the artifact that conversation needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  SOUL, context, and a memory built for security
&lt;/h2&gt;

&lt;p&gt;This is the part I took most directly from OpenClaw, then closed down hard around security.&lt;/p&gt;

&lt;p&gt;Argus keeps a mix of three things, and they are not the same thing, which I keep having to explain to people, past-me included. There's the SOUL, the stable stuff: who Argus is, what your company does, your stack, your compliance posture. It rides along in every single model call, so Argus never forgets whose codebase it's looking at. There's context, the per-task working set for whatever it's reasoning about right now. And there's memory, the curated knowledge that persists from one conversation to the next.&lt;/p&gt;

&lt;p&gt;The memory is where the security focus shows. Argus remembers findings. It remembers the things you tell it. And, my favorite part, it remembers your false positives, so the thing you already triaged and dismissed doesn't come back to eat your afternoon next week. That is the single most colleague-like behavior in the whole system. A good reviewer remembers what you already decided.&lt;/p&gt;

&lt;p&gt;Say &lt;code&gt;config.py&lt;/code&gt; in one of your repos only holds deploy-time placeholders that your secret manager swaps out at deploy. Tell Argus once, and it stops flagging them on that repo. The next review comes back clean, with a note explaining why, instead of the same false alarm you waved off last sprint.&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%2Fwrp3bklpmabmti0obnwn.webp" 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%2Fwrp3bklpmabmti0obnwn.webp" alt="A later Argus review on the same repository reporting no new findings, noting that the config.py credentials are known deploy-time placeholders and not a vulnerability in this project" width="800" height="273"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The SOUL is what makes the reasoning fit your world instead of the abstract world of security-in-general. And the cross-session, cross-project knowledge is where I think this goes somewhere a single-repo tool can't follow. A coding agent pointed at one repository is blind to how that repository talks to the other twelve. Plenty of the security problems that actually hurt live in exactly that gap: the service that trusts input another service should have sanitized. Feed Argus enough of your world and it can start to reason across that boundary. It's early, but that's the direction.&lt;/p&gt;

&lt;p&gt;OpenClaw is a general assistant, open-ended by design. Argus is the opposite. It knows it's a security tool, and everything (the tools, the skills, the memory, the persona) is shaped for that one job. Narrower, but deeper.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Argus does today (v0.2)
&lt;/h2&gt;

&lt;p&gt;Enough philosophy. Here is what actually runs.&lt;/p&gt;

&lt;p&gt;One thing to set straight first, because the word "security" invites the wrong expectation. Argus doesn't watch your infrastructure, it doesn't sit in front of your traffic, and it is not a SOC. It reads code. That's the job it's built for, and the rest is somebody else's product.&lt;/p&gt;

&lt;p&gt;Argus is a daemon, &lt;code&gt;argusd&lt;/code&gt;, one shared instance per organization. Every way you talk to it (a terminal UI, the MCP endpoint, GitHub webhooks, and Slack soon) is a different door into the same process, sharing one memory, one tool registry, one audit log.&lt;/p&gt;

&lt;p&gt;The most visible thing it does is review pull requests. Connect it as a GitHub App and it turns up on the thread like any other reviewer, except this one only cares about security. When it flags something you can push back right there, "Argus, is this finding real?", and it answers in context instead of making you go and dig. That back-and-forth isn't a nicety, it's the design: Argus is meant to be argued with, not obeyed.&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%2Fa8m1fou1bt4bt0nb9k2e.webp" 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%2Fa8m1fou1bt4bt0nb9k2e.webp" alt="Argus posts a security review on a GitHub pull request, flagging hardcoded AWS credentials in config.py and a SQL injection in the /users endpoint, with two findings on the changed lines" width="800" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because it's also an MCP server, the AI tools you already use can consult it as a colleague rather than a black box. This is the part I reach for most: I point at something I just wrote and ask whether it's safe. And I want to be fair about the alternative, because the tools you already have are good at this. Claude and friends ship security skills, people write their own, and they work. What none of them has is your company: the other twelve repos, how your services talk to each other, which of your systems actually holds the data worth stealing. Ask Argus whether a pattern is a problem and it answers for your world instead of for security in the abstract, because the SOUL and the memory are right there. And it runs skills, which is how security methodology gets baked in. It ships with a handful: &lt;code&gt;pr-quick-check&lt;/code&gt; for a fast pass over a diff, &lt;code&gt;authz-audit&lt;/code&gt; for hunting authorization flaws, &lt;code&gt;secret-rotation-plan&lt;/code&gt; for finding committed secrets and deciding what to rotate first, and &lt;code&gt;threat-modeling&lt;/code&gt; for a STRIDE pass. You write your own the same way the built-ins are written, as a Markdown &lt;code&gt;SKILL.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Getting started is &lt;code&gt;./argus init&lt;/code&gt; to pick a provider and write your SOUL, &lt;code&gt;./argus doctor&lt;/code&gt; to check the scanners are wired up, &lt;code&gt;./argus codehost setup&lt;/code&gt; to connect GitHub, and plain &lt;code&gt;./argus&lt;/code&gt; to just chat with it. There's a container image on &lt;code&gt;ghcr.io&lt;/code&gt; with the scanners pre-bundled if you'd rather not install Semgrep and friends by hand.&lt;/p&gt;

&lt;p&gt;A word on which model runs all this, because it's a design choice and not an afterthought. Argus is built to run on the AI you already have and already trust, rather than the one a vendor picks for you. The provider layer sits behind an interface, and &lt;code&gt;argus init&lt;/code&gt; is where you choose. Today that choice is still narrow, with one provider implemented, and I won't dress that up as a feature. Support for any OpenAI-compatible endpoint is what I'm building right now, so depending on when you read this it may already be in. That's also the change that opens the door to local models, since most local runners speak the same API, and it matters for the teams I most want to reach: the ones who can't send their code to somebody else's API and don't have the budget to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it's going
&lt;/h2&gt;

&lt;p&gt;The idea list is longer than the changelog, which is the correct ratio for a v0.&lt;/p&gt;

&lt;p&gt;The near stuff: Slack as a first-class channel, so Argus really is the colleague you message. Local models. More skills.&lt;/p&gt;

&lt;p&gt;The stuff I'm actually excited about: service and code discovery, so Argus builds its own map of your systems instead of waiting to be told, and a view of how your projects connect, because that cross-project graph is where the non-obvious problems hide.&lt;/p&gt;

&lt;p&gt;But I want to be careful, because I've made this mistake before. With &lt;a href="https://dev.to/blog/i-built-a-tool-i-dont-use"&gt;Worky&lt;/a&gt; I built a showcase site and full docs for a tool with zero users, then watched it drift because there was no real usage telling me what to build next. A roadmap without users is fiction. So I'm not going to pretend I know exactly what Argus needs. The roadmap comes from whoever picks it up.&lt;/p&gt;

&lt;p&gt;The difference from Worky, the reason I'm not worried this one ends up on the same shelf, is that I use Argus. It has a mission I care about. This is the tool I want to keep alive, and I get a vote in what it becomes precisely because I'm one of its users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Come build in AI and security
&lt;/h2&gt;

&lt;p&gt;There's a bigger thing behind Argus, and it's the reason I'm writing this instead of just pushing a repo.&lt;/p&gt;

&lt;p&gt;AI and cybersecurity are converging fast, and most people are only watching one side of it. Back in June we ran a hackathon at RedCarbon: one day, tiny teams, AI pointed at security problems. I wrote about being on the jury in &lt;a href="https://dev.to/blog/i-built-a-tool-i-dont-use"&gt;the Worky retrospective&lt;/a&gt;, but the part I keep coming back to is the range. One team went after phishing aimed at non-technical family members. Another built for the SOC. Another attacked the security of AI models themselves. Same prompt, completely different directions, all of them real.&lt;/p&gt;

&lt;p&gt;Argus is just my direction, and it comes from something I actually live. I'm lucky here: I have security colleagues at RedCarbon who helped me shape it, pick the tools, and read what those tools are really saying. But they spend their days pointing our own AI agents at SOC problems, and none of them has time to sit behind our code's security posture every single day. So the developers have to, and Argus automates the exact work those colleagues would otherwise do by hand. And that job, it turns out, is increasingly buildable: models are getting good enough at security reasoning that the bottleneck is no longer the model, but whether someone shapes it into something trustworthy and useful.&lt;/p&gt;

&lt;p&gt;A while back I wrote that &lt;a href="https://dev.to/blog/ai-will-not-secure-your-codebase"&gt;AI will not secure your codebase&lt;/a&gt;, that it won't do the job for you but it can surface risk earlier, act as an early-warning system. Argus is me trying to make that concrete. Not a magic box that makes you secure, but a colleague who has read your whole codebase, remembers what you decided, and is awake at 2am when the PR lands.&lt;/p&gt;

&lt;p&gt;It's &lt;a href="https://github.com/argusappsec/argus" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;, Apache 2.0, v0.2 and honest about it. If you do security, or you just want the security colleague you don't currently have, clone it, break it, write a skill, and tell me how you use it. Real usage is the one thing that turns a fun experiment into a tool that matters, and I learned that the expensive way.&lt;/p&gt;

&lt;p&gt;Worky has two stars and I made my peace with that. Argus I'd actually like you to star, because this is the one I'm keeping. And if you've been sitting on your own idea at the intersection of AI and security, stop sitting on it. The water's warm.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;The 4.8 million figure is ISC2's estimate of the global cybersecurity workforce gap: the distance between the security professionals organizations say they need and the ones actually available to hire. Source: &lt;a href="https://www.isc2.org/Insights/2024/10/ISC2-2024-Cybersecurity-Workforce-Study" rel="noopener noreferrer"&gt;ISC2 2024 Cybersecurity Workforce Study&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>go</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Gemini 3.6 Flash: Google's Fastest Bet in a Crowded Race 🚀</title>
      <dc:creator>Domenico Tenace</dc:creator>
      <pubDate>Wed, 29 Jul 2026 08:33:15 +0000</pubDate>
      <link>https://dev.to/playfulprogramming/gemini-36-flash-googles-fastest-bet-in-a-crowded-race-2a77</link>
      <guid>https://dev.to/playfulprogramming/gemini-36-flash-googles-fastest-bet-in-a-crowded-race-2a77</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

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

&lt;p&gt;Google dropped Gemini 3.6 Flash on July 21, 2026, and it's worth talking about. Not because it's a revolution, but because it quietly improves on almost everything from 3.5 Flash while getting cheaper and faster at the same time. That's a combination you don't see often.&lt;/p&gt;

&lt;p&gt;Let me break down what actually changed, how it stacks up against the competition, and whether you should care.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  What Is Gemini 3.6 Flash? 🤔
&lt;/h2&gt;

&lt;p&gt;Gemini 3.6 Flash is Google's workhorse model for developers, the go-to for agentic workflows, coding tasks, long-document reasoning, and multimodal work. It's not Google's most capable model (that's still the upcoming 3.5 Pro and eventually Gemini 4), but it's the one most developers will actually use day to day.&lt;/p&gt;

&lt;p&gt;It was released alongside two companion models: Gemini 3.5 Flash-Lite, designed for high-throughput and low-latency tasks like agentic search and document processing, and the limited-pilot Gemini 3.5 Flash Cyber for governments and trusted partners.&lt;/p&gt;

&lt;p&gt;The model is available in Google AI Studio, the Gemini API (model ID &lt;code&gt;gemini-3.6-flash&lt;/code&gt;), the Gemini app, Antigravity, Android Studio, and Vertex AI.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Numbers: What Actually Changed 📊
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Benchmarks
&lt;/h3&gt;

&lt;p&gt;Gemini 3.6 Flash improves on Gemini 3.5 Flash across the board: coding went from 55.1% to 58.7% on SWE-Bench Pro, long-context retrieval went from roughly 27% to 54.0%, and computer use went from 78.4% to 83% on OSWorld Verified.&lt;/p&gt;

&lt;p&gt;Average task time fell from 2.7 minutes to 1.3 minutes, while estimated task cost declined about 18 percent. That's not a minor tweak. That's a meaningfully faster agent in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speed
&lt;/h3&gt;

&lt;p&gt;It runs at about 280 tokens per second, which makes it one of the faster models in its class for interactive use. For agentic workflows where you're chaining multiple LLM calls, this compounds fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pricing
&lt;/h3&gt;

&lt;p&gt;Gemini 3.6 Flash costs $1.50 per million input tokens and $7.50 per million output tokens. Input pricing is unchanged from Gemini 3.5 Flash, but output dropped from $9.00 to $7.50 per million tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context Window and Knowledge Cutoff
&lt;/h3&gt;

&lt;p&gt;The model runs a 1 million-token context window with a 64,000-token output cap, and its knowledge cutoff is March 2026, up from January 2025 on Gemini 3.5 Flash. That's a 14-month jump, which matters more than it sounds. A model that knows about recent framework releases, pricing changes, and API updates needs less web retrieval to stay useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Token Efficiency
&lt;/h3&gt;

&lt;p&gt;The model takes fewer reasoning steps and tool calls to complete multi-step workflows, which is a large part of why it uses roughly 17 percent fewer output tokens than its predecessor. You're paying for less and getting more done. That's the ideal direction.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Compares to the Competition 🥊
&lt;/h2&gt;

&lt;h3&gt;
  
  
  vs. Claude Sonnet 5
&lt;/h3&gt;

&lt;p&gt;Claude Sonnet 5 outperforms on 3 benchmarks including SWE-Bench Pro and GDPval-AA, while Gemini 3.6 Flash wins on OSWorld-Verified and CharXiv-R. Claude Sonnet 5 has a slight edge in raw benchmark performance.&lt;/p&gt;

&lt;p&gt;But the cost story is different. Gemini 3.6 Flash is 25% cheaper on both input and output compared to Claude Sonnet 5. At 100,000 tasks per month with 5,000 input and 10,000 output tokens each, that's $2,750/month in savings. For cost-sensitive workloads, Gemini is the clear winner.&lt;/p&gt;

&lt;p&gt;Gemini 3.6 Flash also runs at 304 tokens per second compared to roughly 180 for Claude Sonnet 5. For agent workflows where latency matters, the speed difference is noticeable.&lt;/p&gt;

&lt;p&gt;My take: if code quality at any cost is the priority, Claude Sonnet 5 still wins on most coding benchmarks. If you're building high-volume agent pipelines or cost is a real constraint, Gemini 3.6 Flash makes a strong argument.&lt;/p&gt;

&lt;h3&gt;
  
  
  vs. GPT-5.6 Luna
&lt;/h3&gt;

&lt;p&gt;GPT-5.6 Luna remains cheaper than Gemini 3.6 Flash at public API rates, but Artificial Analysis measured Gemini at 304 output tokens per second compared to 190 for Luna. This creates a practical trade-off: Luna offers lower token rates, while Gemini produces output faster.&lt;/p&gt;

&lt;p&gt;Independent testers report Gemini 3.6 Flash loses most coding benchmarks and GDPVal to Grok 4.5 and GPT-5.6 Luna. So if pure coding benchmark numbers are what you're optimizing for, Google isn't at the top of that list right now.&lt;/p&gt;

&lt;h3&gt;
  
  
  vs. Gemini 3.5 Flash
&lt;/h3&gt;

&lt;p&gt;Both Gemini 3.6 Flash and Gemini 3.5 Flash currently score 50 on the Artificial Analysis Intelligence Index. Existing Gemini 3.5 Flash users have the clearest migration case, especially when latency and output volume drive costs.&lt;/p&gt;

&lt;p&gt;In other words: same intelligence, faster, cheaper, fresher knowledge cutoff. The upgrade is a no-brainer if you're already on 3.5 Flash.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Missing: The Elephant in the Room 🐘
&lt;/h2&gt;

&lt;p&gt;The most notable absence in this announcement is Gemini 3.5 Pro. Google originally said at I/O in May that 3.5 Pro would launch the following month. That deadline passed without a public release. Bloomberg reported on July 16 that the model was running months behind schedule, with Google spending extra time on its coding capabilities.&lt;/p&gt;

&lt;p&gt;Google did not release Gemini 3.5 Pro in this announcement. The company said the Pro model fell short of internal expectations on coding and complex reasoning, so its broader release was delayed.&lt;/p&gt;

&lt;p&gt;This is the real story. A delayed Pro model means that right now, Google's top available offering in the Flash tier competes on price and speed but not on raw reasoning against Claude Opus or GPT-5.6. Google also confirmed that pre-training has begun on Gemini 4, which the team described as its most ambitious pre-training run yet. So the roadmap is clearly ambitious, but the present gap is real.&lt;/p&gt;




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

&lt;p&gt;Cost efficiency is excellent. Roughly half the output price of Claude Sonnet 5 at production scale is meaningful money. For startups or anyone watching API bills closely, this matters.&lt;/p&gt;

&lt;p&gt;Speed is genuinely impressive. At 304 tokens per second, it's 1.7x faster than Claude Sonnet 5 for agent workflows where latency matters.&lt;/p&gt;

&lt;p&gt;Multimodal support is broad. Gemini 3.6 Flash supports voice, video processing, images, PDFs, and audio as input. Claude Sonnet 5 doesn't support voice or video. For multimodal applications, Gemini is the stronger choice.&lt;/p&gt;

&lt;p&gt;The knowledge cutoff jump is significant. Going from January 2025 to March 2026 in one model generation means you're working with a model that actually knows about tools and frameworks released in the past year and a half.&lt;/p&gt;

&lt;p&gt;Token efficiency means lower costs in practice. Fewer output tokens for the same quality work, combined with a lower output price, compounds into real savings at volume.&lt;/p&gt;




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

&lt;p&gt;The Pro model is still missing. For developers who need the strongest possible reasoning and coding quality today, Google's delays leave a gap that Claude Opus and GPT-5.6 are filling.&lt;/p&gt;

&lt;p&gt;Raw coding benchmark scores lag behind top competitors. On the benchmarks that matter most to developers, Grok 4.5, GPT-5.6 Luna, and Claude Opus 4.8 still outperform Gemini 3.6 Flash. Speed and cost don't compensate for quality when you're debugging something complex.&lt;/p&gt;

&lt;p&gt;Intelligence Index hasn't moved. Gemini 3.6 Flash and Gemini 3.5 Flash both score 50 on the Artificial Analysis Intelligence Index. Faster and cheaper is great, but it's not smarter. If you were hoping for a reasoning leap, this isn't it.&lt;/p&gt;

&lt;p&gt;Gemini 4 uncertainty. Google teasing their next major model while 3.5 Pro is still delayed creates uncertainty for teams planning their model strategy. Do you build on 3.6 Flash now, or wait?&lt;/p&gt;




&lt;h2&gt;
  
  
  Who Should Use Gemini 3.6 Flash? 🎯
&lt;/h2&gt;

&lt;p&gt;Existing Gemini 3.5 Flash users. The migration is a pure upgrade: same intelligence, faster, cheaper, better knowledge cutoff. No reason not to switch.&lt;/p&gt;

&lt;p&gt;Cost-sensitive teams building agents. If you're running thousands or millions of API calls, the pricing advantage compounds fast. At scale, Gemini 3.6 Flash is the most efficient option in its class.&lt;/p&gt;

&lt;p&gt;Multimodal applications. Voice, video, images, audio all in one model with a 1M token context window. If your use case touches multiple modalities, this is the most capable option at this price point.&lt;/p&gt;

&lt;p&gt;Developers already in the Google ecosystem. Antigravity, AI Studio, Vertex AI, Android Studio. If you're already in these tools, the integration is seamless.&lt;/p&gt;

&lt;p&gt;Who should look elsewhere: if you need the strongest possible code generation today and budget is secondary, Claude Opus 4.8 or GPT-5.6 Luna are stronger choices on raw coding benchmarks.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Take 💭
&lt;/h2&gt;

&lt;p&gt;Gemini 3.6 Flash is a solid, honest upgrade. Google didn't promise a revolution and didn't deliver one. What they delivered is a faster, cheaper, more efficient model with a dramatically improved knowledge cutoff. For most agentic workloads, that's exactly what you need.&lt;/p&gt;

&lt;p&gt;The absence of 3.5 Pro is frustrating, and the coding benchmark gap against the top tier is real. But if you're building cost-sensitive pipelines, working with multimodal content, or already deep in Google's ecosystem, 3.6 Flash makes a compelling case.&lt;/p&gt;

&lt;p&gt;The question isn't "is this the best model?" It's "is this the best model for what I'm building?" For a lot of teams, the answer is yes.&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 Linktree to discover my links and to check out Domenico Tenace Open Labs for my open-source projects! 🫰🏻&lt;/p&gt;

&lt;p&gt;🌲 Linktree: &lt;a href="https://linktr.ee/domenicotenace" rel="noopener noreferrer"&gt;https://linktr.ee/domenicotenace&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;/p&gt;

&lt;p&gt;Follow me on dev.to for more articles 👇&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__985143"&gt;
    &lt;a href="/dvalin99" 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%2F985143%2Fc4c372a7-0b38-4f9e-b206-7ed65597ea31.png" alt="dvalin99 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="/dvalin99"&gt;Domenico Tenace&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/dvalin99"&gt;Passionate about the IT world and everything related to it ✌🏻
Open Source enthusiastic 🦠&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&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;&lt;a href="https://www.buymeacoffee.com/domenicotenace" rel="noopener noreferrer"&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%2Fb5vrzbmybu3q0sb5bzs1.png" alt="Buy Me A Coffee" width="545" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>google</category>
      <category>gemini</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
