<?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: Andrés Clúa</title>
    <description>The latest articles on DEV Community by Andrés Clúa (@andresclua).</description>
    <link>https://dev.to/andresclua</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%2Fuser%2Fprofile_image%2F812332%2Feb83ad9a-bb84-4902-98a8-1f92591b4ae6.png</url>
      <title>DEV Community: Andrés Clúa</title>
      <link>https://dev.to/andresclua</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/andresclua"/>
    <language>en</language>
    <item>
      <title>Stop Letting AI Decide Your JavaScript Architecture</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Thu, 05 Nov 2026 21:32:04 +0000</pubDate>
      <link>https://dev.to/andresclua/stop-letting-ai-decide-your-javascript-architecture-77o</link>
      <guid>https://dev.to/andresclua/stop-letting-ai-decide-your-javascript-architecture-77o</guid>
      <description>&lt;p&gt;&lt;em&gt;AI can generate working code in seconds. But working code and good architecture are two very different things.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We've reached a point where generating JavaScript is no longer the difficult part.&lt;/p&gt;

&lt;p&gt;You can ask an AI coding assistant to build a slider, a modal system, a router, or an entire application, and chances are you'll get something functional within minutes.&lt;/p&gt;

&lt;p&gt;That's impressive.&lt;/p&gt;

&lt;p&gt;But there's a question we don't ask often enough:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who is making the architectural decisions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When AI creates a class, introduces an abstraction, extends a base component, or splits something into five different modules, it's making decisions that will influence how your codebase evolves.&lt;/p&gt;

&lt;p&gt;And those decisions aren't necessarily wrong.&lt;/p&gt;

&lt;p&gt;They're just not necessarily right, either.&lt;/p&gt;

&lt;p&gt;The problem is that we tend to evaluate AI-generated code by asking one question:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Does it work?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I think we should be asking another:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will this still make sense when the requirements change?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's explore that with a small JavaScript experiment.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Experiment: One Slider, Three Architectures
&lt;/h2&gt;

&lt;p&gt;Imagine we're building a lightweight JavaScript slider.&lt;/p&gt;

&lt;p&gt;Nothing particularly complicated.&lt;/p&gt;

&lt;p&gt;Our initial requirements are simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Navigate between slides.&lt;/li&gt;
&lt;li&gt;Support autoplay.&lt;/li&gt;
&lt;li&gt;Emit events when slides change.&lt;/li&gt;
&lt;li&gt;Expose a small public API.&lt;/li&gt;
&lt;li&gt;Allow functionality to be extended later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We could give an AI coding assistant this prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build a lightweight JavaScript slider with navigation, autoplay, and change events. Use modern JavaScript.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;No architectural requirements. No preferences for composition, inheritance, or functional programming.&lt;/p&gt;

&lt;p&gt;Now here's the interesting part.&lt;/p&gt;

&lt;p&gt;There isn't one objectively correct implementation.&lt;/p&gt;

&lt;p&gt;An AI might generate a single class, a hierarchy of classes, several modules, or a collection of functions.&lt;/p&gt;

&lt;p&gt;All of them could work.&lt;/p&gt;

&lt;p&gt;So rather than pretending one particular architecture is what every AI would generate, let's compare three valid approaches and see what happens when we add complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Approach One: The Everything Class
&lt;/h2&gt;

&lt;p&gt;Our first implementation puts all the functionality inside a single class.&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;class&lt;/span&gt; &lt;span class="nc"&gt;Slider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;#index&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="nx"&gt;#timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;#listeners&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;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;slides&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;slides&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="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&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="nx"&gt;#index&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;#index&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;#emitChange&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;#listeners&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="nx"&gt;callback&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="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="nx"&gt;#listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;startAutoplay&lt;/span&gt;&lt;span class="p"&gt;()&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;stopAutoplay&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="nx"&gt;#timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&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="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;stopAutoplay&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearInterval&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="nx"&gt;#timer&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="nx"&gt;#timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;#emitChange&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &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;callback&lt;/span&gt; &lt;span class="k"&gt;of&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;#listeners&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;callback&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="nx"&gt;#index&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;For the sake of the example, assume the slider receives a non-empty array of slides.&lt;/p&gt;

&lt;p&gt;This implementation isn't terrible.&lt;/p&gt;

&lt;p&gt;Actually, for a small project, it's pretty reasonable.&lt;/p&gt;

&lt;p&gt;It has encapsulated state, a straightforward public API, and very little indirection.&lt;/p&gt;

&lt;p&gt;But notice what happens as the library grows.&lt;/p&gt;

&lt;p&gt;We want to introduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Previous-slide navigation.&lt;/li&gt;
&lt;li&gt;Looping and non-looping modes.&lt;/li&gt;
&lt;li&gt;Different autoplay behaviors.&lt;/li&gt;
&lt;li&gt;Keyboard controls.&lt;/li&gt;
&lt;li&gt;Custom transitions.&lt;/li&gt;
&lt;li&gt;Touch and swipe interactions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the &lt;code&gt;Slider&lt;/code&gt; class starts accumulating responsibilities.&lt;/p&gt;

&lt;p&gt;Navigation logic, event management, timing, input handling, and rendering all live in the same place.&lt;/p&gt;

&lt;p&gt;Every new feature potentially touches the same class.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem isn't that we used a class. It's that one class is becoming responsible for too many things.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Still, we shouldn't immediately rewrite it.&lt;/p&gt;

&lt;p&gt;Complexity needs to justify abstraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Approach Two: Inheritance
&lt;/h2&gt;

&lt;p&gt;One way to organize this functionality is through inheritance.&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;class&lt;/span&gt; &lt;span class="nc"&gt;BaseSlider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;slides&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;slides&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="nx"&gt;index&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="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&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="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;index&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AutoplaySlider&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseSlider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;startAutoplay&lt;/span&gt;&lt;span class="p"&gt;()&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="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;stopAutoplay&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearInterval&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="nx"&gt;timer&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AnimatedSlider&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AutoplaySlider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Run transition animation&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 looks nicely organized.&lt;/p&gt;

&lt;p&gt;We have a base class and specialized implementations.&lt;/p&gt;

&lt;p&gt;But something subtle has happened.&lt;/p&gt;

&lt;p&gt;An animated slider now inherits autoplay behavior, whether it needs it or not.&lt;/p&gt;

&lt;p&gt;What if we want an animated slider without autoplay?&lt;/p&gt;

&lt;p&gt;Or autoplay without animations?&lt;/p&gt;

&lt;p&gt;What if we want multiple interchangeable animation engines?&lt;/p&gt;

&lt;p&gt;We could introduce more subclasses, but eventually we risk creating an increasingly complicated hierarchy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BaseSlider
  └── AutoplaySlider
       └── AnimatedSlider
            └── TouchSlider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hierarchy is starting to describe combinations of features rather than genuine relationships between types.&lt;/p&gt;

&lt;p&gt;And that's where inheritance becomes uncomfortable.&lt;/p&gt;

&lt;p&gt;Inheritance is useful when one class is a meaningful specialization of another.&lt;/p&gt;

&lt;p&gt;It becomes problematic when we're using it simply to assemble features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not every reusable behavior needs to become a parent class.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Approach Three: Composition
&lt;/h2&gt;

&lt;p&gt;Let's try a different approach.&lt;/p&gt;

&lt;p&gt;Instead of asking what our slider should inherit, let's ask what responsibilities it needs.&lt;/p&gt;

&lt;p&gt;We identify three pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A navigation system.&lt;/li&gt;
&lt;li&gt;An autoplay controller.&lt;/li&gt;
&lt;li&gt;An event system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each can be implemented independently.&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;class&lt;/span&gt; &lt;span class="nc"&gt;Navigation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;#index&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="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&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="nx"&gt;#index&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;#index&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&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;#index&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Autoplay&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;#timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&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;stop&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="nx"&gt;#timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;interval&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearInterval&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="nx"&gt;#timer&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="nx"&gt;#timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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;Our main slider becomes responsible for coordinating these pieces.&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;class&lt;/span&gt; &lt;span class="nc"&gt;Slider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;#navigation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;#autoplay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;#listeners&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;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&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="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="nx"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Slider requires at least one slide&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;#navigation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;navigation&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;Navigation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="nx"&gt;#autoplay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;autoplay&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;Autoplay&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;next&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;index&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="nx"&gt;#navigation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;for &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;callback&lt;/span&gt; &lt;span class="k"&gt;of&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;#listeners&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&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;onChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;#listeners&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="nx"&gt;callback&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="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="nx"&gt;#listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;startAutoplay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;#autoplay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nx"&gt;interval&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;()&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="nx"&gt;#autoplay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&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="nx"&gt;#listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&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;Now navigation and autoplay are separate responsibilities.&lt;/p&gt;

&lt;p&gt;We can test them independently.&lt;/p&gt;

&lt;p&gt;We can substitute implementations without changing the slider's public API.&lt;/p&gt;

&lt;p&gt;And we haven't needed a complicated class hierarchy.&lt;/p&gt;

&lt;p&gt;That's composition.&lt;/p&gt;

&lt;p&gt;But there's something important worth mentioning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composition isn't automatically better just because we've created more classes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If our slider only needs ten lines of logic and will never grow, introducing multiple modules might make the code harder to understand.&lt;/p&gt;

&lt;p&gt;The goal isn't to maximize modularity.&lt;/p&gt;

&lt;p&gt;It's to introduce useful boundaries where they actually make sense.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Eight OOP Techniques That Solve Different Problems
&lt;/h2&gt;

&lt;p&gt;This is where I think AI-generated code becomes particularly interesting.&lt;/p&gt;

&lt;p&gt;We often discuss object-oriented programming as though it were one architectural approach.&lt;/p&gt;

&lt;p&gt;But OOP includes several techniques, each solving a different problem.&lt;/p&gt;

&lt;p&gt;Understanding their differences makes it much easier to decide when to use them.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Encapsulation: Protect Internal State
&lt;/h3&gt;

&lt;p&gt;Encapsulation controls access to an object's internal data and behavior.&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;class&lt;/span&gt; &lt;span class="nc"&gt;Slider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;#index&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="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&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="nx"&gt;#index&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="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;index&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;#index&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;Consumers can read the current index, but they can't directly modify the private field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; you want to enforce state invariants and prevent consumers from depending on implementation details.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Inheritance: Specialize Existing Behavior
&lt;/h3&gt;

&lt;p&gt;Inheritance allows a class to reuse and extend another class's behavior.&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;class&lt;/span&gt; &lt;span class="nc"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&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="s2"&gt;Destroyed&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Slider&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;Slider&lt;/code&gt; inherits &lt;code&gt;destroy()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; there's a genuine &lt;em&gt;is-a&lt;/em&gt; relationship and the subclass can safely fulfill the expectations of its parent.&lt;/p&gt;

&lt;p&gt;Avoid using inheritance purely to share unrelated functionality.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Composition: Combine Independent Behaviors
&lt;/h3&gt;

&lt;p&gt;Composition builds complex objects from smaller objects.&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;class&lt;/span&gt; &lt;span class="nc"&gt;Slider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;autoplay&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;navigation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;navigation&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="nx"&gt;autoplay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;autoplay&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;The slider &lt;em&gt;has a&lt;/em&gt; navigation system and &lt;em&gt;has an&lt;/em&gt; autoplay controller.&lt;/p&gt;

&lt;p&gt;It doesn't need to inherit from either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; responsibilities can evolve independently or features need to be combined in different ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Polymorphism: Interchange Implementations
&lt;/h3&gt;

&lt;p&gt;Polymorphism allows different objects to be used through the same expected interface.&lt;/p&gt;

&lt;p&gt;Consider a page-transition library.&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;class&lt;/span&gt; &lt;span class="nc"&gt;FadeTransition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&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="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;opacity&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;opacity&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;finished&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SlideTransition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&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="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;animate&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="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;translateX(100%)&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;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;translateX(0)&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="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;finished&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;Both expose the same &lt;code&gt;run()&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;Our transition controller doesn't need to care which implementation it's using.&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;class&lt;/span&gt; &lt;span class="nc"&gt;TransitionController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&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;&lt;strong&gt;Use it when:&lt;/strong&gt; different implementations share a meaningful contract.&lt;/p&gt;

&lt;p&gt;JavaScript doesn't require formal interfaces to achieve this. Objects can satisfy contracts through their behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Abstraction: Hide Unnecessary Complexity
&lt;/h3&gt;

&lt;p&gt;Abstraction exposes what a system does without forcing consumers to understand how it works.&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;slider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;slider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startAutoplay&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;slider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consumer doesn't need to know how timers, event listeners, or internal state are managed.&lt;/p&gt;

&lt;p&gt;That's the purpose of a good public API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; implementation details would otherwise leak into consuming code.&lt;/p&gt;

&lt;p&gt;Encapsulation and abstraction are related, but they're not identical: encapsulation controls access, while abstraction simplifies the model exposed to consumers.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Dependency Injection: Receive Dependencies
&lt;/h3&gt;

&lt;p&gt;Dependency injection means a class receives dependencies rather than constructing all of them internally.&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;class&lt;/span&gt; &lt;span class="nc"&gt;PageLoader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;fetcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fetcher&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;fetcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Request failed&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;Now we can provide another fetch implementation, including one for tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; dependencies need to be replaced, configured, or tested independently.&lt;/p&gt;

&lt;p&gt;You don't need a dependency-injection framework to use dependency injection.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Delegation: Let the Right Object Do the Work
&lt;/h3&gt;

&lt;p&gt;Delegation is about forwarding responsibility to another object.&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;class&lt;/span&gt; &lt;span class="nc"&gt;Slider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;navigation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;next&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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;The public API belongs to &lt;code&gt;Slider&lt;/code&gt;, but navigation logic lives in &lt;code&gt;Navigation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; a class should coordinate behavior without implementing every operation itself.&lt;/p&gt;

&lt;p&gt;Delegation and composition often work together, but they describe different things.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Mixins: Share Behavior Across Classes
&lt;/h3&gt;

&lt;p&gt;Mixins allow behavior to be added to multiple classes without creating a parent-child relationship.&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;EventEmitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;emit&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;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;event&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Slider&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Modal&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Slider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EventEmitter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EventEmitter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both classes gain an &lt;code&gt;emit()&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; sharing behavior this way is appropriate and the resulting method ownership remains clear.&lt;/p&gt;

&lt;p&gt;Personally, I'd be careful with mixins. They can introduce naming collisions and make it difficult to understand where behavior originates.&lt;/p&gt;

&lt;p&gt;In many cases, an explicit composed object is easier to maintain.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. The Real Test: Change the Requirements
&lt;/h2&gt;

&lt;p&gt;Let's return to our slider.&lt;/p&gt;

&lt;p&gt;Imagine a new requirement arrives:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We need to support different autoplay strategies. Some sliders should advance at a fixed interval, while others should wait until the current animation finishes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is where architectural decisions become visible.&lt;/p&gt;

&lt;p&gt;With the everything class, we might add conditionals to the existing timer logic.&lt;/p&gt;

&lt;p&gt;With the inheritance-based architecture, we might introduce more subclasses or override existing methods.&lt;/p&gt;

&lt;p&gt;With composition, we can introduce another autoplay implementation.&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;class&lt;/span&gt; &lt;span class="nc"&gt;AnimationAwareAutoplay&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;#running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;advance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;#running&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="k"&gt;while &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="nx"&gt;#running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;#running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&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;advance&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;stop&lt;/span&gt;&lt;span class="p"&gt;()&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="nx"&gt;#running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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;For simplicity, this example shows the scheduling mechanism; a production implementation would also need cancellation and lifecycle handling for pending waits.&lt;/p&gt;

&lt;p&gt;The important part is that we can replace the autoplay behavior without rewriting the slider itself.&lt;/p&gt;

&lt;p&gt;That's a meaningful benefit of composition.&lt;/p&gt;

&lt;p&gt;But here's the catch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The best architecture isn't necessarily the one with the most flexibility. It's the one with the appropriate amount of flexibility.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If we never need multiple autoplay strategies, a separate abstraction might not be worth maintaining.&lt;/p&gt;

&lt;p&gt;This is why architectural decisions should be evaluated against real requirements, not just theoretical extensibility.&lt;/p&gt;

&lt;p&gt;And it's exactly the kind of reasoning I want an AI coding assistant to perform.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The Instructions I Give AI Before It Writes JavaScript
&lt;/h2&gt;

&lt;p&gt;Instead of asking AI to "write clean, scalable, maintainable code," I'd rather give it specific architectural guidance.&lt;/p&gt;

&lt;p&gt;Here's a reusable set of instructions for a &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, or similar project configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## JavaScript Architecture Guidelines&lt;/span&gt;

&lt;span class="gu"&gt;### Core principles&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Prefer composition over inheritance.
&lt;span class="p"&gt;-&lt;/span&gt; Favor simple solutions over speculative abstractions.
&lt;span class="p"&gt;-&lt;/span&gt; Keep responsibilities focused and explicit.
&lt;span class="p"&gt;-&lt;/span&gt; Protect internal state through encapsulation.
&lt;span class="p"&gt;-&lt;/span&gt; Design small, predictable public APIs.
&lt;span class="p"&gt;-&lt;/span&gt; Follow KISS, DRY, and YAGNI.

&lt;span class="gu"&gt;### Classes and functions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Use classes when managing persistent state,
  identity, or an object lifecycle.
&lt;span class="p"&gt;-&lt;/span&gt; Prefer functions for stateless transformations.
&lt;span class="p"&gt;-&lt;/span&gt; Avoid classes that only group unrelated helpers.
&lt;span class="p"&gt;-&lt;/span&gt; Avoid unnecessary inheritance hierarchies.

&lt;span class="gu"&gt;### Dependencies&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Use dependency injection when dependencies
  need to be configurable or replaceable.
&lt;span class="p"&gt;-&lt;/span&gt; Prefer explicit dependencies.
&lt;span class="p"&gt;-&lt;/span&gt; Avoid introducing containers or frameworks
  without a concrete requirement.

&lt;span class="gu"&gt;### Extensibility&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Use polymorphism when multiple implementations
  share a meaningful contract.
&lt;span class="p"&gt;-&lt;/span&gt; Prefer delegation over duplicating responsibilities.
&lt;span class="p"&gt;-&lt;/span&gt; Avoid premature design patterns.
&lt;span class="p"&gt;-&lt;/span&gt; Do not create interfaces or abstract layers
  for hypothetical future use cases.

&lt;span class="gu"&gt;### Before introducing an abstraction&lt;/span&gt;
Ask:
&lt;span class="p"&gt;1.&lt;/span&gt; What problem does this abstraction solve?
&lt;span class="p"&gt;2.&lt;/span&gt; Do multiple implementations actually exist?
&lt;span class="p"&gt;3.&lt;/span&gt; Does it reduce coupling or just move code?
&lt;span class="p"&gt;4.&lt;/span&gt; Would a simple function be clearer?
&lt;span class="p"&gt;5.&lt;/span&gt; Is the added complexity justified?

&lt;span class="gu"&gt;### General rule&lt;/span&gt;
Choose the simplest architecture that satisfies
current requirements while allowing reasonable
future changes.

Do not overengineer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These guidelines aren't meant to replace architectural judgment.&lt;/p&gt;

&lt;p&gt;They're meant to establish a useful default.&lt;/p&gt;

&lt;p&gt;And defaults matter.&lt;/p&gt;

&lt;p&gt;Especially when a coding assistant can generate hundreds of lines before you've had time to question the first architectural decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. What I'd Actually Evaluate in AI-Generated Code
&lt;/h2&gt;

&lt;p&gt;I wouldn't judge an implementation by counting classes.&lt;/p&gt;

&lt;p&gt;Or measuring how many design patterns it uses.&lt;/p&gt;

&lt;p&gt;Or checking whether it follows every SOLID principle.&lt;/p&gt;

&lt;p&gt;Instead, I'd ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I understand it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Does the code communicate its responsibilities clearly, or do I need to jump between six abstractions to understand a simple operation?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I change it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What happens when a new feature requires modifying an existing behavior?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I test it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Can I test navigation without starting timers or interacting with the DOM?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are the boundaries meaningful?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Are classes separated because they genuinely represent different responsibilities, or simply because someone decided smaller files are always better?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the complexity justified?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Would a function solve the same problem more effectively?&lt;/p&gt;

&lt;p&gt;These questions apply to code written by humans and AI alike.&lt;/p&gt;

&lt;p&gt;The difference is that AI makes it easier to generate complex structures without experiencing the effort of writing them.&lt;/p&gt;

&lt;p&gt;And that can make unnecessary complexity harder to notice.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Composition Isn't a Religion
&lt;/h2&gt;

&lt;p&gt;There's a danger in turning any architectural recommendation into an absolute rule.&lt;/p&gt;

&lt;p&gt;"Always use composition" isn't much better than "always use classes."&lt;/p&gt;

&lt;p&gt;Inheritance has legitimate uses.&lt;/p&gt;

&lt;p&gt;A single class can be the right solution.&lt;/p&gt;

&lt;p&gt;Functional programming can be more appropriate than OOP.&lt;/p&gt;

&lt;p&gt;And sometimes a straightforward procedural implementation is all we need.&lt;/p&gt;

&lt;p&gt;JavaScript is a multi-paradigm language.&lt;/p&gt;

&lt;p&gt;That's one of its strengths.&lt;/p&gt;

&lt;p&gt;We don't have to commit to one technique for every problem.&lt;/p&gt;

&lt;p&gt;A useful default might be:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Functions for stateless logic. Classes for stateful objects. Composition for assembling independent behaviors.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But even that is a guideline, not a law.&lt;/p&gt;

&lt;p&gt;The objective is to make deliberate decisions instead of following patterns automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Stop Outsourcing Architectural Judgment
&lt;/h2&gt;

&lt;p&gt;AI coding assistants are becoming remarkably good at producing working implementations.&lt;/p&gt;

&lt;p&gt;And I think that's great.&lt;/p&gt;

&lt;p&gt;But generating code and designing software aren't quite the same thing.&lt;/p&gt;

&lt;p&gt;Architecture involves understanding constraints, anticipating likely changes, identifying meaningful boundaries, and deciding which complexity is worth introducing.&lt;/p&gt;

&lt;p&gt;Those decisions require context.&lt;/p&gt;

&lt;p&gt;Context that isn't always present in a prompt.&lt;/p&gt;

&lt;p&gt;So the next time an AI assistant generates a perfectly functional class hierarchy, don't immediately accept it.&lt;/p&gt;

&lt;p&gt;Ask why inheritance was necessary.&lt;/p&gt;

&lt;p&gt;Ask whether composition would simplify it.&lt;/p&gt;

&lt;p&gt;Ask whether the abstraction solves a real problem.&lt;/p&gt;

&lt;p&gt;And, perhaps most importantly, ask whether the entire thing could have been a function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI doesn't need more design patterns. It needs better reasons to use them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And until it has enough context to make those decisions reliably, architectural judgment should remain part of our job.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>designpatterns</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Replaced Math.random() With CSS. Here Is What Works.</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Wed, 28 Oct 2026 21:27:25 +0000</pubDate>
      <link>https://dev.to/andresclua/i-replaced-mathrandom-with-css-here-is-what-works-1dp4</link>
      <guid>https://dev.to/andresclua/i-replaced-mathrandom-with-css-here-is-what-works-1dp4</guid>
      <description>&lt;h2&gt;
  
  
  Why I care about this
&lt;/h2&gt;

&lt;p&gt;I build creative sites for a living. Almost every project has some small random detail. Cards that sit a bit crooked. Particles that start at different times. A hue that changes on every load.&lt;/p&gt;

&lt;p&gt;For years, that meant the same ten lines of JavaScript. Loop over the elements, call &lt;code&gt;Math.random()&lt;/code&gt;, write a CSS variable. It works. But it runs after the HTML loads, it needs a script, and it fights with my server rendering.&lt;/p&gt;

&lt;p&gt;On October 20, Google shipped &lt;a href="https://gigazine.net/gsc_news/en/20261021-google-chrome-156/" rel="noopener noreferrer"&gt;Chrome 156&lt;/a&gt;. It includes the CSS &lt;code&gt;random()&lt;/code&gt; function. Safari already had it. So I spent a few evenings replacing my JavaScript with plain CSS.&lt;/p&gt;

&lt;p&gt;This post is what I learned. What works, what does not, and the one thing that will confuse you if you read older tutorials.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic syntax
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;random()&lt;/code&gt; takes a minimum and a maximum. It returns a value between them. You can add a third value, a step, to snap the result to fixed increments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-4deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c"&gt;/* any angle in the range */&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;320px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;40px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c"&gt;/* 200, 240, 280 or 320px */&lt;/span&gt;
  &lt;span class="nl"&gt;animation-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2s&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;Three rules to remember, all from &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/random" rel="noopener noreferrer"&gt;MDN&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Min, max and step must be the same data type. Lengths with lengths, angles with angles. Units can differ (&lt;code&gt;1em&lt;/code&gt; and &lt;code&gt;40vw&lt;/code&gt; is fine).&lt;/li&gt;
&lt;li&gt;The range is inclusive on both ends.&lt;/li&gt;
&lt;li&gt;If max is smaller than min, you get the min value. No error, no warning.
It works anywhere CSS accepts a length, angle, time, number, percentage, integer, frequency or resolution. That covers most of what I randomize.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The part that confused me: who shares the value
&lt;/h2&gt;

&lt;p&gt;By default, every &lt;code&gt;random()&lt;/code&gt; call is independent. Every element, every property, every position in a shorthand gets its own value. Most of the time that is what you want.&lt;/p&gt;

&lt;p&gt;But sometimes you need a square. Width and height must be random, but equal. That is what the optional first argument is for. MDN calls it the &lt;code&gt;&amp;lt;random-key&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Same value across elements?&lt;/th&gt;
&lt;th&gt;Same value across properties?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;auto&lt;/code&gt; (default)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;element-scoped&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;property-scoped&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--my-name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--my-name element-scoped&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, for calls with that name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fixed 0.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;You pick the seed (0 to 1)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So a set of random squares, each one a different size, looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.tile&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--tile-size&lt;/span&gt; &lt;span class="n"&gt;element-scoped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;60px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;140px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--tile-size&lt;/span&gt; &lt;span class="n"&gt;element-scoped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;60px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;140px&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;Here is the trap. Older articles, including the &lt;a href="https://webkit.org/blog/17285/rolling-the-dice-with-css-random/" rel="noopener noreferrer"&gt;WebKit announcement&lt;/a&gt; and &lt;a href="https://frontendmasters.com/blog/very-early-playing-with-random-in-css/" rel="noopener noreferrer"&gt;early Frontend Masters tests&lt;/a&gt;, use a keyword called &lt;code&gt;element-shared&lt;/code&gt;. The spec has moved on. The current names are &lt;code&gt;element-scoped&lt;/code&gt;, &lt;code&gt;property-scoped&lt;/code&gt; and &lt;code&gt;property-index-scoped&lt;/code&gt;. If you copy a snippet from 2025 and nothing happens, check this first.&lt;/p&gt;

&lt;p&gt;One more gotcha. A normal custom property does not store a random result. &lt;code&gt;--size: random(1px, 100px)&lt;/code&gt; is text substitution, so every &lt;code&gt;var(--size)&lt;/code&gt; rolls again. If you want one stored value, register the property with &lt;code&gt;@property&lt;/code&gt;. MDN &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/random#custom_properties" rel="noopener noreferrer"&gt;explains this&lt;/a&gt; with a short example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1: a messy stack of cards
&lt;/h2&gt;

&lt;p&gt;This is a pattern I use a lot. A grid of photos or cards that look like they were dropped on a table. Each one tilted and shifted a little.&lt;/p&gt;

&lt;p&gt;Here is the JavaScript version I have shipped many times:&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;cards&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;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.card&lt;/span&gt;&lt;span class="dl"&gt;'&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;randomBetween&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;max&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="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;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;card&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;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--tilt&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;randomBetween&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;deg`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--shift-x&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;randomBetween&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--shift-y&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;randomBetween&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--delay&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;randomBetween&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="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;s`&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--tilt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--shift-x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--shift-y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt; &lt;span class="m"&gt;0.5s&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--delay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;both&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;It works. But the cards render straight, then jump when the script runs. With server rendering you also get a mismatch between server and client values.&lt;/p&gt;

&lt;p&gt;Here is the CSS version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-6deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;6deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-12px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-12px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt; &lt;span class="m"&gt;0.5s&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.6s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;both&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;No script. No flash. Each card gets its own values because the default key is &lt;code&gt;auto&lt;/code&gt;. The two values inside &lt;code&gt;translate&lt;/code&gt; are also independent, because &lt;code&gt;auto&lt;/code&gt; separates calls by their position in the declaration.&lt;/p&gt;

&lt;p&gt;If you add the step argument, you get a more designed feel. Tilts of exactly -6, -3, 0, 3 or 6 degrees look less random and more intentional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-6deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;6deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3deg&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;h2&gt;
  
  
  Example 2: a palette that changes on every load
&lt;/h2&gt;

&lt;p&gt;Pure random color looks bad. Fast. What I usually want is one random hue for the whole page, and small variations per element. That is where the keys pay off.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.blob&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* One hue for the whole page: a named key alone is global */&lt;/span&gt;
  &lt;span class="py"&gt;--hue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--page-hue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;360&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c"&gt;/* Lightness varies per element, in steps of 10 */&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;hsl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--hue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="m"&gt;70%&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;35%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;75%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10%&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="c"&gt;/* Each blob is a circle of its own size */&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--blob-size&lt;/span&gt; &lt;span class="n"&gt;element-scoped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;80px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;240px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--blob-size&lt;/span&gt; &lt;span class="n"&gt;element-scoped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;80px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;240px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c"&gt;/* Scatter them across the hero */&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;90%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;90%&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;Every load gives you a new mood. But it always looks like one palette, because every blob uses the same &lt;code&gt;--page-hue&lt;/code&gt; seed.&lt;/p&gt;

&lt;p&gt;The JavaScript version needed one value for the page and one loop for the elements. Something like this:&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;hue&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;floor&lt;/span&gt;&lt;span class="p"&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;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;361&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;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--hue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hue&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="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.blob&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;blob&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;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&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;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;160&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;light&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;35&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;floor&lt;/span&gt;&lt;span class="p"&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;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--size&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--light&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;light&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="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--top&lt;/span&gt;&lt;span class="dl"&gt;'&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="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;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;90&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="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--left&lt;/span&gt;&lt;span class="dl"&gt;'&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="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;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;90&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten lines of script turned into a few declarations. For a hero section, I will take that trade every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping it today: a fallback that only runs when needed
&lt;/h2&gt;

&lt;p&gt;Firefox does not support &lt;code&gt;random()&lt;/code&gt; yet. &lt;a href="https://daily.dev/posts/embracing-playful-imperfection-with-the-css-random-function-smashing-magazine-cwvipesbh" rel="noopener noreferrer"&gt;Smashing Magazine&lt;/a&gt; reports positive signals from Mozilla, but no release. So for client work, I treat it as progressive enhancement.&lt;/p&gt;

&lt;p&gt;Step one: write a calm default first, then add the random layer inside a feature query. MDN uses &lt;code&gt;order: random(1, 2)&lt;/code&gt; as the test, which is a nice trick.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--tilt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@supports&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-6deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;6deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3deg&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;Step two: if the random effect really matters to the design, add a tiny script that only runs in browsers without support.&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;supportsRandom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;CSS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;supports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order&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;random(1, 2)&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;supportsRandom&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;steps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&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="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;card&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;tilt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&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;floor&lt;/span&gt;&lt;span class="p"&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;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
    &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--tilt&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;tilt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;deg`&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;Chrome and Safari users get pure CSS. Firefox users get the old behavior. Nobody gets a broken page.&lt;/p&gt;

&lt;p&gt;If you want one code path for every browser, &lt;a href="https://css-tricks.com/css-random-function-polyfill/" rel="noopener noreferrer"&gt;CSS-Tricks covered a polyfill&lt;/a&gt; that resolves &lt;code&gt;random()&lt;/code&gt; with JavaScript. I have not used it in production, so test it before you trust it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest caveats
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;random()&lt;/code&gt; is great. It is not a full replacement for &lt;code&gt;Math.random()&lt;/code&gt;. Here is where JavaScript still wins, or where you need to be careful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You cannot re-roll on demand.&lt;/strong&gt; The value is picked when styles are computed. There is no "shuffle" button. If a user action should create new values, you still need JavaScript.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hover states may not change.&lt;/strong&gt; With the default &lt;code&gt;auto&lt;/code&gt; key, the browser builds the seed name from the property and position. So &lt;code&gt;width: random(100px, 200px)&lt;/code&gt; on &lt;code&gt;.foo&lt;/code&gt; and on &lt;code&gt;.foo:hover&lt;/code&gt; share a seed, and nothing changes on hover. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/random#automatic_behavior" rel="noopener noreferrer"&gt;MDN shows this case&lt;/a&gt;. Use a different named key for the hover rule if you want a new value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You cannot read the value easily.&lt;/strong&gt; If your WebGL scene or GSAP timeline needs to know the exact random number, keep it in JavaScript. Syncing CSS randomness into a canvas is more work than it saves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contrast is your problem now.&lt;/strong&gt; A random text color can land on an unreadable value. MDN warns about this directly. Randomize decoration, not readable content, or keep the range tight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Motion still needs an off switch.&lt;/strong&gt; Random delays and positions are often used in animation. Wrap that animation in &lt;code&gt;prefers-reduced-motion&lt;/code&gt; like any other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support is still partial.&lt;/strong&gt; Chrome 156 and Safari support it. Firefox does not, as of today. MDN still marks it as &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/random" rel="noopener noreferrer"&gt;limited availability&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do this week
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open Chrome 156 or Safari and paste the card example into a CodePen. It takes two minutes and you will understand the keys faster than by reading.&lt;/li&gt;
&lt;li&gt;Search your codebase for &lt;code&gt;Math.random()&lt;/code&gt; used only to set styles. Those are your candidates.&lt;/li&gt;
&lt;li&gt;Pick one decorative effect and move it to CSS behind &lt;code&gt;@supports (order: random(1, 2))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Keep a calm default for Firefox. Add the JavaScript fallback only if the effect is part of the design.&lt;/li&gt;
&lt;li&gt;Ignore any snippet that uses &lt;code&gt;element-shared&lt;/code&gt;. Translate it to &lt;code&gt;element-scoped&lt;/code&gt; or a named key.&lt;/li&gt;
&lt;li&gt;Leave anything interactive, readable or synced with canvas in JavaScript.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>css</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Tokens Are Not the Metric</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Tue, 06 Oct 2026 15:55:40 +0000</pubDate>
      <link>https://dev.to/andresclua/tokens-are-not-the-metric-46pm</link>
      <guid>https://dev.to/andresclua/tokens-are-not-the-metric-46pm</guid>
      <description>&lt;p&gt;When developers start using AI seriously, one metric appears almost immediately: &lt;strong&gt;token usage&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many tokens did this agent consume?&lt;/li&gt;
&lt;li&gt;How much did this coding session cost?&lt;/li&gt;
&lt;li&gt;Can we reduce the context window?&lt;/li&gt;
&lt;li&gt;Can we use a cheaper model?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are reasonable questions. But they can also lead us toward the wrong optimization.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The goal should not be to use fewer tokens.&lt;br&gt;
The goal should be to &lt;strong&gt;waste&lt;/strong&gt; fewer tokens.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is a big difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  A million tokens can be cheap
&lt;/h2&gt;

&lt;p&gt;Imagine two AI agents. These are hypothetical examples, not benchmark results.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Agent A&lt;/th&gt;
&lt;th&gt;Agent B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tokens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;50 million&lt;/td&gt;
&lt;td&gt;5 million&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What it does&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Implements a feature&lt;/td&gt;
&lt;td&gt;Repeatedly scans the same repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Writes tests&lt;/td&gt;
&lt;td&gt;Retries failed tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Fixes regressions&lt;/td&gt;
&lt;td&gt;Summarizes information nobody reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Opens a pull request&lt;/td&gt;
&lt;td&gt;Runs every 30 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Generates documentation&lt;/td&gt;
&lt;td&gt;Produces no meaningful output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Result&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Saves an engineer two days of work&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Which one is expensive?&lt;/p&gt;

&lt;p&gt;Looking only at token consumption, Agent A looks ten times worse. Looking at actual value, Agent B may be the worse investment.&lt;/p&gt;

&lt;p&gt;This is why I increasingly think about AI usage in three buckets: &lt;strong&gt;learning&lt;/strong&gt;, &lt;strong&gt;production&lt;/strong&gt;, and &lt;strong&gt;spin&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Learning tokens
&lt;/h2&gt;

&lt;p&gt;These are the tokens we spend figuring things out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trying a new coding agent&lt;/li&gt;
&lt;li&gt;Experimenting with MCP&lt;/li&gt;
&lt;li&gt;Building a better system prompt&lt;/li&gt;
&lt;li&gt;Testing whether an agent can translate a Figma design into production code&lt;/li&gt;
&lt;li&gt;Letting a model explore an unfamiliar codebase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of this work will fail. That's fine.&lt;/p&gt;

&lt;p&gt;In traditional software development, we don't expect every minute spent by an engineer to produce production code. We research. We prototype. We read documentation. We throw things away.&lt;/p&gt;

&lt;p&gt;AI systems need the same space. Trying to aggressively eliminate this type of token usage can actually slow down adoption.&lt;/p&gt;

&lt;p&gt;The important question isn't &lt;em&gt;"How many tokens did we spend?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It's &lt;em&gt;"What did we learn from spending them?"&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Production tokens
&lt;/h2&gt;

&lt;p&gt;These are the easiest tokens to justify. They produce something useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A pull request&lt;/li&gt;
&lt;li&gt;A migration&lt;/li&gt;
&lt;li&gt;A test suite&lt;/li&gt;
&lt;li&gt;A component&lt;/li&gt;
&lt;li&gt;A technical document&lt;/li&gt;
&lt;li&gt;A content update&lt;/li&gt;
&lt;li&gt;A bug fix&lt;/li&gt;
&lt;li&gt;An accessibility audit&lt;/li&gt;
&lt;li&gt;A production deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once AI moves beyond autocomplete and starts operating as an agent, token consumption becomes closer to compute.&lt;/p&gt;

&lt;p&gt;You wouldn't measure a CI pipeline only by how many CPU seconds it consumed. You care about what the pipeline accomplished. AI should be evaluated the same way.&lt;/p&gt;

&lt;p&gt;A useful metric might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI cost / completed task
AI cost / pull request merged
AI cost / engineering hour saved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suddenly, token usage becomes much more meaningful.&lt;/p&gt;

&lt;p&gt;These metrics still need context: task complexity, quality, review time, and downstream rework. A merged PR is useful evidence, but it isn't proof that the work saved time.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Spin
&lt;/h2&gt;

&lt;p&gt;This is where the real problem lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spin is AI activity that looks productive but produces little or no value.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some common examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agents repeatedly reading the same files without a reason&lt;/li&gt;
&lt;li&gt;Enormous context windows being resent unnecessarily&lt;/li&gt;
&lt;li&gt;Automated jobs running far more often than needed&lt;/li&gt;
&lt;li&gt;Multiple agents duplicating the same research&lt;/li&gt;
&lt;li&gt;Retry loops without proper termination conditions&lt;/li&gt;
&lt;li&gt;Long reasoning chains for trivial operations&lt;/li&gt;
&lt;li&gt;Generated reports nobody reads&lt;/li&gt;
&lt;li&gt;Background agents running simply because they can&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the AI equivalent of an infinite loop with a cloud bill attached.&lt;/p&gt;

&lt;p&gt;And as AI systems become more autonomous, spin becomes much easier to create. A developer using an AI assistant manually will eventually notice that something isn't working. A scheduled agent can waste money quietly for weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "make it cheaper"
&lt;/h2&gt;

&lt;p&gt;When AI costs start increasing, the first reaction is usually: &lt;em&gt;use a cheaper model&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Sometimes that's correct. But it's often treating the symptom instead of the cause.&lt;/p&gt;

&lt;p&gt;Suppose an agent performs a task 48 times per day when it only needs to run twice. Switching to a model that costs half as much sounds like an optimization. But reducing the execution frequency would cut the execution cost by roughly &lt;strong&gt;96%&lt;/strong&gt;, assuming the cost per run stays the same.&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;costPerRun&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cheaperModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;costPerRun&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 24&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fewerRuns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;costPerRun&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// 2&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;savingsCheaperModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;cheaperModel&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 0.50&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;savingsFewerRuns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;fewerRuns&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// ~0.96&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Architecture can matter more than model pricing.&lt;/p&gt;

&lt;p&gt;Before changing models, I would ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this agent need to run this often?&lt;/li&gt;
&lt;li&gt;Does it need the entire repository as context?&lt;/li&gt;
&lt;li&gt;Can it retrieve only the relevant files?&lt;/li&gt;
&lt;li&gt;Does every step need the most capable model?&lt;/li&gt;
&lt;li&gt;Can deterministic code replace part of the workflow?&lt;/li&gt;
&lt;li&gt;Does the output lead to an actual action?&lt;/li&gt;
&lt;li&gt;Does the agent know when to stop?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These questions can produce much larger savings.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI efficiency is becoming an engineering discipline
&lt;/h2&gt;

&lt;p&gt;We already have mature ways of measuring traditional software systems: latency, CPU, memory, error rates, throughput, availability.&lt;/p&gt;

&lt;p&gt;AI systems need their own operational metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost per successful task
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;total AI cost / successfully completed tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Include failed runs in the numerator, and define success before running the workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retry rate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;executions requiring at least one retry / total executions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A high retry rate can reveal brittle prompts, bad tools, or poor agent architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Human intervention rate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tasks requiring human correction / total tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Track correction time too. A quick approval and a two-hour repair have very different costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Useful output rate
&lt;/h3&gt;

&lt;p&gt;How often does an agent produce something that actually gets used?&lt;/p&gt;

&lt;h3&gt;
  
  
  Context efficiency
&lt;/h3&gt;

&lt;p&gt;How much of the provided context was actually relevant? This is harder to measure directly, but comparing focused retrieval against larger context inputs can help.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent idle activity
&lt;/h3&gt;

&lt;p&gt;How often does the system execute when nothing meaningful changed?&lt;/p&gt;

&lt;p&gt;These are more useful indicators of AI efficiency than raw token counts alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheapest token is not always the best token
&lt;/h2&gt;

&lt;p&gt;Developers have spent decades learning that premature optimization can make systems worse. AI is no different.&lt;/p&gt;

&lt;p&gt;If a stronger model uses 30% more tokens but solves the problem correctly on the first attempt, it may be cheaper overall than a smaller model requiring five retries. The result depends on pricing and the work each attempt performs.&lt;/p&gt;

&lt;p&gt;Likewise, giving an agent more context can sometimes reduce total cost if it prevents it from exploring blindly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token efficiency is not the same thing as token minimization.&lt;/strong&gt; The objective is productive computation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Think in terms of return
&lt;/h2&gt;

&lt;p&gt;A useful starting point is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;value-to-cost ratio = value produced / total workflow cost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total workflow cost should include AI execution, tools, human review, and rework. Estimating the value produced is harder, but ignoring it doesn't make the cost metric more useful.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;$20&lt;/strong&gt; agent run that saves four hours of engineering time is probably excellent.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;$0.20&lt;/strong&gt; automated task that runs thousands of times without producing anything useful is not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As AI becomes embedded in engineering workflows, teams will need to become comfortable with this distinction. Otherwise, we risk optimizing systems for impressive-looking token dashboards rather than actual productivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real optimization
&lt;/h2&gt;

&lt;p&gt;So when your AI bill starts growing, don't immediately ask &lt;em&gt;"How do we use fewer tokens?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which spending produces useful work, which helps us learn, and which keeps the system busy without moving anything forward?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is how I would start:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick one workflow.&lt;/li&gt;
&lt;li&gt;Define what success looks like.&lt;/li&gt;
&lt;li&gt;Record the full cost of getting there.&lt;/li&gt;
&lt;li&gt;Check whether anyone actually uses the output.&lt;/li&gt;
&lt;li&gt;Remove unnecessary runs, bound retries, and make the stopping condition explicit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Protect experiments that teach you something. Improve workflows that deliver useful work. Shut down activity that does neither.&lt;/p&gt;

&lt;p&gt;That's a much better optimization target than a smaller number on a token dashboard.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Inspired by &lt;a href="https://dev.toLINK_AQUI"&gt;The Token Gym&lt;/a&gt; and its distinction between learning, shipped work, and spin. The agent scenarios and workflow examples above are illustrative.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What do you measure in your AI workflows today: token consumption, cost per task, or whether the output actually gets used?&lt;/strong&gt; Let me know in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>You Don't Need an LLM for Every Decision</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Wed, 23 Sep 2026 09:38:43 +0000</pubDate>
      <link>https://dev.to/andresclua/you-dont-need-an-llm-for-every-decision-2fj0</link>
      <guid>https://dev.to/andresclua/you-dont-need-an-llm-for-every-decision-2fj0</guid>
      <description>&lt;p&gt;Most of the AI calls in my code never needed words. They needed a choice.&lt;/p&gt;

&lt;p&gt;Route this ticket. Is this chunk useful. Should we retry. Is this tool call safe. All of them went to a text model. The model wrote tokens. My code parsed the JSON, checked it, and then a normal &lt;code&gt;if&lt;/code&gt; made the real decision.&lt;/p&gt;

&lt;p&gt;That is a lot of work for a yes or no.&lt;/p&gt;

&lt;p&gt;Last week a new kind of model appeared. It does only the decision part. I tried it for a few days. Here is what it is, where it helps, and where it does not.&lt;/p&gt;

&lt;p&gt;All examples are JavaScript, and I kept them small on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a System One model is
&lt;/h2&gt;

&lt;p&gt;TypeSafe released &lt;a href="https://docs.typesafe.ai/introduction" rel="noopener noreferrer"&gt;Jev&lt;/a&gt; on September 15, 2026, in early access. They call it the first public System One model.&lt;/p&gt;

&lt;p&gt;The idea is simple. You send your application state. You send typed questions. You get back probabilities. There is no text. A normal LLM writes JSON one token at a time. This one returns typed answers directly.&lt;/p&gt;

&lt;p&gt;All the answers come back in a single forward pass. That is why it is fast.&lt;/p&gt;

&lt;p&gt;There are three question types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;You give&lt;/th&gt;
&lt;th&gt;You get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;noul&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a yes or no question&lt;/td&gt;
&lt;td&gt;the probability of yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;choice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;instructions plus named options&lt;/td&gt;
&lt;td&gt;the chosen option, probabilities, confidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;score&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;instructions plus ordered levels&lt;/td&gt;
&lt;td&gt;a number, probabilities, confidence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;choice&lt;/code&gt; accepts up to 255 options. &lt;code&gt;score&lt;/code&gt; accepts 2 to 10 levels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smallest example
&lt;/h2&gt;

&lt;p&gt;Install it and run it. You need Node 20 or newer.&lt;br&gt;
&lt;/p&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; @typesafe-ai/sdk
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TYPESAFE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-key"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One question. One answer.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;noul&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TypeSafeClient&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="s2"&gt;@typesafe-ai/sdk&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;client&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;TypeSafeClient&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;response&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;systemOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;I was charged twice. Please fix this.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;isBilling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;noul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Is this about billing?&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="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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isBilling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;noul&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 0.97&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That number is the probability of yes. Nothing else comes back.&lt;/p&gt;

&lt;p&gt;Now a question with options.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TypeSafeClient&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="s2"&gt;@typesafe-ai/sdk&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;response&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;systemOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ticketText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;team&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Which team should read this?&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;billing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payments and invoices&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;support&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bugs and how-to questions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;New customers and pricing&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="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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "billing"&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 0.88&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The types come from the questions you wrote. No schema file. No parser. No retry when the JSON is broken.&lt;/p&gt;

&lt;p&gt;You can also send several questions at once. They all travel in one request, which matters for the cost numbers below.&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;response&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;systemOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ticketText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;isBilling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;noul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Is this about billing?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;isAngry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;noul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Is the customer angry?&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Case 1: put it in front of your model, not instead of it
&lt;/h2&gt;

&lt;p&gt;This is where I would start. The common advice at launch was not to replace your model. Let the small decision happen first, so the expensive model runs only when you really need it.&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;gate&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;systemOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;needsWriting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;noul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Does answering this need a written reply?&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;needsWriting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;noul&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&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="nf"&gt;sendCannedReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userMessage&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;callClaude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the expensive path, now less common&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;0.5&lt;/code&gt; is my code, not the model. It is an example value, not a measured or recommended one. Choose yours from your own data.&lt;/p&gt;

&lt;p&gt;Some numbers were reported from an email tool that made this change. Two AI calls per email became one decision call. A test with 120 tickets took 42 seconds instead of 22 minutes, and cost $0.0003 instead of $0.059. The person who ran the test said his slow version was also slowed down by retry errors, so the real gap is smaller.&lt;/p&gt;

&lt;p&gt;Read that as a direction, not as a benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 2: check the output before you send it
&lt;/h2&gt;

&lt;p&gt;This is the shape I liked most. The LLM writes the reply. The small model checks it.&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;draft&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;callClaude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&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;check&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;systemOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;isGood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;noul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Does the draft answer the question?&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isGood&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;noul&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.8&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="nf"&gt;sendToHuman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;draft&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="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use an object for &lt;code&gt;state&lt;/code&gt; when you have more than one piece of context, like here. A plain string is fine when you have one.&lt;/p&gt;

&lt;p&gt;The official advice is to ask only for judgments that a person who knows the topic could make in about a second. If a question needs slow thinking, split it into smaller questions and combine the answers in your own code.&lt;/p&gt;

&lt;p&gt;That rule is the whole design. The model judges. Your code decides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 3: check relevance before you build the prompt
&lt;/h2&gt;

&lt;p&gt;This one is cheap and people talk about it too little. Before you put twenty retrieved chunks into a prompt, rate them.&lt;/p&gt;

&lt;p&gt;Build the questions in a loop, one per chunk.&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;questions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;chunk&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`chunk_&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;noul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Does this passage help answer the question?&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="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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;systemOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;passages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;questions&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;keep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunks&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;chunk&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`chunk_&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;noul&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.6&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;Send many questions at once, then filter in code. The prompt that reaches the expensive model gets smaller, and that is usually where the bill comes from.&lt;/p&gt;

&lt;p&gt;If you want more than yes or no, &lt;code&gt;score&lt;/code&gt; gives you levels instead.&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="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;How relevant is this passage?&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not at all&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="s2"&gt;a little&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="s2"&gt;a lot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is already an unofficial LlamaIndex integration that does this. The model rates the retrieved passages and picks which tool answers a query. Note that score mode is a rubric, not cosine similarity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 4: when the data cannot leave your servers
&lt;/h2&gt;

&lt;p&gt;For agency work this is the important part. I would rather not explain to a client why their data goes to a third party on every routing call.&lt;/p&gt;

&lt;p&gt;Jared Palmer, who built Turborepo and v0, released &lt;a href="https://runtimewire.com/article/jared-palmer-kev-qwen35-decision-models" rel="noopener noreferrer"&gt;Kev&lt;/a&gt;. It is an open source family that follows the same interface. There are three models, 0.8B, 4B and 9B, and you can train, inspect and run them on your own hardware under Apache-2.0. The server, the weights and the training code stay local.&lt;/p&gt;

&lt;p&gt;So the change is a base URL. The SDK appends &lt;code&gt;/v1/systemone&lt;/code&gt; to it.&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;client&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;TypeSafeClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:8000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// your own server&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else in your code stays the same. You can also skip the constructor and set &lt;code&gt;TYPESAFE_BASE_URL&lt;/code&gt; in the environment, so the same code runs against the hosted model in development and against your server in production.&lt;/p&gt;

&lt;p&gt;You pay for this in accuracy. On the published numbers, Kev-9B gets 0.837 on the locked test set, against 0.857 for the hosted model. The cost of building it still surprises me. The whole port to Qwen3.5 was about $95 of Modal H100 time plus three cents of API calls.&lt;/p&gt;

&lt;p&gt;Latency is not free either. Kev-4B on an Apple M5 took 779ms for five questions, up from 174ms on the earlier Qwen3 version. Measure it on your own hardware first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worth an afternoon
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You delete the parsing layer.&lt;/strong&gt; No schema prompt, no JSON repair, no error branch that nobody tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The options are yours.&lt;/strong&gt; You write the list of possible answers in your own code. The model does not invent them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You get real probabilities.&lt;/strong&gt; Confidence is a number you can compare against a threshold. That is how you build a safe fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One request, many questions.&lt;/strong&gt; You can classify, score and gate a ticket in a single call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can undo it.&lt;/strong&gt; Put it behind a flag, keep the old call, run both for a week, compare, then delete.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The ecosystem is already usable. There are provider integrations for Cloudflare AI, the Netlify AI Gateway, OpenRouter and the Vercel AI Gateway. On the JavaScript side there are integrations for LangChain.js and Effect. In the Vercel AI SDK, the Boolean type is what TypeSafe calls Noul. Knowing that will save you ten minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest caveats
&lt;/h2&gt;

&lt;p&gt;I like this, and I am still not putting it in a client's critical path this month.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is one week old.&lt;/strong&gt; Early access, launched September 15, 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The launch numbers are marketing numbers.&lt;/strong&gt; The claims of 20 to 200 times faster and 40 to 400 times cheaper came from launch posts. The most careful writeup I found says clearly that it is an analysis of the documentation and architecture of Jev 1.13, not an independent latency test and not a production deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model is uneven.&lt;/strong&gt; The TypeSafe docs list limits around arithmetic, dates, distractors, adversarial state and inconsistent structure. Do not ask it to do math.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The training data is synthetic.&lt;/strong&gt; That is confirmed, and it is 100% synthetic. The architecture is still a guess from outside. The best guesses are ModernBERT and diffusion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The open models are not the same model.&lt;/strong&gt; The Kev README says clearly that no outputs from the hosted model were used for training. That is good practice, and it also means you get the same interface, not the same quality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence is not accuracy.&lt;/strong&gt; Choice and Score report confidence separately from the answer probability, and Noul has no confidence field at all. Read that page before you write a threshold.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would do next
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Search your code for AI calls whose output only ever feeds an &lt;code&gt;if&lt;/code&gt;. Those are your candidates.&lt;/li&gt;
&lt;li&gt;Pick the one with the most traffic. Ticket routing, moderation, relevance filtering.&lt;/li&gt;
&lt;li&gt;Write the list of options in your own code, not in a prompt.&lt;/li&gt;
&lt;li&gt;Put the decision call in front of the existing call, behind a feature flag, and keep the old path as a fallback.&lt;/li&gt;
&lt;li&gt;Run both for a week and compare on your data, not on the launch benchmarks.&lt;/li&gt;
&lt;li&gt;If the data cannot leave your servers, run Kev locally and point the SDK at it.&lt;/li&gt;
&lt;li&gt;Only then delete the old call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the idea I keep coming back to. An agent is a long chain of small judgments with a little writing at the end. We have been paying the writing price for the whole chain.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Problem Isn’t AI. It’s the Pace.</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Tue, 18 Aug 2026 21:42:00 +0000</pubDate>
      <link>https://dev.to/andresclua/the-problem-isnt-ai-its-the-pace-3p3b</link>
      <guid>https://dev.to/andresclua/the-problem-isnt-ai-its-the-pace-3p3b</guid>
      <description>&lt;p&gt;Swipe and there is food at the door. Swipe and there is a date on Thursday. Swipe and there is a stranger's car outside taking you home.&lt;/p&gt;

&lt;p&gt;Ask for a function and there is code. Ask for a design and there is a mockup. Ask for a summary of a forty page document and it is done before you have finished reading the first page yourself.&lt;/p&gt;

&lt;p&gt;None of this felt like a decision. It arrived, and we adapted to the interface without ever adapting to the change.&lt;/p&gt;

&lt;p&gt;That is the part I keep thinking about. Not whether the technology is good. Whether the gap between how fast it moves and how fast we absorb it is still a survivable gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The race is worth running
&lt;/h2&gt;

&lt;p&gt;I want to be clear about where I stand, because the easy version of this article is a complaint, and I do not want to write that one.&lt;/p&gt;

&lt;p&gt;I like this. I have built things in the last two years that I could not have built in the previous ten. I read release notes for fun. The speed is genuinely exciting and I am not nostalgic for a slower industry.&lt;/p&gt;

&lt;p&gt;The problem is not the speed. It is that the speed now exceeds my ability to form an opinion about what is moving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I notice it in my own work
&lt;/h2&gt;

&lt;p&gt;The clearest version for me is tooling.&lt;/p&gt;

&lt;p&gt;I &lt;a href="https://dev.to/andresclua/rust-ate-the-javascript-toolchain-then-cloudflare-bought-it-4d87"&gt;wrote&lt;/a&gt; a while back about Rust taking over the JavaScript toolchain. Vite, Rolldown, the whole rewrite of the layer underneath everything we ship. It was a good story to write. It was also a story where, by the time I understood the tradeoffs well enough to have a real position, the position was already outdated.&lt;/p&gt;

&lt;p&gt;That used to be an annoyance. Now it is the normal condition. I do not reach competence with a tool anymore. I reach a working familiarity, and then the tool changes, and I am back near the start with a slightly better instinct than last time.&lt;/p&gt;

&lt;p&gt;Evaluation has become the bottleneck. Not learning. Deciding what is worth learning.&lt;/p&gt;

&lt;p&gt;Multiply that by every developer on a team, and you get an organisation where nobody is deeply expert in anything because expertise takes longer to build than the thing takes to be replaced. That has architectural consequences. It is why so many codebases are now three eras of tooling stacked on top of each other, with nobody left who understands the bottom layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest version of "our brains cannot keep up"
&lt;/h2&gt;

&lt;p&gt;The line I first wanted to write was that our brains do not finish processing the change. It feels true. It is how the tiredness actually feels from the inside.&lt;/p&gt;

&lt;p&gt;But I do not think I can defend it as stated, and there is a lot of confident pop neuroscience about attention spans and dopamine that does not survive contact with the actual research. So let me say the thing I can defend instead.&lt;/p&gt;

&lt;p&gt;What lags is not biology. It is everything built on top of biology.&lt;/p&gt;

&lt;p&gt;Norms take years. Nobody knew what phone etiquette was for a decade. Laws take longer. Professional identity takes a career, which is why a developer who trained on one paradigm spends their whole working life partly translating. Institutions are the slowest of all, and they are the layer that decides what is permitted rather than what is possible.&lt;/p&gt;

&lt;p&gt;Those lags are measurable. You do not need a claim about neurons. The technology iterates in months and the scaffolding around it iterates in decades, and every year that ratio gets worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every generation said this
&lt;/h2&gt;

&lt;p&gt;I have to include this part or the article is dishonest.&lt;/p&gt;

&lt;p&gt;People said it about the printing press. The telegraph. Radio. Television. The internet. Every one of those came with a serious argument that human beings were about to be overwhelmed, and in every case we absorbed it and the panic reads as quaint now.&lt;/p&gt;

&lt;p&gt;"This time is different" is the most overused sentence in technology. I do not get to skip that objection just because I feel tired.&lt;/p&gt;

&lt;p&gt;So here is the only version of the difference I can actually argue for, and I hold it loosely.&lt;/p&gt;

&lt;p&gt;Every previous case had a plateau. The technology arrived, there was a shock, then a long flat stretch while people, laws and habits caught up, and then the next thing. The plateau is what made it survivable. Adaptation happened during the pause.&lt;/p&gt;

&lt;p&gt;The proposal on the table now is to remove the pause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which brings me to the letter
&lt;/h2&gt;

&lt;p&gt;On July 28, 2026, more than a thousand employees of the frontier AI labs signed a statement called &lt;a href="https://www.pacingthefrontier.com/" rel="noopener noreferrer"&gt;Pacing the Frontier&lt;/a&gt;. The site listed 1,178 signatories when I checked and the form is still open. Anthropic, OpenAI, Google and Meta are all represented, including several CEOs and chief scientists.&lt;/p&gt;

&lt;p&gt;They are not asking for a pause. The single request is that the U.S. government support an international effort to build the technical and governance tools needed to "deliberately pace the frontier of automated AI development."&lt;/p&gt;

&lt;p&gt;Automated AI development is the phrase that matters. Models used to build better models, which are then used to build better models again. That is a description of a process with no plateau in it. The gap between capability and comprehension stops being constant and starts compounding.&lt;/p&gt;

&lt;p&gt;What struck me most was not the request. It was the personal comments on the signatory page. Several of them are people who work on capability evaluations for a living saying the recent pace has been a shock to them too.&lt;/p&gt;

&lt;p&gt;That is the same feeling I have on a much smaller scale about build tools. It goes all the way up. Nobody is standing somewhere calm.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what do we actually do
&lt;/h2&gt;

&lt;p&gt;I do not have a policy answer. I build websites and internal tools. The governance question is above my pay grade and I am suspicious of developers who pretend otherwise.&lt;/p&gt;

&lt;p&gt;But I have a working answer for myself, and it is not "keep up."&lt;/p&gt;

&lt;p&gt;Keeping up is no longer a coherent goal. There is more happening per week than any person can evaluate, and treating that as a personal failure is a category error. The skill is not consumption. It is choosing what to deliberately not keep up with, and being at peace with that choice.&lt;/p&gt;

&lt;p&gt;For my own work that means a short list of things I go deep on and a much longer list I let pass. It means I do not adopt a tool until it has survived long enough to have known problems. It means I write about things a few weeks late, once the first wave of takes has burned off and there is something real to say.&lt;/p&gt;

&lt;p&gt;That is a personal mitigation, not a solution. The version of this problem that operates at the scale of an industry does not get solved by individuals choosing to read less. Which is, I think, the strongest thing the letter has going for it. It is the institutional form of the same admission. Someone should be allowed to say wait, and right now nobody is.&lt;/p&gt;

&lt;p&gt;I do not think we should stop out of fear. I do think there is something absurd about waiting until we have lost control before starting the conversation about how we would want to exercise it.&lt;/p&gt;




&lt;p&gt;Sources: the statement and signatory list at &lt;a href="https://www.pacingthefrontier.com/" rel="noopener noreferrer"&gt;pacingthefrontier.com&lt;/a&gt;, with reporting from &lt;a href="https://thenextweb.com/news/pacing-the-frontier-ai-employees-letter-us-government" rel="noopener noreferrer"&gt;The Next Web&lt;/a&gt;. Signatory counts were accurate at the time of writing.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Tell If You Can Actually Drop Your Animation Library</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Tue, 11 Aug 2026 11:56:46 +0000</pubDate>
      <link>https://dev.to/andresclua/how-to-tell-if-you-can-actually-drop-your-animation-library-571n</link>
      <guid>https://dev.to/andresclua/how-to-tell-if-you-can-actually-drop-your-animation-library-571n</guid>
      <description>&lt;h1&gt;
  
  
  The Demo Is Not the Migration
&lt;/h1&gt;

&lt;p&gt;Every post telling me to delete my animation library opens with the same snippet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.progress-bar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grow&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;scroll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;grow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scaleX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scaleX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&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;Zero bytes. Native platform. The browser does it now.&lt;/p&gt;

&lt;p&gt;That code is real, it works, and it is genuinely great. It is also not the decision.&lt;/p&gt;

&lt;p&gt;I lead a team that builds animation-heavy sites, so when scroll-driven animations got real browser support I took the question seriously. I expected to come out with a smaller bundle. I came out with two conclusions that sit oddly together: this is the most fun I have had with the platform in years, and I am not removing GSAP from a single project.&lt;/p&gt;

&lt;p&gt;The reason is not that CSS is worse. It is that a stack decision is not scored on its best example.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this decision actually works
&lt;/h2&gt;

&lt;p&gt;Nobody switches stacks because the new thing is prettier. You switch when everything you currently do has an equivalent in the new thing, or when the gaps are small enough to absorb.&lt;/p&gt;

&lt;p&gt;So the question is never "can CSS do a progress bar." It is: go through the animations that exist in your codebase right now, every one of them, and find the ones with no equivalent. Those are the whole decision. The happy path is not information, because the happy path is what the demo already showed you.&lt;/p&gt;

&lt;p&gt;And there is arithmetic that people skip. If forty animations move to CSS and three cannot, the library is still in your bundle. You did not save 35 KB. You saved nothing, and now you maintain two animation systems, two mental models, two sets of debugging habits, and a rule about which one owns what. Partial migration is often more expensive than either endpoint.&lt;/p&gt;

&lt;p&gt;That reframes what the rest of this article is for. It is not a comparison table. It is the inventory: what has an equivalent, what has a partial one, and what has none. I would rather you finish it able to run the same audit on your own projects than agree with my conclusion.&lt;/p&gt;

&lt;p&gt;Starting with the thing almost every comparison gets wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSS can scrub. That was never the gap.
&lt;/h2&gt;

&lt;p&gt;Scrubbing is exactly what &lt;code&gt;animation-timeline&lt;/code&gt; does. Animation progress is tied to scroll position, one to one. The snippet at the top of this article is a scrubbed animation, running off the main thread, with no library.&lt;/p&gt;

&lt;p&gt;What CSS does not have is &lt;strong&gt;smoothed&lt;/strong&gt; scrub:&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;gsap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.panel&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;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scrollTrigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.section&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;scrub&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;scrub: true&lt;/code&gt; is one to one, same as CSS. &lt;code&gt;scrub: 1&lt;/code&gt; means the animation takes a second to catch up to the scroll position. It lags behind you. It eases toward the target instead of snapping to it.&lt;/p&gt;

&lt;p&gt;That single digit is a large part of why GSAP work feels expensive and CSS scroll animations can feel mechanical. There is no CSS-only equivalent.&lt;/p&gt;

&lt;p&gt;There is, however, a four kilobyte one, and it is probably already in your project. I come back to that below, because it is the most interesting thing I found while writing this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CSS is genuinely great at
&lt;/h2&gt;

&lt;p&gt;I want to be fair before I list limits, because the limits are not a reason to ignore this. Some of it is better than what we were doing.&lt;/p&gt;

&lt;p&gt;Reveals on enter, the most common item in any brief:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.reveal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fade-up&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="py"&gt;animation-range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;fade-up&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&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;That replaces an IntersectionObserver, a class toggle, and the teardown for both. I have written the JavaScript version of this hundreds of times and I do not miss it.&lt;/p&gt;

&lt;p&gt;Progress bars are one declaration. Parallax is a keyframe with &lt;code&gt;animation-range: cover&lt;/code&gt;. And named timelines are the part people overlook, because they unlock layouts where the scroll container is not the animated element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.gallery&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;overflow-x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;scroll-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--gallery&lt;/span&gt; &lt;span class="nb"&gt;inline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.indicator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grow&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--gallery&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;Three things push me toward CSS that have nothing to do with capability. It runs on the compositor thread, which a ScrollTrigger &lt;code&gt;onUpdate&lt;/code&gt; does not, and on a mid-range Android that gap is visible. It has no lifecycle to get wrong, which removes an entire class of bug on client-side routed sites. And it costs nothing.&lt;/p&gt;

&lt;p&gt;Hold onto that first one. If you run WebGL it reverses completely, and that is the last section of this article.&lt;/p&gt;

&lt;p&gt;So I use it. Just not for everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The refresh problem, which CSS simply does not have
&lt;/h2&gt;

&lt;p&gt;This is the advantage I underrated, and if your pages have accordions, filters, lazy content or late-loading images, it is probably the one that matters most to you.&lt;/p&gt;

&lt;p&gt;ScrollTrigger measures start and end positions when it is created and caches them. Change the page height afterwards and those numbers are stale.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gsap.com/resources/st-mistakes/" rel="noopener noreferrer"&gt;GSAP's own docs are direct about it&lt;/a&gt;: ScrollTrigger refreshes automatically when the viewport resizes, but a value you hardcoded at creation will not update unless you made it function-based. And if you load images without width and height attributes, or fetch content that affects layout, you normally have to call &lt;code&gt;ScrollTrigger.refresh()&lt;/code&gt; yourself in the callback.&lt;/p&gt;

&lt;p&gt;So real code ends up looking like this:&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;ScrollTrigger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.section&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;start&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;top top&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;end&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;invalidateOnRefresh&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="na"&gt;scrub&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="nx"&gt;accordion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;toggle&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ScrollTrigger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&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;ResizeObserver&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;ScrollTrigger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&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="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.dynamic-grid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Function-based start and end so they recompute. &lt;code&gt;invalidateOnRefresh&lt;/code&gt; so recorded tween values are discarded. A refresh call on every DOM mutation you can anticipate. A ResizeObserver for the ones you cannot.&lt;/p&gt;

&lt;p&gt;And refresh brings its own problems. It re-measures every trigger on the page, so it is not cheap when you have forty of them. Refresh order matters, which is why &lt;code&gt;refreshPriority&lt;/code&gt; exists. Internally it jumps scroll position to zero so every element sits at its natural position, which is why &lt;code&gt;scroll-behavior: smooth&lt;/code&gt; on your &lt;code&gt;html&lt;/code&gt; element quietly breaks it. And progress is not preserved across a refresh unless you save and restore it yourself with &lt;code&gt;refreshInit&lt;/code&gt; and &lt;code&gt;refresh&lt;/code&gt; listeners.&lt;/p&gt;

&lt;p&gt;None of this is a knock on ScrollTrigger. It is the unavoidable cost of caching measurements in userland.&lt;/p&gt;

&lt;p&gt;The CSS version of that code is the CSS version of any other scroll animation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.reveal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fade-up&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="py"&gt;animation-range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="m"&gt;100%&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;Open an accordion, load a grid, swap a font, filter a list. Nothing to call. A scroll timeline is not a stored number, it is derived from layout and evaluated as part of it. When the scroll range changes, the range changes. There is no cache to invalidate because there was never a cache.&lt;/p&gt;

&lt;p&gt;Two honest caveats.&lt;/p&gt;

&lt;p&gt;The underlying physics do not disappear. If the scroll range changes while an animation is mid-progress, the animation's position remaps immediately to the new range, so you can still get a visual jump when something expands above the fold. The difference is that CSS self-corrects and lands correct, whereas a stale ScrollTrigger stays wrong until you refresh it. Wrong-then-right beats silently wrong.&lt;/p&gt;

&lt;p&gt;And if you run &lt;a href="https://lenis.dev/" rel="noopener noreferrer"&gt;Lenis&lt;/a&gt;, it has to know the new dimensions too. It observes wrapper and content size by default and exposes &lt;code&gt;lenis.resize()&lt;/code&gt;, so this is mostly handled, but it is one more thing that can fall out of sync.&lt;/p&gt;

&lt;p&gt;For a marketing site with static sections, this whole section is irrelevant. For anything with expandable content, filtering, or user-generated length, it is the strongest single argument for moving your simple animations to CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stops
&lt;/h2&gt;

&lt;p&gt;This is the real body of the article. Every item here is something we ship regularly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smoothed scrub
&lt;/h3&gt;

&lt;p&gt;Covered above. No CSS counterpart.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real pinning
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;position: sticky&lt;/code&gt; is not pinning. ScrollTrigger's &lt;code&gt;pin: true&lt;/code&gt; takes the element out of flow, holds it, injects spacer height so the page below does not jump, and restores it at the end.&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;ScrollTrigger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.panels&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pin&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="na"&gt;scrub&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="na"&gt;end&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+=&lt;/span&gt;&lt;span class="dl"&gt;'&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;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.panels&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;offsetWidth&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Horizontal scroll sections, pinned storytelling, anything where pin duration is computed from measured content. Simple cases approximate with sticky and a tall parent. &lt;code&gt;pinSpacing&lt;/code&gt;, nested pins and measured durations do not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orchestration
&lt;/h3&gt;

&lt;p&gt;A CSS keyframe animation drives one element. A GSAP timeline sequences many, with labels, relative offsets, nesting and stagger.&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;tl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gsap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;scrollTrigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.hero&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;scrub&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="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.title&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;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.subtitle&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;opacity&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-=0.3&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="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.cards&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;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stagger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cards&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="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.bg&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;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cards&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can hand-roll this by giving every element the same named timeline and different &lt;code&gt;animation-range&lt;/code&gt; values. It works. It is also a spreadsheet of magic numbers, and when a designer asks to shift one beat by 200ms you are recalculating all of them. For a solo experiment that is fine. For a team where someone else maintains it next year, it is not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Progress in JavaScript
&lt;/h3&gt;

&lt;p&gt;A shader uniform is not a CSS property. If scroll progress has to drive a canvas, a video scrub or an analytics event, you need the number in JavaScript.&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;ScrollTrigger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.section&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onUpdate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;uniforms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uProgress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a &lt;code&gt;ScrollTimeline&lt;/code&gt; JavaScript API you can pass to &lt;code&gt;element.animate()&lt;/code&gt;, but &lt;a href="https://developer.mozilla.org/docs/Web/API/ScrollTimeline" rel="noopener noreferrer"&gt;MDN lists it as limited availability&lt;/a&gt;, and it hands you an animation timeline rather than an arbitrary per-frame callback.&lt;/p&gt;

&lt;p&gt;If you run WebGL this is the biggest item on your list by some distance, and it has a wrinkle that surprised me. It gets its own section at the end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Responsive scene logic
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ScrollTrigger.matchMedia()&lt;/code&gt; lets you build structurally different scenes per breakpoint with correct teardown. CSS media queries get you different values, not different structures.&lt;/p&gt;

&lt;h3&gt;
  
  
  The plugin surface
&lt;/h3&gt;

&lt;p&gt;SplitText for per-character reveals, MorphSVG, DrawSVG, MotionPath with real control. No CSS equivalent, and these are precisely what an art director asks for by name.&lt;/p&gt;

&lt;p&gt;Worth noting: &lt;a href="https://webflow.com/updates/gsap-becomes-free" rel="noopener noreferrer"&gt;GSAP has been 100% free since April 2025&lt;/a&gt;, every former Club plugin included, commercial use included, after Webflow acquired GreenSock. Cost is no longer an argument for leaving. Only bundle size and main-thread work are, and those are real but they are a tradeoff rather than a verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you use Lenis, half of this changes
&lt;/h2&gt;

&lt;p&gt;I almost published without this section, and it would have been wrong.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lenis.dev/" rel="noopener noreferrer"&gt;Lenis&lt;/a&gt; is on most of the sites we build, and it changes the scrub answer completely. To see why, you have to know how it works, because a lot of people assume it is doing what &lt;a href="https://locomotive.ca/en" rel="noopener noreferrer"&gt;Locomotive&lt;/a&gt; v4 used to do.&lt;/p&gt;

&lt;p&gt;Old smooth scroll libraries faked it. They fixed a wrapper and translated it every frame. Under that approach, native scroll position never actually changes, so &lt;code&gt;animation-timeline: scroll()&lt;/code&gt; sees nothing and your CSS scroll animations simply never run.&lt;/p&gt;

&lt;p&gt;Lenis is not that. It runs on top of the native scroll engine, as a small animation layer built on &lt;code&gt;scrollTo&lt;/code&gt;, with no transforms painted per frame, in under 4 KB. It intercepts the input, interpolates toward the target, and then sets the &lt;em&gt;real&lt;/em&gt; scroll position each frame.&lt;/p&gt;

&lt;p&gt;That single design decision is the whole story. The document's actual scroll offset updates, just smoothly. And &lt;code&gt;animation-timeline&lt;/code&gt; reads the actual scroll offset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So with Lenis on the page, your CSS scroll-driven animations inherit the smoothing for free.&lt;/strong&gt; No configuration, no wiring, no &lt;code&gt;lenis.on('scroll', ...)&lt;/code&gt;. The lag I said CSS could not do arrives through the scroll position itself.&lt;/p&gt;

&lt;p&gt;That is a genuinely good deal. Compare the integration cost:&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="c1"&gt;// GSAP with Lenis: you wire it up&lt;/span&gt;
&lt;span class="nx"&gt;lenis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ScrollTrigger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;gsap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ticker&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="nx"&gt;time&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;lenis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nx"&gt;gsap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lagSmoothing&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* CSS with Lenis: nothing */&lt;/span&gt;
&lt;span class="nc"&gt;.reveal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fade-up&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;view&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;The CSS version has no integration code because there is nothing to sync. Both are reading the same number from the same place.&lt;/p&gt;

&lt;p&gt;Four caveats before you get excited, because I did and then thought about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The smoothing is global, not per animation.&lt;/strong&gt; Lenis has one lerp value for the entire page. ScrollTrigger's &lt;code&gt;scrub&lt;/code&gt; is per instance, so you can have a hero lag by a full second and a progress bar track instantly. With Lenis everything shares one feel. For most sites that is fine, and arguably more coherent. For a hero where one layer should drag and another should stick to the cursor, it is not enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not double-smooth.&lt;/strong&gt; Lenis plus &lt;code&gt;scrub: 1&lt;/code&gt; is a lerp on top of a lerp. It feels mushy and disconnected, and it is the most common mistake I see in this combination. If Lenis is running, use &lt;code&gt;scrub: true&lt;/code&gt; and let Lenis own the feel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You lose the compositor argument.&lt;/strong&gt; This is the honest cost. One of the best things about CSS scroll-driven animations is that they run off the main thread. Lenis drives scroll from a main-thread rAF loop, so the position updates are now gated by main-thread frames. The animation still avoids the class of jank that comes from JavaScript touching style on every scroll event, but "this runs on the compositor and your JS cannot block it" is no longer true. If off-main-thread smoothness was your reason for moving to CSS, adding Lenis takes back part of what you bought.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reduced motion removes the effect.&lt;/strong&gt; Lenis honors &lt;code&gt;prefers-reduced-motion&lt;/code&gt; by default and forces lerp to 1, so scroll tracks the input device one to one. That is correct behaviour, and it means users with that preference get the raw unsmoothed version of your animation. Do not build a scroll effect that only reads correctly with the lag.&lt;/p&gt;

&lt;p&gt;The upshot: if Lenis is already in your stack, the case for CSS on simple scroll work gets meaningfully stronger, because you were paying for the smoothing anyway and CSS gets it for nothing. If it is not, adding a library to make your zero-byte solution feel right is a trade you should make deliberately rather than by habit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where support actually stands
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Chrome and Edge&lt;/strong&gt; shipped it unflagged in 115, July 2023. Three years of runway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safari&lt;/strong&gt; landed it in Safari 26 in September 2025, added threaded scroll-driven animations in 26.4, and fixed progress-accuracy bugs in 26.5 in June 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firefox&lt;/strong&gt; is still behind the &lt;code&gt;layout.css.scroll-driven-animations.enabled&lt;/code&gt; flag in stable as of Firefox 152, June 2026. On by default in Nightly, and a named Interop 2026 priority, so it is coming.&lt;/p&gt;

&lt;p&gt;Around 82% globally, which means &lt;strong&gt;not Baseline&lt;/strong&gt;, blocked by Firefox.&lt;/p&gt;

&lt;p&gt;I ship it anyway because the failure mode is benign. An unsupported browser ignores &lt;code&gt;animation-timeline&lt;/code&gt; and the element sits at its natural state. Write it so that natural state is the visible one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.reveal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@supports&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.reveal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fade-up&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="py"&gt;animation-range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="m"&gt;100%&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;Visible by default, animated as enhancement. Compare that to a library failing to initialise, which usually means content stuck at &lt;code&gt;opacity: 0&lt;/code&gt; and a client on the phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the audit yourself
&lt;/h2&gt;

&lt;p&gt;Here is the version of this you can actually apply, and it is not a list of my conclusions.&lt;/p&gt;

&lt;p&gt;Open a real project. List every scroll animation in it. For each one, mark it green if CSS has a direct equivalent, yellow if you could get close with named timelines and some magic numbers, red if there is no equivalent at all. Reveals, parallax and progress bars will be green. Anything driving a WebGL uniform will be red.&lt;/p&gt;

&lt;p&gt;Then look only at the red column. That column is the decision. If it is empty, you have a genuine migration and a smaller bundle. If it has three items, the library stays and the honest question becomes a different one: is CSS worth adopting &lt;em&gt;alongside&lt;/em&gt; something I am keeping anyway?&lt;/p&gt;

&lt;p&gt;For me the answer is yes, and for reasons the bundle-size framing misses entirely.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reveals, parallax, progress bars: CSS.&lt;/strong&gt; Wrapped in &lt;code&gt;@supports&lt;/code&gt;, visible by default. Most animation work by volume, and it deletes a category of init and teardown bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sections with dynamic height: CSS whenever possible.&lt;/strong&gt; No refresh calls, no observers, nothing to invalidate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pinned sequences, horizontal scroll, orchestrated heroes: GSAP.&lt;/strong&gt; No hesitation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything driving WebGL or canvas: GSAP, and everything coupled to it too.&lt;/strong&gt; The uniform needs the number in JS, and any DOM element that must stay locked to the scene has to come off the same clock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never both on one element.&lt;/strong&gt; One owner per animation or you will lose an afternoon to a fight you cannot see in DevTools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS first when prototyping.&lt;/strong&gt; No install, no init, no cleanup, edit in DevTools and see it immediately. I now rough out scroll ideas in a stylesheet before deciding whether the finished version needs a timeline. Half the time it does not, and the other half I already know exactly what I am asking GSAP to do.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is not a migration. It is a second tool, and it is a better one for a large share of the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you run WebGL, one entry in that red column is different
&lt;/h2&gt;

&lt;p&gt;I left this until now because it does not apply to everyone. If it applies to you, it is probably the most important section here.&lt;/p&gt;

&lt;p&gt;Pure WebGL is easy to reason about. It is all JavaScript, so you write JavaScript. The trouble starts when a DOM element has to move &lt;em&gt;with&lt;/em&gt; the WebGL. A heading locked to a distorted image plane. A caption sitting on a mesh as it scrolls. Text revealing in step with a shader wipe. This comes up constantly, partly because you usually want the text to stay real DOM for accessibility and SEO rather than baking it into the canvas.&lt;/p&gt;

&lt;p&gt;Now two things have to agree, and here is the twist: &lt;strong&gt;the compositor advantage becomes the problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That was the first item on my list of reasons to like CSS. Scroll-driven animations are updated by the compositor, deliberately independent of your main thread. Your WebGL renders in a main-thread rAF. Those are two different clocks. On a given frame the CSS-animated heading can reflect one scroll position while the rendered WebGL frame reflects a slightly older one. The offset is a fraction of a frame. It is invisible in a screenshot and glaring in motion. The text swims against the image.&lt;/p&gt;

&lt;p&gt;It reads as cheap, which on this kind of project is the single thing you were paid to avoid.&lt;/p&gt;

&lt;p&gt;Off the main thread means on a different clock. Everywhere else that is the feature. Here it is the bug.&lt;/p&gt;

&lt;p&gt;This is not hypothetical, it is the reason Lenis exists at all. It began as an internal tool for syncing WebGL and the DOM, and its own pitch is that jumps and delays in scroll-linked animations come from browsers running effects asynchronously from native scroll.&lt;/p&gt;

&lt;p&gt;So the rule is about coupling, not co-presence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A DOM element that must stay locked to WebGL:&lt;/strong&gt; drive both from the same JS loop. Same rAF, same progress value, one source of truth. No CSS scroll-driven animation on that element, ever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A DOM element that merely shares a page with WebGL:&lt;/strong&gt; CSS is fine. A nav shrinking, an unrelated section fading in, a progress bar. Nobody perceives sub-frame desync between two things that are not visually related.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get that line wrong and you produce the worst class of bug in this kind of work. Nothing breaks. Nothing errors. It just feels slightly wrong, and nobody on the call can articulate why.&lt;/p&gt;

&lt;p&gt;Which is a decent argument for the whole thesis, actually. The snippet at the top of this article would have worked perfectly in that project, on that page, next to a WebGL scene it was quietly fighting with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that generalises
&lt;/h2&gt;

&lt;p&gt;None of the above is really about animation.&lt;/p&gt;

&lt;p&gt;The pattern repeats for every stack decision I have made in fifteen years. Someone posts the snippet, the snippet is real, and the snippet is the easiest 80% of a problem whose difficulty lives entirely in the other 20%. Vue to Nuxt, REST to GraphQL, SCSS to Tailwind, npm to pnpm. The demo is always the happy path, because a demo of the hard path would not be a demo.&lt;/p&gt;

&lt;p&gt;Stack decisions are not made on the best case. They are made on the worst thing you currently do that the new thing cannot do, and on whether keeping both is cheaper than keeping either.&lt;/p&gt;

&lt;p&gt;Zero bytes is a great headline. It is not a feature inventory. I would rather ship 35 KB and the animation the client approved.&lt;/p&gt;




&lt;p&gt;Sources: &lt;a href="https://developer.mozilla.org/docs/Web/CSS/CSS_scroll-driven_animations" rel="noopener noreferrer"&gt;MDN on CSS scroll-driven animations&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/docs/Web/API/ScrollTimeline" rel="noopener noreferrer"&gt;ScrollTimeline&lt;/a&gt;, the &lt;a href="https://github.com/darkroomengineering/lenis" rel="noopener noreferrer"&gt;Lenis repo&lt;/a&gt;, and &lt;a href="https://webflow.com/updates/gsap-becomes-free" rel="noopener noreferrer"&gt;Webflow's GSAP announcement&lt;/a&gt;. Support figures accurate at time of writing. Check &lt;a href="https://caniuse.com" rel="noopener noreferrer"&gt;caniuse&lt;/a&gt; before shipping, this one is moving.&lt;/p&gt;

</description>
      <category>css</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Problem Isn’t AI. It’s the Pace.</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Wed, 29 Jul 2026 22:02:54 +0000</pubDate>
      <link>https://dev.to/andresclua/we-got-used-to-it-before-we-understood-it-149f</link>
      <guid>https://dev.to/andresclua/we-got-used-to-it-before-we-understood-it-149f</guid>
      <description>&lt;p&gt;Swipe and there is food at the door. Swipe and there is a date on Thursday. Swipe and there is a stranger's car outside taking you home.&lt;/p&gt;

&lt;p&gt;Ask for a function and there is code. Ask for a design and there is a mockup. Ask for a summary of a forty page document and it is done before you have finished reading the first page yourself.&lt;/p&gt;

&lt;p&gt;None of this felt like a decision. It arrived, and we adapted to the interface without ever adapting to the change.&lt;/p&gt;

&lt;p&gt;That is the part I keep thinking about. Not whether the technology is good. Whether the gap between how fast it moves and how fast we absorb it is still a survivable gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The race is worth running
&lt;/h2&gt;

&lt;p&gt;I want to be clear about where I stand, because the easy version of this article is a complaint, and I do not want to write that one.&lt;/p&gt;

&lt;p&gt;I like this. I have built things in the last two years that I could not have built in the previous ten. I read release notes for fun. The speed is genuinely exciting and I am not nostalgic for a slower industry.&lt;/p&gt;

&lt;p&gt;The problem is not the speed. It is that the speed now exceeds my ability to form an opinion about what is moving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I notice it in my own work
&lt;/h2&gt;

&lt;p&gt;The clearest version for me is tooling.&lt;/p&gt;

&lt;p&gt;I &lt;a href="https://dev.to/andresclua/rust-ate-the-javascript-toolchain-then-cloudflare-bought-it-4d87"&gt;wrote&lt;/a&gt; a while back about Rust taking over the JavaScript toolchain. Vite, Rolldown, the whole rewrite of the layer underneath everything we ship. It was a good story to write. It was also a story where, by the time I understood the tradeoffs well enough to have a real position, the position was already outdated.&lt;/p&gt;

&lt;p&gt;That used to be an annoyance. Now it is the normal condition. I do not reach competence with a tool anymore. I reach a working familiarity, and then the tool changes, and I am back near the start with a slightly better instinct than last time.&lt;/p&gt;

&lt;p&gt;Evaluation has become the bottleneck. Not learning. Deciding what is worth learning.&lt;/p&gt;

&lt;p&gt;Multiply that by every developer on a team, and you get an organisation where nobody is deeply expert in anything because expertise takes longer to build than the thing takes to be replaced. That has architectural consequences. It is why so many codebases are now three eras of tooling stacked on top of each other, with nobody left who understands the bottom layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest version of "our brains cannot keep up"
&lt;/h2&gt;

&lt;p&gt;The line I first wanted to write was that our brains do not finish processing the change. It feels true. It is how the tiredness actually feels from the inside.&lt;/p&gt;

&lt;p&gt;But I do not think I can defend it as stated, and there is a lot of confident pop neuroscience about attention spans and dopamine that does not survive contact with the actual research. So let me say the thing I can defend instead.&lt;/p&gt;

&lt;p&gt;What lags is not biology. It is everything built on top of biology.&lt;/p&gt;

&lt;p&gt;Norms take years. Nobody knew what phone etiquette was for a decade. Laws take longer. Professional identity takes a career, which is why a developer who trained on one paradigm spends their whole working life partly translating. Institutions are the slowest of all, and they are the layer that decides what is permitted rather than what is possible.&lt;/p&gt;

&lt;p&gt;Those lags are measurable. You do not need a claim about neurons. The technology iterates in months and the scaffolding around it iterates in decades, and every year that ratio gets worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every generation said this
&lt;/h2&gt;

&lt;p&gt;I have to include this part or the article is dishonest.&lt;/p&gt;

&lt;p&gt;People said it about the printing press. The telegraph. Radio. Television. The internet. Every one of those came with a serious argument that human beings were about to be overwhelmed, and in every case we absorbed it and the panic reads as quaint now.&lt;/p&gt;

&lt;p&gt;"This time is different" is the most overused sentence in technology. I do not get to skip that objection just because I feel tired.&lt;/p&gt;

&lt;p&gt;So here is the only version of the difference I can actually argue for, and I hold it loosely.&lt;/p&gt;

&lt;p&gt;Every previous case had a plateau. The technology arrived, there was a shock, then a long flat stretch while people, laws and habits caught up, and then the next thing. The plateau is what made it survivable. Adaptation happened during the pause.&lt;/p&gt;

&lt;p&gt;The proposal on the table now is to remove the pause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which brings me to the letter
&lt;/h2&gt;

&lt;p&gt;On July 28, 2026, more than a thousand employees of the frontier AI labs signed a statement called &lt;a href="https://www.pacingthefrontier.com/" rel="noopener noreferrer"&gt;Pacing the Frontier&lt;/a&gt;. The site listed 1,178 signatories when I checked and the form is still open. Anthropic, OpenAI, Google and Meta are all represented, including several CEOs and chief scientists.&lt;/p&gt;

&lt;p&gt;They are not asking for a pause. The single request is that the U.S. government support an international effort to build the technical and governance tools needed to "deliberately pace the frontier of automated AI development."&lt;/p&gt;

&lt;p&gt;Automated AI development is the phrase that matters. Models used to build better models, which are then used to build better models again. That is a description of a process with no plateau in it. The gap between capability and comprehension stops being constant and starts compounding.&lt;/p&gt;

&lt;p&gt;What struck me most was not the request. It was the personal comments on the signatory page. Several of them are people who work on capability evaluations for a living saying the recent pace has been a shock to them too.&lt;/p&gt;

&lt;p&gt;That is the same feeling I have on a much smaller scale about build tools. It goes all the way up. Nobody is standing somewhere calm.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what do we actually do
&lt;/h2&gt;

&lt;p&gt;I do not have a policy answer. I build websites and internal tools. The governance question is above my pay grade and I am suspicious of developers who pretend otherwise.&lt;/p&gt;

&lt;p&gt;But I have a working answer for myself, and it is not "keep up."&lt;/p&gt;

&lt;p&gt;Keeping up is no longer a coherent goal. There is more happening per week than any person can evaluate, and treating that as a personal failure is a category error. The skill is not consumption. It is choosing what to deliberately not keep up with, and being at peace with that choice.&lt;/p&gt;

&lt;p&gt;For my own work that means a short list of things I go deep on and a much longer list I let pass. It means I do not adopt a tool until it has survived long enough to have known problems. It means I write about things a few weeks late, once the first wave of takes has burned off and there is something real to say.&lt;/p&gt;

&lt;p&gt;That is a personal mitigation, not a solution. The version of this problem that operates at the scale of an industry does not get solved by individuals choosing to read less. Which is, I think, the strongest thing the letter has going for it. It is the institutional form of the same admission. Someone should be allowed to say wait, and right now nobody is.&lt;/p&gt;

&lt;p&gt;I do not think we should stop out of fear. I do think there is something absurd about waiting until we have lost control before starting the conversation about how we would want to exercise it.&lt;/p&gt;




&lt;p&gt;Sources: the statement and signatory list at &lt;a href="https://www.pacingthefrontier.com/" rel="noopener noreferrer"&gt;pacingthefrontier.com&lt;/a&gt;, with reporting from &lt;a href="https://thenextweb.com/news/pacing-the-frontier-ai-employees-letter-us-government" rel="noopener noreferrer"&gt;The Next Web&lt;/a&gt;. Signatory counts were accurate at the time of writing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
      <category>career</category>
    </item>
    <item>
      <title>HTTP Just Got Its First New Method in 20 Years. Here's Why You Should Care.</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Fri, 03 Jul 2026 12:00:27 +0000</pubDate>
      <link>https://dev.to/andresclua/http-just-got-its-first-new-method-in-20-years-heres-why-you-should-care-2h5p</link>
      <guid>https://dev.to/andresclua/http-just-got-its-first-new-method-in-20-years-heres-why-you-should-care-2h5p</guid>
      <description>&lt;p&gt;If you have ever written this, this article is for you:&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="c1"&gt;// a "search" that is actually a POST&lt;/span&gt;
&lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/search&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;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shoes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or the vanilla version:&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/search&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shoes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&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;We all did it. The query was too big for a URL, so we used POST. It worked. But it was always a lie. We were telling HTTP "this modifies something" when it didn't.&lt;/p&gt;

&lt;p&gt;In June 2026 the IETF published RFC 10008. It defines a new HTTP method called QUERY. It is the first new method in more than two decades. And it fixes exactly this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem in 30 seconds
&lt;/h2&gt;

&lt;p&gt;GET is perfect for reading data. It is safe, idempotent and cacheable. But it cannot carry a body, and URLs have length limits. Around 8000 characters if you are lucky. Less if a proxy in the middle disagrees.&lt;/p&gt;

&lt;p&gt;POST carries any body you want. But HTTP assumes it changes state. So no automatic caching. No safe retries. If a POST times out, the client cannot just retry it. Maybe it already did something.&lt;/p&gt;

&lt;p&gt;For years we had to pick one. Clean semantics or a real payload. Never both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What QUERY does
&lt;/h2&gt;

&lt;p&gt;QUERY is a GET with a body. That's it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;QUERY&lt;/span&gt; &lt;span class="nn"&gt;/products&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shoes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"price_asc"&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;I use axios for most of my projects, so this is how it looks there:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&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;axios&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QUERY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shoes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;price_asc&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no &lt;code&gt;axios.query()&lt;/code&gt; helper yet, but axios passes any custom method straight through to the underlying HTTP client, in the browser and in Node. No plugin needed.&lt;/p&gt;

&lt;p&gt;If you prefer plain fetch, same idea:&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;res&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/products&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QUERY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shoes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;price_asc&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server runs the query and returns the result. Nothing changes on the server. The spec guarantees two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Safe.&lt;/strong&gt; It does not modify state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent.&lt;/strong&gt; Sending it twice gives the same result as sending it once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those two guarantees are what everything below is built on.&lt;/p&gt;

&lt;h2&gt;
  
  
  This pattern is going to change
&lt;/h2&gt;

&lt;p&gt;Look at almost any API in production today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /search        &amp;lt;- reads data, says it writes
POST /reports/run   &amp;lt;- reads data, says it writes
POST /products      &amp;lt;- actually writes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of those three endpoints are lying. Here is where APIs are heading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;QUERY /search       &amp;lt;- reads
QUERY /reports      &amp;lt;- reads
POST  /products     &amp;lt;- writes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not overnight. But the direction is clear. So the two questions that matter are: what works today, and how will our apps actually change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compatibility: where we are right now
&lt;/h2&gt;

&lt;p&gt;The RFC is weeks old, so let's be honest about the current state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The protocol side is already fine.&lt;/strong&gt; HTTP does not have a fixed list of methods. QUERY is just a token in the request line. Any HTTP/1.1 or HTTP/2 connection can carry it today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Servers.&lt;/strong&gt; Nginx and Apache will pass QUERY requests through, but your config may need small tweaks. Anything that whitelists methods (&lt;code&gt;limit_except&lt;/code&gt;, security rules, WAFs) needs QUERY added explicitly. This is where most early friction will live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frameworks.&lt;/strong&gt; This is the real bottleneck. Your router needs to understand the method. Spring has a pull request in progress. Rails is discussing it. Express lets you handle custom methods with a bit of manual work. Expect native support to roll out across 2026 and 2027.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP clients.&lt;/strong&gt; Axios and fetch can already send arbitrary methods, so both of these work today:&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="c1"&gt;// axios&lt;/span&gt;
&lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QUERY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shoes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vanilla fetch&lt;/span&gt;
&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/products&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QUERY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shoes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&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;One detail worth knowing: axios ignores the body only on GET requests in browsers, because fetch and XHR do not allow it. For any other method, including QUERY, &lt;code&gt;data&lt;/code&gt; becomes the request body without touching &lt;code&gt;transformRequest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The catch in browsers is CORS. QUERY is not a safelisted method like GET or POST, so cross-origin requests trigger a preflight. This applies to axios and fetch equally, since underneath it is the same mechanism. Your server has to answer the OPTIONS request with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Access-Control-Allow-Methods: GET, POST, QUERY, OPTIONS
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same-origin requests need nothing. And in Node there is no CORS at all, so server-to-server QUERY calls work right now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CDNs and proxies.&lt;/strong&gt; Here is the interesting signal: the RFC was co-authored by engineers from Cloudflare and Akamai. CDN support may land before your framework ships it. That is not a coincidence. Caching is where QUERY pays off the most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discovery.&lt;/strong&gt; The RFC also defines a response header, &lt;code&gt;Accept-Query&lt;/code&gt;. A server uses it to announce that an endpoint supports QUERY and which formats it accepts. So clients and tools can detect support instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How our apps will change
&lt;/h2&gt;

&lt;p&gt;This is the part I find more interesting than the spec itself. The migration is not just renaming a method. Some habits and workarounds simply stop making sense.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The fake POST endpoints disappear
&lt;/h3&gt;

&lt;p&gt;Every &lt;code&gt;/search&lt;/code&gt;, &lt;code&gt;/filter&lt;/code&gt;, &lt;code&gt;/reports&lt;/code&gt; endpoint that uses POST today is a QUERY endpoint waiting to happen. The body stays identical. Only the method changes:&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="c1"&gt;// before&lt;/span&gt;
&lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// after&lt;/span&gt;
&lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QUERY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will be the first and easiest migration in most codebases.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Retry logic gets simpler
&lt;/h3&gt;

&lt;p&gt;Today, if a POST search times out, most clients do not retry. They can't. Maybe the server already processed it. So we write error screens and "try again" buttons.&lt;/p&gt;

&lt;p&gt;With QUERY, clients and SDKs can retry automatically. The request is idempotent by contract. A whole category of defensive code in mobile apps and flaky-network handling gets thinner.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Caching layers move down the stack
&lt;/h3&gt;

&lt;p&gt;Right now, caching a POST search means building it yourself. Redis, cache keys hashed from the body, invalidation logic. Application code.&lt;/p&gt;

&lt;p&gt;With QUERY, the CDN can do it. Same body, same cached response, standard HTTP semantics. A lot of hand-rolled cache layers will quietly get deleted.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. GraphQL gets a proper home
&lt;/h3&gt;

&lt;p&gt;Every GraphQL query today travels over POST, even though almost all of them are pure reads. That is the most visible version of this problem. QUERY gives GraphQL correct semantics without touching the query language. Expect GraphQL clients and servers to adopt it early.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. URLs get readable again
&lt;/h3&gt;

&lt;p&gt;We have all seen this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /products?filters=%7B%22category%22%3A%22shoes%22%7D&amp;amp;sort=price
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSON, URL-encoded, jammed into a query param because the framework offered nothing better. Those endpoints migrate to QUERY and the URL goes back to being just &lt;code&gt;/products&lt;/code&gt;. Cleaner logs too. Query bodies do not end up in access logs and bookmarks the way URLs do.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Reading an API becomes self-documenting
&lt;/h3&gt;

&lt;p&gt;When the method itself tells you if an endpoint reads or writes, documentation gets lighter and integration mistakes go down. New devs on your team stop asking "does this POST actually change anything?"&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do now
&lt;/h2&gt;

&lt;p&gt;Nothing dramatic. But you can prepare:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Audit your API. List every POST endpoint that only reads. Those are your future QUERY endpoints.&lt;/li&gt;
&lt;li&gt;Check your infra. WAFs, proxies and method whitelists that would block an unknown method.&lt;/li&gt;
&lt;li&gt;Watch your framework's releases through 2026 and 2027.&lt;/li&gt;
&lt;li&gt;If you control both client and server, nothing stops you from experimenting now with a small middleware.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Twenty years for a new HTTP method. The migration will take a while. The direction is already clear.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>api</category>
      <category>webdev</category>
      <category>http</category>
    </item>
    <item>
      <title>Rust Ate the JavaScript Toolchain. Then Cloudflare Bought It</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Fri, 26 Jun 2026 15:37:11 +0000</pubDate>
      <link>https://dev.to/andresclua/rust-ate-the-javascript-toolchain-then-cloudflare-bought-it-4d87</link>
      <guid>https://dev.to/andresclua/rust-ate-the-javascript-toolchain-then-cloudflare-bought-it-4d87</guid>
      <description>&lt;p&gt;I run Vite on almost everything. Astro sites, Nuxt projects, a small group of libraries I maintain on the side. The build tool is the part of the stack I think about least, because it just works.&lt;/p&gt;

&lt;p&gt;So when the thing under all of that changes twice in three months, I read the release notes properly.&lt;/p&gt;

&lt;p&gt;Here is what actually changed, what breaks, and the part that made developers argue for a week straight.&lt;/p&gt;




&lt;h2&gt;
  
  
  For Five Years, Vite Ran on Two Bundlers
&lt;/h2&gt;

&lt;p&gt;When Vite launched, it made a pragmatic bet. esbuild for the dev server, because it is fast. Rollup for production, because its output is well optimized. Two tools, two jobs.&lt;/p&gt;

&lt;p&gt;It worked. But it had a cost. Two bundlers meant two configs, two sets of quirks, and output that could drift between dev and prod. You tuned one, and the other behaved slightly differently.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vite.dev/blog/announcing-vite8" rel="noopener noreferrer"&gt;Vite 8&lt;/a&gt; ends the split. It shipped on March 12 with a single bundler called Rolldown, written in Rust, with the Rollup plugin API on top. Under Rolldown sits Oxc, a Rust parser and transformer that does the TypeScript and JSX work Babel used to do.&lt;/p&gt;

&lt;p&gt;One language. One pipeline. Dev and prod finally agree.&lt;/p&gt;




&lt;h2&gt;
  
  
  This Is a Pattern, Not a One-Off
&lt;/h2&gt;

&lt;p&gt;esbuild (Go) made webpack look slow. Bun did the same to Node for some workloads. Biome replaced Prettier and ESLint and runs many times faster. Now Rolldown does it to Rollup and esbuild at the same time.&lt;/p&gt;

&lt;p&gt;Every time a core JavaScript tool gets rewritten in a compiled language, the same thing happens. The speed jump is large enough to make the old version look broken.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The interesting part is not the speed. It is the compatibility.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;These Rust tools do not ask you to relearn your stack. Rolldown speaks the Rollup plugin API. Biome follows ESLint and Prettier conventions. The migration is designed to be boring, and boring is the point.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Numbers, With a Grain of Salt
&lt;/h2&gt;

&lt;p&gt;The headline figure is real. &lt;a href="https://voidzero.dev/posts/announcing-vite-8-beta" rel="noopener noreferrer"&gt;Linear cut its production build from 46 seconds to 6&lt;/a&gt;. Vite reports builds 10 to 30 times faster than the old Rollup path. Other large projects report 3x to 8x.&lt;/p&gt;

&lt;p&gt;I treat these like any benchmark. Your build is not Linear's build.&lt;/p&gt;

&lt;p&gt;A small marketing site will not see a 40 second drop, because it never had a 40 second build in the first place. The gains land hardest where you were already hurting: big dependency graphs, slow CI, painful full reloads. If your build finishes in four seconds today, this is a nice-to-have, not a rescue.&lt;/p&gt;

&lt;p&gt;Still, the scale matters. Vite sits around 130 million weekly downloads. Changing its bundler changes a large slice of the web's tooling in a single release.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Breaks
&lt;/h2&gt;

&lt;p&gt;This is the part the launch posts skip. Most projects upgrade with a version bump. Some do not. Here is what I would check before touching production.&lt;/p&gt;

&lt;p&gt;Plugins that call &lt;code&gt;transformWithEsbuild&lt;/code&gt; will fail. esbuild is no longer bundled. You move to &lt;code&gt;transformWithOxc&lt;/code&gt;, or you install esbuild yourself as a peer dependency.&lt;/p&gt;

&lt;p&gt;A few more sharp edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;build.rollupOptions&lt;/code&gt; is renamed to &lt;code&gt;build.rolldownOptions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CJS interop behaves differently in some setups&lt;/li&gt;
&lt;li&gt;Yarn PnP projects have reported friction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The safe path is two steps, not one. First, swap &lt;code&gt;vite&lt;/code&gt; for the &lt;code&gt;rolldown-vite&lt;/code&gt; package while you are still on Vite 7:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# step 1: stay on Vite 7, swap the bundler underneath&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;rolldown-vite@latest

&lt;span class="c"&gt;# run your build, check plugins, diff the output&lt;/span&gt;
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, once that is green, take the major:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# step 2: upgrade to Vite 8&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;vite@8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separates the bundler change from the major version change. If something breaks, you know which one did it.&lt;/p&gt;

&lt;p&gt;Real wins, real edges. Not magic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Then Cloudflare Bought the Whole Thing
&lt;/h2&gt;

&lt;p&gt;Now the part that lit up Hacker News.&lt;/p&gt;

&lt;p&gt;On June 4, &lt;a href="https://blog.cloudflare.com/voidzero-joins-cloudflare/" rel="noopener noreferrer"&gt;Cloudflare acquired VoidZero&lt;/a&gt;, the company Evan You founded to build this toolchain. Vite, Vitest, Rolldown, and Oxc all come with it. The whole team joins Cloudflare. &lt;a href="https://voidzero.dev/posts/voidzero-cloudflare" rel="noopener noreferrer"&gt;Evan You stays in charge&lt;/a&gt; of the open-source roadmap.&lt;/p&gt;

&lt;p&gt;The pledges are what you would expect. The projects stay MIT-licensed and vendor-neutral. No Cloudflare-only features land in core Vite. Cloudflare put one million dollars into an independent Vite ecosystem fund for maintainers outside both companies.&lt;/p&gt;

&lt;p&gt;I also read the why. Evan You was honest that VoidZero had a money problem: huge adoption, no clear revenue. The team had tried a mixed license model, and was already building a Vite-native deploy platform on top of Cloudflare before the talks (&lt;a href="https://siliconangle.com/2026/06/04/cloudflare-acquires-voidzero-maker-vite-javascript-toolchain/" rel="noopener noreferrer"&gt;SiliconANGLE has the details&lt;/a&gt;). The acquisition solves the funding question in one move.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Part Worth Arguing About
&lt;/h2&gt;

&lt;p&gt;Here is what makes me pause, and it is not the build tool.&lt;/p&gt;

&lt;p&gt;This is the second time in six months Cloudflare has bought its way into foundational web tooling. It acquired the Astro team earlier this year, with the same promises: open source, MIT, portable, governed in the open.&lt;/p&gt;

&lt;p&gt;So one company now sits very close to Vite, Vitest, Rolldown, Oxc, and Astro. The license is open. The governance is open. The pledges read well. But influence is not the same as control, and the influence here is real.&lt;/p&gt;

&lt;p&gt;I am not waving a flag about it. Cloudflare has kept its hands off so far, and putting money behind maintainers is genuinely good news. But "trust us, it stays neutral" is a promise, not a guarantee. The healthy move is to keep watching, not to panic.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Open source has always run on trust. This just concentrates more of it in one place.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Should You Upgrade?
&lt;/h2&gt;

&lt;p&gt;For most projects: yes, and soon.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The migration is usually a version bump, with a compatibility shim covering most plugins&lt;/li&gt;
&lt;li&gt;The speed gain is free once you are on it&lt;/li&gt;
&lt;li&gt;Staying on the old pipeline is slowly turning into a CI tax&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But move like an adult about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test &lt;code&gt;rolldown-vite&lt;/code&gt; in a branch first, then take Vite 8&lt;/li&gt;
&lt;li&gt;Check any plugin that touches &lt;code&gt;transformWithEsbuild&lt;/code&gt; or &lt;code&gt;rollupOptions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If you run Yarn PnP, budget extra time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Rust toolchain is here, it is faster, and the upgrade is less painful than most majors. The ownership question is a separate one. Adopt the tool on its merits, and keep an eye on who steers it.&lt;/p&gt;




&lt;p&gt;So my take is simple. Try it. Give it a chance in a throwaway branch this week, before you have an opinion about it.&lt;/p&gt;

&lt;p&gt;Faster builds are the easy win. But a single Rust pipeline that owns the whole thing opens a better question, and it is the one I actually care about.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If one tool now controls parsing, transforming, and bundling end to end, shouldn't our bundles be getting smaller too? That is the part I want to see next. Tell me in the comments if you are seeing it already.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vite</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Leading a Dev Team Is Not a Coding Job (In Some Cases). It Just Looks Like One.</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Tue, 09 Jun 2026 13:51:26 +0000</pubDate>
      <link>https://dev.to/andresclua/leading-a-dev-team-is-not-a-coding-job-in-some-cases-it-just-looks-like-one-3cdo</link>
      <guid>https://dev.to/andresclua/leading-a-dev-team-is-not-a-coding-job-in-some-cases-it-just-looks-like-one-3cdo</guid>
      <description>&lt;p&gt;Everyone tells you the same thing when you move into leading a dev team. "Now you stop writing code." Cool. I get it.&lt;/p&gt;

&lt;p&gt;I lead a dev team at a digital agency. Frontend, backend, client work, mixed stacks, real deadlines. So when I made that move, I had a pretty clear picture of what the job would be. I was mostly wrong about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I thought the job was
&lt;/h2&gt;

&lt;p&gt;I thought leading meant being the person who makes the hard technical calls.&lt;/p&gt;

&lt;p&gt;The architecture decisions. The "should we use WordPress or go headless on this one" conversations. The hairy bug nobody else can crack. The code review where I catch the thing that would have blown up in production.&lt;/p&gt;

&lt;p&gt;That was the real work in my head. Everything else felt like overhead.&lt;/p&gt;

&lt;p&gt;That part of the job does exist. But it's maybe twenty percent of it. The other eighty is something nobody really prepares you for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first time someone got stuck
&lt;/h2&gt;

&lt;p&gt;Early on, someone on the team hit a wall on a ticket. I looked at it and saw the fix in about thirty seconds. My hand was already moving to the keyboard.&lt;/p&gt;

&lt;p&gt;Then it hit me. The right move depends on how clear it actually is to me.&lt;/p&gt;

&lt;p&gt;If I have it crystal clear, fine, I fix it and move on. No ceremony. But if I have any doubt, that's the signal to not solve it alone. That's when we work it through together with the person who owns it, and sometimes it's worth pulling in someone else too. The doubt is not a weakness to hide. It's the most useful thing in the room, because it's exactly where the team learns something instead of just receiving an answer.&lt;/p&gt;

&lt;p&gt;If I fix everything the second I see it, the bug is gone today and the person is no better next week. So now I check myself before grabbing the keyboard. Is this a thirty-second fix I can hand back with an explanation, or is this a "let's look at it together" moment. That single question reset how I think about the whole role.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment delegation starts working
&lt;/h2&gt;

&lt;p&gt;Here's the part that genuinely surprised me with how good it feels.&lt;/p&gt;

&lt;p&gt;When you start transmitting clear ideas, and the other side responds the way you would have done it yourself (or at least follows your work patterns at 80%), something clicks. You hand off a piece of a project, you brace for it to come back wrong, and instead it comes back right. Maybe better than right.&lt;/p&gt;

&lt;p&gt;So you hand off a bigger piece. Then a whole feature. Then a whole project.&lt;/p&gt;

&lt;p&gt;That's the addictive part of the job nobody warns you about. Not control. The opposite of control. Watching a thing you used to hold get done well without you in the loop. The first time it happens you almost don't trust it. After a while it's the most satisfying part of the week.&lt;/p&gt;

&lt;h2&gt;
  
  
  You start seeing who they really are
&lt;/h2&gt;

&lt;p&gt;The more this happens, the more you start noticing the shape of each person.&lt;/p&gt;

&lt;p&gt;One gets sharp on performance. Another is the one who actually enjoys untangling a messy data model. Someone turns out to be great at the client conversation you used to dread. These specializations don't show up in a job description. They show up over months, in small moments, and a big part of leading is just paying attention long enough to see them.&lt;/p&gt;

&lt;p&gt;And then the job is to expose them. A retro is one of the best places for this. When someone solved something clever, I don't summarize it for them. I put them in front of the team and let them walk through it. People need to be seen for what they're good at, by the people they work with, not just by me in a 1:1. That visibility is fuel, and it's free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Throwing people into the deep end
&lt;/h2&gt;

&lt;p&gt;The harder version of this is putting people into situations you've already lived through.&lt;/p&gt;

&lt;p&gt;I've been in plenty of those. Some I walked into well prepared. Plenty I learned the hard way, getting hit until I figured it out. Part of leading is deciding when to recreate that on purpose for someone else.&lt;/p&gt;

&lt;p&gt;Sometimes it's planned. You give someone a project that's slightly above their current level because you've seen enough to know they'll rise to it. Sometimes it's a bit "a los golpes", you let them take a hit you could have softened, because the lesson only lands when it costs something.&lt;/p&gt;

&lt;p&gt;The skill is calibration. Too easy and they don't grow. Too hard and you break their confidence instead of building it. You're constantly guessing where that line is for each individual person, and you don't always guess right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The variable nobody puts in the spreadsheet
&lt;/h2&gt;

&lt;p&gt;And here's the thing that makes all of this messy: people aren't constant.&lt;/p&gt;

&lt;p&gt;When I decide who goes on which project, the technical fit is only half the equation. The other half is where the person is at. Someone coming off a brutal sprint shouldn't get dropped into the next firefight. Someone who just nailed something hard might be exactly ready for a stretch. Someone going through a rough patch outside work needs a different kind of ticket this month.&lt;/p&gt;

&lt;p&gt;And personal situations cut both ways. Sometimes life outside work is a distraction that pulls someone off their game. Sometimes the work is the calm, the steady thing someone holds onto while everything else is moving. Reading which one it is, for which person, in which week, is part of the job too.&lt;/p&gt;

&lt;p&gt;You can't put that in a staffing spreadsheet, but it's real, and ignoring it is how good people burn out on your watch. Skill, timing, morale, what else they're carrying, who they work well with. A lot of things matter, and most of them have nothing to do with code.&lt;/p&gt;

&lt;p&gt;Here's the payoff for getting this right over time. Even when I have to be out for four days, the team holds. They respond at maybe 80% to how I've taught them, and the gap between that and 100% is mostly the screwups I made and learned from the hard way, the ones I can now spare them from repeating. That's the whole point of teaching instead of just fixing.&lt;/p&gt;

&lt;p&gt;And to be fully honest, I've had a margin of comfort that not every lead gets. Part of my team has been solid for more than six years. There's a real bond there, in every sense, and I don't take it for granted. I hope it stays that way. A lot of what works isn't a system I designed. It's trust that took years to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not a promotion, it's a different job
&lt;/h2&gt;

&lt;p&gt;This is the thing I'd tell anyone moving into it. Leading a dev team is not "senior engineer plus meetings." It's a career change that happens to keep the same job title.&lt;/p&gt;

&lt;p&gt;The skills barely overlap. Being great at the code can even get in your way, because it makes the keyboard so tempting. The new job is teaching, reading people, knowing when to delegate, knowing when to expose someone and when to protect them, and keeping a group of humans pointed in the same direction. The code is the most visible part. It's not the part that needs you most.&lt;/p&gt;

&lt;p&gt;The first time someone on the team builds something better than what I would have written, a small part of me wants to find a flaw in it. That reaction is the trap. If everything good has to come from me, the team has a ceiling, and the ceiling is me. The whole job is raising that ceiling above my own head. You're trying to make yourself the least impressive engineer on the team over time. That's success, not failure.&lt;/p&gt;

&lt;p&gt;I used to think this was the boring half of the work. Now it's the part I find genuinely interesting, because it never gets "done." There's always another person to read, another call to calibrate, another thing to hand off and watch come back better than you left it.&lt;/p&gt;

&lt;p&gt;If you're about to make this jump, go in knowing you're starting a new job, not getting a reward for the old one. The sooner you stop reaching for the keyboard, the sooner you're actually doing it.&lt;/p&gt;

</description>
      <category>leadership</category>
      <category>development</category>
      <category>learning</category>
    </item>
    <item>
      <title>Visual regression testing: the missing piece in your testing stack</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Wed, 06 May 2026 15:54:34 +0000</pubDate>
      <link>https://dev.to/andresclua/visual-regression-testing-the-missing-piece-in-your-testing-stack-1c79</link>
      <guid>https://dev.to/andresclua/visual-regression-testing-the-missing-piece-in-your-testing-stack-1c79</guid>
      <description>&lt;p&gt;In agencies, testing exists but it's mostly manual. Someone opens the site before deploy, clicks around, checks the forms, makes sure nothing looks broken. Sometimes there are unit tests with Vitest or Jest. Sometimes there aren't, depending on the project and the deadline. When the deadline is in three days, automated testing is the first thing to go.&lt;/p&gt;

&lt;p&gt;And inside that manual process, there's one type of testing that's almost impossible to do well by hand: visual.&lt;/p&gt;

&lt;p&gt;I've been researching visual regression testing for a while. I built it in research projects, tried it on side experiments, read a lot. What strikes me most isn't the tool itself. It's how rarely it's used in agency work, compared to the problem it solves and how easy it's become to set it up.&lt;/p&gt;

&lt;p&gt;I'm not here to tell you my team has it running in production and we're rockstars. I'm here to share what I learned researching it, hoping to save you the first few hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why visual testing is the hardest
&lt;/h2&gt;

&lt;p&gt;Think about the types of testing you know.&lt;/p&gt;

&lt;p&gt;Unit tests with Vitest. They check that a function returns what it should. Fast, deterministic, easy to write.&lt;/p&gt;

&lt;p&gt;Integration tests. Slower, more fragile, still manageable.&lt;/p&gt;

&lt;p&gt;End-to-end. Slow, fragile, environment-heavy. Less common.&lt;/p&gt;

&lt;p&gt;Visual. Checks that the site looks the way it should. And here everything breaks. What does "looks good" even mean? How do you express that as an assertion? &lt;code&gt;expect(button).toBeVisible()&lt;/code&gt; tells you nothing about padding, color, border-radius, or how a component looks next to three others.&lt;/p&gt;

&lt;p&gt;Visual testing breaks the mental model of "logical assertion over code". What you want to assert is perceptual, not logical. That's why most stacks skip it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What VRT actually does
&lt;/h2&gt;

&lt;p&gt;It solves the problem indirectly. Instead of describing how something "looks good", it compares screenshots. You take a baseline, take another after a change, compare pixel by pixel. If the diff goes over a threshold, the test fails.&lt;/p&gt;

&lt;p&gt;Not elegant in theory. Pragmatic and it works.&lt;/p&gt;

&lt;p&gt;This catches the bugs every agency knows. You change a Tailwind variable, touch a design system component, update a UI dependency. Unit tests pass. Lighthouse passes. Site goes live. Days later a client reports the secondary button has a thicker border, or the footer lost its padding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's barely used
&lt;/h2&gt;

&lt;p&gt;Four reasons, none technical.&lt;/p&gt;

&lt;p&gt;First, agency testing is mostly manual. The human eye is bad at catching subtle visual changes. Nobody notices a padding going from 16 to 14 pixels during a quick walkthrough on a Friday afternoon. And since the manual process is already in place and "works", nobody questions what it systematically misses.&lt;/p&gt;

&lt;p&gt;Second, the first attempt always fails. Someone sets it up on a Friday, by Monday the pipeline is red with 20 false positives, the team disables it, it dies in the repo.&lt;/p&gt;

&lt;p&gt;Third, the team learning curve. Not because it's complex, but because every dev needs to understand baselines, when to update them, how to review diffs in a PR. If only one person gets it, it doesn't scale.&lt;/p&gt;

&lt;p&gt;Fourth, and this one bothers me the most. Most content on the topic is written by SaaS vendors (Percy, Chromatic, Applitools) selling their product. They tell you it's complex, you need their tool, self-hosted doesn't scale. It's not true. Playwright ships everything you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack that makes sense
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Playwright for running tests and comparing screenshots&lt;/li&gt;
&lt;li&gt;GitHub Actions for CI&lt;/li&gt;
&lt;li&gt;Screenshots versioned in your repo&lt;/li&gt;
&lt;li&gt;A masking convention for dynamic content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No external services. No licenses. All inside your repo.&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;// playwright.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;defineConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;devices&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;@playwright/test&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;testDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./tests/visual&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;toHaveScreenshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;maxDiffPixelRatio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&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="na"&gt;projects&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mobile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iPhone 13&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desktop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A base test:&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;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&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;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="o"&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;/&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;/about&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;/services&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;/contact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for &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;path&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`visual: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&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;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForLoadState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;fonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&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;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&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;home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveScreenshot&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;fullPage&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="na"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-vrt-mask]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
      &lt;span class="na"&gt;maskColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#808080&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setup is faster than ever
&lt;/h2&gt;

&lt;p&gt;Two years ago, building this stack from scratch was two or three days of focused work. Today, with current codegen tools, it takes a couple of hours if you know what to ask for. The barrier dropped. The knowledge of what to ask for is still critical. If you don't know you need &lt;code&gt;document.fonts.ready&lt;/code&gt; before the screenshot, no tool will suggest it without context.&lt;/p&gt;

&lt;p&gt;Here's the prompt I use to scaffold it on new projects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I need visual regression testing in this project.

Stack: [Nuxt 3 / Next.js / WordPress / whatever]
CI: GitHub Actions

Implement:

1. Install Playwright as dev dependency. Configure Chromium only.

2. Create `playwright.config.ts` with two projects: mobile (iPhone 13)
   and desktop (1440x900). Threshold 0.2, maxDiffPixelRatio 0.01.

3. Create `tests/visual/pages.spec.ts` iterating over the main public
   routes. For each route:
   - Wait for networkidle
   - Wait for document.fonts.ready
   - Scroll to bottom to force lazy loading
   - Disable animations via injected CSS
   - Take fullPage screenshot, masking [data-vrt-mask] elements
     with maskColor #808080

4. Create `.github/workflows/visual-regression.yml` that:
   - Runs on every PR
   - Shards by project (mobile, desktop)
   - Caches node_modules and Playwright browsers
   - Uploads playwright-report as artifact on failure
   - Does NOT auto-update baselines

5. Add scripts to package.json:
   - `test:visual` to run tests
   - `test:visual:update` to update baselines locally

6. Document in `tests/visual/README.md`:
   - How to run tests locally
   - When and how to approve new baselines
   - The [data-vrt-mask] convention

Do not install Percy, Chromatic, or external services.
Self-hosted with Playwright only.
```

`

Drop it into your AI tooling at the repo root and the scaffold comes out in one pass. The prompt isn't the point. Understanding why each decision is there is.

## Masking dynamic content: where it all breaks

This is by far the hardest part of the whole setup. Everything else you can build in an afternoon. Masking can take weeks of fine-tuning if your site has any real complexity.

An honest list of the enemies:

- **Dates and timestamps**. "3 hours ago", "Published May 5, 2026". They change every run.
- **Sliders and carousels**. They start in different positions, change height when slide content has different size, often have autoplay you can't easily disable.
- **Animations and transitions**. A 400ms fade-in produces a different screenshot at ms 200 vs ms 600. Multiply by every component using `motion-safe` or GSAP.
- **WYSIWYG content**. The client edits CMS text and breaks your test. But you can't mask the whole block or you're testing nothing.
- **Lazy-loaded images**. Depending on browser request order, the page has different heights mid-render. Full-page screenshots come out different each time.
- **External iframes**. YouTube, Vimeo, maps, Calendly, chat widgets. Different load times, rotating content.
- **Cookie banners, A/B tests, geolocation**. The site the CI bot sees isn't the site you see, and sometimes it's not even the same between two runs.
- **Skeletons and loading states**. Capture early, you see the skeleton. Capture late, you see the data. Both valid, both break the test.
- **Async fonts (FOUT/FOIT)**. Screenshot before fonts load, you get Arial. After, you get Inter. Catastrophic diff.
- **Real-time counters**. Any "1,234 users online" widget guarantees a false positive every run.

False positives are worse than no tests. They train the team to ignore warnings. Two months in, nobody reads the reports and the setup dies.

The rule that worked for me: anything with genuinely dynamic content gets a `data-vrt-mask` attribute. Not tooling magic, team discipline:

```vue
&amp;lt;template&amp;gt;
  &amp;lt;article class="post"&amp;gt;
    &amp;lt;h2&amp;gt;{{ post.title }}&amp;lt;/h2&amp;gt;
    &amp;lt;time data-vrt-mask&amp;gt;{{ post.publishedAt }}&amp;lt;/time&amp;gt;
    &amp;lt;YouTubeEmbed data-vrt-mask :id="post.videoId" /&amp;gt;
    &amp;lt;HeroSlider data-vrt-mask /&amp;gt;
    &amp;lt;p&amp;gt;{{ post.excerpt }}&amp;lt;/p&amp;gt;
  &amp;lt;/article&amp;gt;
&amp;lt;/template&amp;gt;
```

Playwright paints that region a neutral gray (`#808080`) before comparing. The rest gets compared pixel by pixel.

Use neutral gray, not Playwright's default magenta. When a dev opens a report with 15 diffs and half are screaming magenta blocks, their brain enters alarm mode and approves everything in a hurry. Neutral gray makes real diffs pop and masked regions disappear visually.

For animations, the highest-impact trick is killing them globally during the test:

```typescript
await page.addStyleTag({
  content: `
    *, *::before, *::after {
      animation-duration: 0s !important;
      animation-delay: 0s !important;
      transition-duration: 0s !important;
      transition-delay: 0s !important;
    }
  `,
})
```

This single line eliminates half the false positives.

For lazy images, force-scroll to the bottom before capturing. It's ugly. It works. Put it in a helper and never look at it again.

## Baseline approval

When a diff appears, someone decides if it's a bug or an intentional change. The common mistake is letting each dev approve their own baselines locally with `--update-snapshots` and committing the result. In three months you have approved baselines hiding real bugs. Someone shipped a broken margin, approved it, now it's the official state. Test passes green and lying.

What works:

1. Baselines update via PR. Never direct commit to main.
2. The PR shows visual diffs as artifact.
3. At least one reviewer who isn't the author.

```yaml
- uses: actions/upload-artifact@v4
  if: always()
  with:
    name: playwright-report
    path: playwright-report/
    retention-days: 14
```

## When it's not worth it

If your site barely changes, one client, monthly deploys, the overhead isn't worth it. Baseline maintenance is real work.

If you have a shared design system across multiple clients, deploy several times a week, and every visual bug means an awkward conversation with a client lead, this is the best thing you'll add this year.

VRT doesn't catch logic bugs, accessibility bugs, or performance issues. It doesn't replace unit tests if you have them. It doesn't replace manual QA on critical flows like checkout or login. It's one more layer, the one covering the gap the others leave and the human eye misses worst.

## Closing

Visual regression testing is one of those tools that, when you look at it closely, you wonder why everyone isn't using it. The answer is a mix of inertia, vendor-driven content bias, and the fact that the first attempt always fails.

Visual testing remains the hardest. And it's still mostly done by hand, which in practice means it's not done at all. With today's tooling, the barrier to entry is gone. What's left is the decision.

I don't have the perfect recipe because I don't have it running in production yet. But after researching it deeply, I'm convinced any agency with a shared design system and frequent deploys should have this. And the self-hosted Playwright setup is fully viable, contrary to what the SaaS vendors want to sell you.

I'm starting to roll it into a project soon. If you already use it in production and learned things this article doesn't cover, write them. We need more honest content on this.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>automation</category>
      <category>frontend</category>
      <category>testing</category>
      <category>ui</category>
    </item>
    <item>
      <title>Claude Code, the Pricing Gap, and the Rise of Build-Your-Own AI</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Fri, 24 Apr 2026 14:03:44 +0000</pubDate>
      <link>https://dev.to/andresclua/claude-code-the-pricing-gap-and-the-rise-of-build-your-own-ai-3j3i</link>
      <guid>https://dev.to/andresclua/claude-code-the-pricing-gap-and-the-rise-of-build-your-own-ai-3j3i</guid>
      <description>&lt;p&gt;On April 21, 2026, Anthropic quietly removed Claude Code from its $20 Pro plan. No email, no announcement, no changelog. People noticed because the pricing page changed overnight. A few hours later it got reverted, and Anthropic's Head of Growth &lt;a href="https://x.com/TheAmolAvasare/status/2046724659039932830" rel="noopener noreferrer"&gt;posted on X&lt;/a&gt; that it was a test on 2% of new signups.&lt;/p&gt;

&lt;p&gt;Simon Willison wrote the &lt;a href="https://simonwillison.net/2026/Apr/22/claude-code-confusion/" rel="noopener noreferrer"&gt;best account of what happened&lt;/a&gt;. He pays for Claude Max himself and is not exactly a hater. His point is simple: the revert doesn't matter. What matters is that Anthropic considered it.&lt;/p&gt;

&lt;p&gt;The experiment got pulled. The signal didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  It is going to get more expensive
&lt;/h2&gt;

&lt;p&gt;Claude Code is back in Pro today. But if you zoom out a bit, the direction is obvious.&lt;/p&gt;

&lt;p&gt;GitHub did the same thing on the same day with Copilot. Earlier in April, Anthropic blocked third-party tools from running on Pro and Max subscriptions. Enterprise plans moved to per-token billing. Weekly caps got tighter. Peak-hour limits got stricter.&lt;/p&gt;

&lt;p&gt;Claude Code already makes Anthropic billions of dollars a year. The company just raised $30B. This is not a company in crisis. It is a company figuring out how to charge more for the people who use it the most.&lt;/p&gt;

&lt;p&gt;The logic makes sense from their side. From ours, it means one thing: the tool we built our workflow around can change price on us overnight, and there is nothing we can do about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap nobody talks about
&lt;/h2&gt;

&lt;p&gt;A hundred dollars a month in San Francisco is not a hundred dollars a month in Bilbao, Buenos Aires, Lagos, or Jakarta. It is not the same for a two-person agency billing in euros, or a freelancer on a retainer, or a bootstrapped startup watching runway.&lt;/p&gt;

&lt;p&gt;Most of the AI tooling conversation assumes one user: a well-paid dev at a US tech company. For that person, $100/month is noise. For a lot of working developers, it adds up fast. An agency with 15 devs on Max is $18,000 a year, and that is before you count Cursor, Copilot, or anything else.&lt;/p&gt;

&lt;p&gt;But the price is not even the main problem. The main problem is depending on something you do not control. When your main tool can 5x its price on a Tuesday as a "test", you have a problem that is bigger than the bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  You do not have to build anything from zero
&lt;/h2&gt;

&lt;p&gt;When people hear "build your own AI" they imagine training a model, GPU clusters, a whole research team. That is not what this is about.&lt;/p&gt;

&lt;p&gt;It is more like cooking at home instead of eating out every day. You do not have to grow the wheat to make bread. You buy the pieces, you put them together, you eat.&lt;/p&gt;

&lt;p&gt;The pieces already exist. There are solid open-source models you can run locally or use through an API for a fraction of what Claude costs. There are tools like &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; that make running one of these models about as hard as installing an app. There are terminal workflows like &lt;a href="https://github.com/sst/opencode" rel="noopener noreferrer"&gt;OpenCode&lt;/a&gt; that work similar to Claude Code but against whatever model you want.&lt;/p&gt;

&lt;p&gt;The other piece, which is actually the important one, is your stuff. Your codebase, your docs, your conventions. That is the part nobody else has. That is what makes a generic model useful for the work you actually do.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the big players go open, you should notice
&lt;/h2&gt;

&lt;p&gt;If this still sounds like a fringe thing, look at Cursor.&lt;/p&gt;

&lt;p&gt;Cursor is one of the most popular AI coding tools right now. They just launched their flagship model, &lt;a href="https://cursor.com/blog/composer-2" rel="noopener noreferrer"&gt;Composer 2&lt;/a&gt;, positioning it as their own in-house thing. Less than 24 hours later, &lt;a href="https://x.com/fynnso/status/2034706304875602030" rel="noopener noreferrer"&gt;a developer named Fynn found a model ID&lt;/a&gt; in their API traffic that gave it away: Composer 2 is built on top of Kimi K2.5, an open-weight model from a Chinese lab called Moonshot AI.&lt;/p&gt;

&lt;p&gt;Cursor &lt;a href="https://techcrunch.com/2026/03/22/cursor-admits-its-new-coding-model-was-built-on-top-of-moonshot-ais-kimi/" rel="noopener noreferrer"&gt;confirmed it&lt;/a&gt; and later published a &lt;a href="https://cursor.com/blog/composer-2-technical-report" rel="noopener noreferrer"&gt;technical report&lt;/a&gt; admitting the Kimi base. VC Tomasz Tunguz &lt;a href="https://tomtunguz.com/cursor-kimi-open-source-ai-imperative/" rel="noopener noreferrer"&gt;made the point well&lt;/a&gt;: a $50B company reaches near-parity with state-of-the-art at a fraction of the price, running on an open model they did not build. That is not ideology. That is math.&lt;/p&gt;

&lt;p&gt;If a company valued at $50B reached that conclusion, a dev in Bilbao or an agency in Madrid can reach it too. At a smaller scale, same idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  It is not only about saving money
&lt;/h2&gt;

&lt;p&gt;Cost is the easy argument. The more interesting part is what you gain when you control the model.&lt;/p&gt;

&lt;p&gt;You can shape it to your work. A base model that you use on your own code and your own patterns gets better at your specific job, in a way a generalist model cannot, no matter how big it is.&lt;/p&gt;

&lt;p&gt;Your code stays with you. Depending on what you work on, that is the difference between "we can use AI" and "legal says no".&lt;/p&gt;

&lt;p&gt;No rate limits reset every Tuesday. No weekly caps. No versions deprecated on someone else's schedule. No experiment changing your plan overnight.&lt;/p&gt;

&lt;p&gt;None of this makes Claude irrelevant. What it does is make Claude one tool in a stack, instead of the stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to try
&lt;/h2&gt;

&lt;p&gt;The gap between what you can run locally and what you pay a subscription for is smaller than most people think. If you have not looked at this in a year, you are working with outdated assumptions.&lt;/p&gt;

&lt;p&gt;A reasonable starting point: install Ollama, pull a coding model, plug it into OpenCode or something similar, and take the next small task you would give Claude Code. Give it to the local setup instead. Notice where it falls short and where it surprises you.&lt;/p&gt;

&lt;p&gt;You will find that for a lot of the routine stuff (boilerplate, refactors, small features, tests), local models are closer to what you pay for than the marketing lets on. For hard problems, you will still want a frontier model. That is fine. The point is not to replace Claude. The point is to stop depending on it.&lt;/p&gt;

&lt;p&gt;If running local is not an option, the same logic works through APIs. There are providers selling access to these open models for a lot less than Anthropic charges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The question in 2026 is not "cloud or local". It is "which model for which task, and who decides".&lt;/p&gt;

&lt;p&gt;If Claude Code costs $100/month next week, what happens to how you work? If the answer is "everything breaks", that is not really an AI problem. That is a dependency problem. And it is true for every other tool in your stack.&lt;/p&gt;

&lt;p&gt;The hedge is not loyalty. It is having a plan B. Spend a Saturday on it.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
