<?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>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>
    <item>
      <title>HTML in Canvas: The API That Could Change How We Build Interactive Experiences on the Web</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Fri, 17 Apr 2026 12:04:45 +0000</pubDate>
      <link>https://dev.to/andresclua/html-in-canvas-the-api-that-could-change-how-we-build-interactive-experiences-on-the-web-1435</link>
      <guid>https://dev.to/andresclua/html-in-canvas-the-api-that-could-change-how-we-build-interactive-experiences-on-the-web-1435</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Heads up before we start:&lt;/strong&gt; Everything in this post is &lt;em&gt;very&lt;/em&gt; experimental. HTML-in-Canvas is a proposal sitting behind a Chrome Canary flag (&lt;code&gt;chrome://flags/#canvas-draw-element&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;My background is more related to frontend work and I've spent more time than I'd like to admit wrestling with &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; specifically trying to build rich interactive experiences that look great, behave accessibly, and don't feel like I was fighting the browser the entire time.&lt;/p&gt;

&lt;p&gt;If you've built anything intense with canvas, interactive data dashboards, creative tools, game UIs, WebGL-heavy experiences you probably know the feeling. You get beautiful, performant rendering, but you trade away a lot: accessibility, interactive text, real form elements. The moment you step inside a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;, the browser's layout engine waves goodbye and you're on your own.&lt;/p&gt;

&lt;p&gt;So when a colleague shared the HTML-in-Canvas proposal from WICG with me, my first reaction was: &lt;em&gt;wait, is this real?&lt;/em&gt; &lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Canvas Has Always Had
&lt;/h2&gt;

&lt;p&gt;Canvas is great at pixel-perfect rendering, WebGL shaders, games, creative tools but it has a fundamental tension with the rest of the web platform.&lt;/p&gt;

&lt;p&gt;When you render text in canvas, you lose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Screen reader support&lt;/li&gt;
&lt;li&gt;Native text selection&lt;/li&gt;
&lt;li&gt;Proper i18n and font rendering (bidirectional text, ligatures...)&lt;/li&gt;
&lt;li&gt;The browser's built-in hit testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Chart libraries have been working around this for years, they either maintain a hidden DOM that mirrors the canvas (complex, fragile) or they just... give up on accessibility and... neither is great.&lt;/p&gt;

&lt;p&gt;The same problem shows up in creative tools, in-game UIs, data visualization dashboards, video export features — any time you need the power of canvas but also want your content to &lt;em&gt;behave&lt;/em&gt; like HTML.&lt;/p&gt;




&lt;h2&gt;
  
  
  What HTML-in-Canvas Proposes
&lt;/h2&gt;

&lt;p&gt;The idea is surprisingly straightforward: let HTML elements live &lt;em&gt;inside&lt;/em&gt; a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; and get rendered into it, while still participating in the browser's layout, hit testing, and accessibility tree.&lt;/p&gt;

&lt;p&gt;Three new primitives make this work:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The &lt;code&gt;layoutsubtree&lt;/code&gt; attribute
&lt;/h3&gt;

&lt;p&gt;You add this to a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element. It tells the browser: &lt;em&gt;"treat my children as real layout participants."&lt;/em&gt; They go through normal CSS layout, they're in the accessibility tree, they can receive focus — but they're not painted directly to the screen. Their rendering is invisible until you explicitly draw them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;canvas&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"canvas"&lt;/span&gt; &lt;span class="na"&gt;layoutsubtree&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"my-form"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Name:&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/canvas&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. &lt;code&gt;drawElementImage()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the key method. It takes a child element and draws it into the canvas context at the coordinates you specify. The browser handles the rendering fonts, borders, shadows, everything — and gives you back a CSS transform to keep the element's DOM position in sync with where it's actually drawn.&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;ctx&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onpaint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&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;transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawElementImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;my_form&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="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;my_form&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="nx"&gt;transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&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 &lt;code&gt;transform&lt;/code&gt; return value is doing something important: it keeps the accessibility tree and hit testing aligned with what the user actually sees on screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The &lt;code&gt;paint&lt;/code&gt; event
&lt;/h3&gt;

&lt;p&gt;Instead of polling with &lt;code&gt;requestAnimationFrame&lt;/code&gt;, you get a &lt;code&gt;paint&lt;/code&gt; event that fires when any canvas child's rendering changes. You can also call &lt;code&gt;requestPaint()&lt;/code&gt; to force it — similar to how &lt;code&gt;rAF&lt;/code&gt; 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;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onpaint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// event.changedElements tells you exactly what changed&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changedElements&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;el&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;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawElementImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;getX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;getY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&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;And for &lt;code&gt;OffscreenCanvas&lt;/code&gt; in workers, there's &lt;code&gt;captureElementImage()&lt;/code&gt; — which lets you snapshot an element and transfer it to a worker thread for rendering. That's a big deal for performance-heavy canvas work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters for Rich Interactive Experiences
&lt;/h2&gt;

&lt;p&gt;Think about the kind of experiences that live in canvas today: game UIs, creative tools, interactive data dashboards, 3D scenes with 2D overlays, immersive web experiences with custom controls. They all share the same pain: the moment you need real UI a tooltip, a form, a menu, styled labels you either fake it with canvas drawing primitives or you layer DOM elements on top and pray the positioning stays in sync.&lt;/p&gt;

&lt;p&gt;With HTML-in-Canvas, you could just... write HTML for that content. Real HTML, with real CSS. Drop it inside your canvas, call &lt;code&gt;drawElementImage()&lt;/code&gt; in the &lt;code&gt;paint&lt;/code&gt; event, and let the layout engine do the hard work.&lt;/p&gt;

&lt;p&gt;Imagine building a WebGL scene where the HUD is actual HTML  keyboard-navigable, screen reader accessible, styled with CSS. Or an interactive dashboard where the chart controls are real &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; elements rendered directly into the canvas surface, with proper focus management and no z-index hacks.&lt;/p&gt;

&lt;p&gt;Multi-line text? CSS handles it. RTL content? The browser handles it. Accessibility for your in-canvas UI? It's already in the DOM for free.&lt;/p&gt;

&lt;p&gt;Chart libraries also benefit here: axes, legends, and tooltips are all text-heavy and layout-sensitive. Today they reimplement font measurement and text wrapping from scratch. With this API, they could delegate that entirely to the browser. But honestly, the more exciting territory is the interactive, immersive stuff the experiences that currently feel like you're building against the platform instead of with it.&lt;/p&gt;




&lt;h2&gt;
  
  
  An Idea: What If Someone Built a Wrapper?
&lt;/h2&gt;

&lt;p&gt;Here's where I want to be clear: this is &lt;em&gt;speculation&lt;/em&gt;, not an announcement but I've been thinking about it and OGL is a fantastic example of what a well-designed thin wrapper looks like. It doesn't try to be Three.js  it just makes WebGL less painful without hiding what's happening underneath. You still think in WebGL concepts; it just removes the boilerplate.&lt;/p&gt;

&lt;p&gt;Something similar for HTML-in-Canvas could be interesting. Imagine a small library maybe 5-10KB that handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;layoutsubtree&lt;/code&gt; setup and resize observer wiring&lt;/li&gt;
&lt;li&gt;A declarative way to bind elements to canvas positions&lt;/li&gt;
&lt;li&gt;The transform synchronization so hit testing always works&lt;/li&gt;
&lt;li&gt;A simple reactive &lt;code&gt;paint&lt;/code&gt; loop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Usage might look something like this (pure speculation):&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;CanvasScene&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;html-canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// hypothetical&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scene&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;CanvasScene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#my-canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;scene&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#chart-legend&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="mi"&gt;20&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;20&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;scene&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#chart-tooltip&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dynamic&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dynamic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onpaint&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;draw&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#chart-legend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#chart-tooltip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;getTooltipPosition&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 API surface would stay small. No magic, no virtual DOM, no framework opinion. Just a thin layer that handles the sync complexity so you can focus on what you're building.&lt;/p&gt;

&lt;p&gt;It could be a genuinely useful side project once the API stabilizes. For now it's just an interesting thought experiment — but the kind that's worth keeping in the back of your mind.&lt;/p&gt;




&lt;h2&gt;
  
  
  Should You Try It?
&lt;/h2&gt;

&lt;p&gt;If you're curious: yes, absolutely. Enable the flag in Chrome Canary, clone the &lt;a href="https://github.com/WICG/html-in-canvas" rel="noopener noreferrer"&gt;WICG repo&lt;/a&gt;, and play with the demos. There's a pie chart example, a WebGL cube with HTML on its surface, and an interactive form drawn into canvas. They're genuinely cool.&lt;/p&gt;

&lt;p&gt;But calibrate your expectations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is a &lt;strong&gt;proposal&lt;/strong&gt;, currently in a &lt;strong&gt;dev trial&lt;/strong&gt; in Chrome Canary only&lt;/li&gt;
&lt;li&gt;The API will change — possibly significantly&lt;/li&gt;
&lt;li&gt;No other browser has signaled implementation yet&lt;/li&gt;
&lt;li&gt;There are real constraints (no cross-origin content, no system colors, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The right move today is to watch it, experiment in throwaway projects, and maybe file some issues if you hit interesting edge cases, I'm pretty sure the team is actively looking for feedback.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you hit the canvas + accessibility wall before? I'd love to hear how you've worked around it drop it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webgl</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Web Performance Is an Architecture Decision, Not a Last-Minute Fix</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Mon, 13 Apr 2026 14:08:51 +0000</pubDate>
      <link>https://dev.to/andresclua/web-performance-is-an-architecture-decision-not-a-last-minute-fix-3n95</link>
      <guid>https://dev.to/andresclua/web-performance-is-an-architecture-decision-not-a-last-minute-fix-3n95</guid>
      <description>&lt;p&gt;I work at a digital agency and lead the tech side. One thing we try to get right from day one is performance. Not because we're obsessed with Lighthouse scores. Because we've learned that performance problems don't come from bad code. They come from bad decisions made early in the project.&lt;/p&gt;

&lt;p&gt;The CMS you choose. How you handle third-party scripts. Whether you serve videos as raw iframes or defer them. How your CSS is loaded. Where your assets live.&lt;/p&gt;

&lt;p&gt;These are architecture decisions. And once they're made, they're expensive to undo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern We Try to Avoid
&lt;/h2&gt;

&lt;p&gt;Here's what happens on a lot of projects across the industry. A site launches. The design looks great. Then someone runs a Lighthouse audit and the score is 43. Panic. Everyone wants to "fix performance" like it's a bug you can patch on a Friday afternoon.&lt;/p&gt;

&lt;p&gt;It's not a bug. It's the consequence of choices nobody questioned months ago.&lt;/p&gt;

&lt;p&gt;We try not to end up there. That means asking the uncomfortable questions at the kickoff, not at the QA stage. It means pushing back sometimes. And it means being honest about what certain tools and platforms cost you in performance before you commit to them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your CMS Choice Is a Performance Decision
&lt;/h2&gt;

&lt;p&gt;This is the one that doesn't get enough attention. People choose a CMS based on features, familiarity, or client preference. Performance rarely enters the conversation. But it should. Because some platforms give you a head start and others put you in a hole from the beginning.&lt;/p&gt;

&lt;p&gt;A headless CMS like Sanity, Contentful, or Storyblok gives you full control over the frontend. You decide what gets loaded, when, and how. There's no theme layer injecting scripts you didn't ask for. No plugin ecosystem silently adding database queries and stylesheets on every page load.&lt;/p&gt;

&lt;p&gt;WordPress can absolutely be fast. I've built fast WordPress sites. But it takes discipline. The default experience is a theme that loads everything everywhere, a page builder that inlines CSS for every block variation, and a plugin for every feature that each adds its own JavaScript and stylesheet. Out of the box, a typical WordPress site with a popular theme, a form plugin, an SEO plugin, and a slider is already shipping 2MB+ of assets before you add any actual content.&lt;/p&gt;

&lt;p&gt;That doesn't mean WordPress is bad. It means you have to fight harder for performance on WordPress than on a headless setup. And if you're not aware of that going in, you'll end up with a 43 on Lighthouse wondering what happened.&lt;/p&gt;

&lt;p&gt;The honest breakdown from what I've seen:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headless CMS (Sanity, Contentful, Storyblok):&lt;/strong&gt; You own the frontend. Performance is in your hands. The CMS doesn't inject anything into your build. If it's slow, it's your fault. That's actually a good thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WordPress:&lt;/strong&gt; Can be fast, but requires intentional architecture. Block themes perform better than classic themes. Avoid heavy page builders if performance matters. Audit every plugin. Know what each one adds to the page weight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monolithic platforms (Drupal, AEM, Sitecore):&lt;/strong&gt; Similar challenges to WordPress but at enterprise scale. More moving parts. More potential for bloat. Performance is achievable but requires dedicated effort and expertise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static site generators (Astro, Nuxt, Next.js in SSG mode):&lt;/strong&gt; The fastest option by default because you're serving pre-built HTML. But the moment you start adding client-side interactivity and third-party scripts, you can still ruin it.&lt;/p&gt;

&lt;p&gt;The point is not that one CMS is better than another. The point is that the CMS decision &lt;em&gt;is&lt;/em&gt; a performance decision, and it should be evaluated as one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The YouTube Embed Problem
&lt;/h2&gt;

&lt;p&gt;Here's a concrete example that comes up constantly. A client site scoring poorly on Core Web Vitals. We investigate. The biggest offender? YouTube embeds.&lt;/p&gt;

&lt;p&gt;A standard YouTube iframe loads around 800KB of JavaScript before the user even clicks play. That's one embed. Put four on a page and the browser is downloading over 3MB of scripts for videos most visitors will never watch.&lt;/p&gt;

&lt;p&gt;LCP tanks. INP suffers because the main thread is busy parsing scripts nobody asked for. The page feels slow because it is slow.&lt;/p&gt;

&lt;p&gt;The fix: don't load the iframe until the user wants to watch. Show a placeholder. Load on click. Reserve the space with &lt;code&gt;aspect-ratio: 16/9&lt;/code&gt; so there's no layout shift.&lt;/p&gt;

&lt;p&gt;This is one of the things I built &lt;a href="https://boostifyjs.com" rel="noopener noreferrer"&gt;Boostify.js&lt;/a&gt; to solve. Instead of dropping a raw YouTube iframe in your markup, you set up a container and let Boostify handle the deferred loading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"js--boostify-embed"&lt;/span&gt;
  &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"aspect-ratio: 16/9;"&lt;/span&gt;
  &lt;span class="na"&gt;data-url-youtube=&lt;/span&gt;&lt;span class="s"&gt;"https://www.youtube.com/embed/dQw4w9WgXcQ"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bstf&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;Boostify&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;.js--boostify-embed&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;element&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;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;videoEmbed&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="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&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-url-youtube&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;autoplay&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;appendTo&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="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&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;p&gt;Zero bytes of YouTube JavaScript loaded until the user interacts. The space is reserved so no layout shift. The page loads as if the video isn't there. When the user clicks, the embed loads instantly.&lt;/p&gt;

&lt;p&gt;This needs to be a default in your component library, not a fix applied after someone complains about the Lighthouse score.&lt;/p&gt;

&lt;h2&gt;
  
  
  Third-Party Scripts: The Invisible Weight
&lt;/h2&gt;

&lt;p&gt;YouTube is obvious because you can see it. But the worst offenders are invisible.&lt;/p&gt;

&lt;p&gt;Google Analytics. Facebook Pixel. Hotjar. Microsoft Clarity. HubSpot tracking. LinkedIn Insight Tag. Cookie consent banners. Chat widgets. A/B testing tools.&lt;/p&gt;

&lt;p&gt;On a typical site, I've counted 8 to 12 third-party scripts loading on every page. Each one competes for the main thread. Each one makes network requests. Each one adds weight.&lt;/p&gt;

&lt;p&gt;None of these are critical for the user's first interaction. The user came to read content, check services, fill out a form. They didn't come so Hotjar could record their session before the page renders.&lt;/p&gt;

&lt;p&gt;But these scripts are usually dropped straight into the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;. Render-blocking. Loading before your actual content.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- What I see on most sites when I open the source --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://www.googletagmanager.com/gtag/js?id=G-XXXXX"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://connect.facebook.net/en_US/fbevents.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://static.hotjar.com/c/hotjar-XXXXX.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- The page hasn't started rendering yet --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Boostify, you mark your tracking scripts with &lt;code&gt;type="text/boostify"&lt;/code&gt; instead of &lt;code&gt;type="text/javascript"&lt;/code&gt;. The browser ignores them on load. Boostify picks them up and loads them only after the user interacts with the page (mouse move, scroll, touch).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Deferred until user interaction --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/boostify"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://www.googletagmanager.com/gtag/js?id=G-XXXXX"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bstf&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;Boostify&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onload&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;worker&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;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Third-party scripts loaded after user interaction&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;p&gt;Your content renders immediately. Analytics loads quietly in the background when the user is already engaged. You might lose tracking on 1-2% of ultra-fast bounces, but your site is faster, which typically increases conversions overall.&lt;/p&gt;

&lt;p&gt;My recommendation: consolidate everything into a single Google Tag Manager container, mark that one script with &lt;code&gt;type="text/boostify"&lt;/code&gt;, and let GTM handle all your tags internally. One deferred script instead of twelve blocking ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading the Right Thing at the Right Time
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;onload&lt;/code&gt; trigger handles the global stuff like analytics and tracking. But a real page has more than that. It has a slider halfway down. A contact form with a map at the bottom. An interactive widget in a section most users never scroll to.&lt;/p&gt;

&lt;p&gt;This is where Boostify's &lt;code&gt;scroll&lt;/code&gt; and &lt;code&gt;observer&lt;/code&gt; triggers come in, and where things get interesting when you combine them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scroll trigger&lt;/strong&gt;: fire a callback when the user scrolls past a certain distance. Good for loading resources that you know the user will need soon, like a slider component just below the fold.&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;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scroll&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Load slider CSS and JS only when user starts scrolling&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadStyle&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;https://cdnjs.cloudflare.com/ajax/libs/tiny-slider/2.9.4/tiny-slider.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;attributes&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;media=all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;appendTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;head&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;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadScript&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;https://cdnjs.cloudflare.com/ajax/libs/tiny-slider/2.9.2/min/tiny-slider.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;attributes&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;type="text/javascript"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;appendTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Initialize once loaded&lt;/span&gt;
    &lt;span class="nf"&gt;tns&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.my-slider&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;items&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="na"&gt;slideBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;autoplay&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Observer trigger&lt;/strong&gt;: fire a callback when a specific element enters the viewport. This uses the Intersection Observer API under the hood. Perfect for content further down the page that may or may not be reached.&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;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;rootMargin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0px&lt;/span&gt;&lt;span class="dl"&gt;'&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.01&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;element&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;.contact-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;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Load the map embed only when the contact section is visible&lt;/span&gt;
    &lt;span class="nx"&gt;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;videoEmbed&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;https://www.google.com/maps/embed?pb=YOUR_MAP_EMBED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;appendTo&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;.map-container&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;400px&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;p&gt;&lt;strong&gt;The real power is using all three together.&lt;/strong&gt; Think about a typical landing page:&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;bstf&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;Boostify&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Analytics and tracking: defer until first user interaction&lt;/span&gt;
&lt;span class="nx"&gt;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onload&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Below-the-fold slider: load when user starts scrolling&lt;/span&gt;
&lt;span class="nx"&gt;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scroll&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;distance&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="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadStyle&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;/css/slider.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;appendTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;head&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;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadScript&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;/js/slider.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;appendTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body&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="c1"&gt;// 3. YouTube video in the middle: load when its section is visible&lt;/span&gt;
&lt;span class="nx"&gt;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;element&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;.video-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;options&lt;/span&gt;&lt;span class="p"&gt;:&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.01&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;callback&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;videoEmbed&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;https://www.youtube.com/embed/dQw4w9WgXcQ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;autoplay&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;appendTo&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;.video-container&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 4. Contact form with map: load only if user reaches the bottom&lt;/span&gt;
&lt;span class="nx"&gt;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;element&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;.contact-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;options&lt;/span&gt;&lt;span class="p"&gt;:&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.01&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadScript&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;/js/form-validation.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;appendTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body&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;bstf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadStyle&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;/css/form.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;appendTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;head&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;p&gt;On initial page load, the browser downloads your HTML, your critical CSS, and your above-the-fold content. That's it. Everything else loads exactly when it's needed.&lt;/p&gt;

&lt;p&gt;This isn't micro-optimization. On a page with a slider, two video embeds, a map, and eight tracking scripts, the difference between loading everything upfront and loading it progressively can be 4-5MB of JavaScript. That's the difference between a 2-second load and an 8-second load on mobile.&lt;/p&gt;

&lt;p&gt;And it's an architecture decision. You set it up once in your component system. Every page benefits from it automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLS: Where Your CMS Shows Its True Colors
&lt;/h2&gt;

&lt;p&gt;Cumulative Layout Shift exposes bad architecture more clearly than any other metric.&lt;/p&gt;

&lt;p&gt;You know the experience. You're about to click a button and the page jumps because something loaded above it. A font swaps in and the layout shifts. An image loads without dimensions and pushes everything down.&lt;/p&gt;

&lt;p&gt;This is where CMS choice really matters. A good CMS setup enforces constraints that prevent CLS. A bad one lets editors do whatever they want and hopes for the best.&lt;/p&gt;

&lt;p&gt;If your CMS lets editors drop iframes anywhere without aspect ratio containers, you'll have CLS problems. If your theme doesn't enforce image dimensions, you'll have CLS problems. If your font loading strategy is "link to Google Fonts in the head," you'll have CLS problems.&lt;/p&gt;

&lt;p&gt;The fix isn't a CSS hack after launch. It's building the system so these issues can't happen.&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="c"&gt;/* Defaults that should exist in every project */&lt;/span&gt;
&lt;span class="nt"&gt;img&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;video&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&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="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.embed-container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;9&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="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;On a headless setup, you control the component layer. You can enforce these patterns at the component level. On WordPress, you need to be more careful with theme defaults and block output. On page builder-heavy setups, good luck.&lt;/p&gt;

&lt;h2&gt;
  
  
  INP: The Metric That Exposes JavaScript Bloat
&lt;/h2&gt;

&lt;p&gt;Interaction to Next Paint replaced First Input Delay in 2024. It's harder to pass because it measures the responsiveness of &lt;em&gt;all&lt;/em&gt; interactions, not just the first one.&lt;/p&gt;

&lt;p&gt;That hamburger menu that takes 400ms to respond? INP catches it. The accordion that freezes before opening? INP catches that too.&lt;/p&gt;

&lt;p&gt;In 2026, pages at position 1 on Google show a 10% higher Core Web Vitals pass rate than those at position 9. Only 47% of sites reach Google's "good" thresholds across all three metrics. More than half the web is failing.&lt;/p&gt;

&lt;p&gt;The most common INP killer is too much JavaScript on the main thread. Not just third-party scripts. First-party JavaScript too. Heavy page builders that inline JS for every component. Animations running on the main thread. Event listeners doing too much work synchronously.&lt;/p&gt;

&lt;p&gt;This is another place where CMS architecture matters. A site built with a lightweight frontend framework on top of a headless CMS will naturally ship less JavaScript than a WordPress site running a page builder with 40 registered block types, each loading its own script.&lt;/p&gt;

&lt;p&gt;And this is exactly why progressive loading matters. If you defer everything that's not immediately needed (using the &lt;code&gt;onload&lt;/code&gt;, &lt;code&gt;scroll&lt;/code&gt;, and &lt;code&gt;observer&lt;/code&gt; pattern described above), the main thread is free to handle user interactions without competing for resources. INP improves because the browser isn't busy parsing tracking scripts and slider libraries while the user is trying to click things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Budgets at the Kickoff
&lt;/h2&gt;

&lt;p&gt;I've never seen a creative brief that includes a performance budget. Not once across any project in any agency.&lt;/p&gt;

&lt;p&gt;Nobody says "this page should load in under 2.5 seconds on a mid-range Android phone on 4G." But that's how Google evaluates your site. Mobile performance is the primary ranking signal. Even for desktop results.&lt;/p&gt;

&lt;p&gt;53% of mobile visitors abandon a site that takes more than 3 seconds to load. A one-second delay reduces conversions by 7%. For a site generating $100K/month, that's $84K lost per year. For one second.&lt;/p&gt;

&lt;p&gt;These numbers belong in every project kickoff. We try to set targets early:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LCP under 2.5s on mobile&lt;/li&gt;
&lt;li&gt;INP under 200ms&lt;/li&gt;
&lt;li&gt;CLS under 0.1&lt;/li&gt;
&lt;li&gt;Total page weight under 1.5MB&lt;/li&gt;
&lt;li&gt;No more than 3 third-party scripts loading before first paint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't aggressive. They're the thresholds Google considers "good." Most sites don't hit them because nobody set them as a goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Field Data vs Lab Data
&lt;/h2&gt;

&lt;p&gt;One more thing. Stop obsessing over Lighthouse scores in isolation.&lt;/p&gt;

&lt;p&gt;Lighthouse is lab data. It runs in a simulated environment. It's useful for diagnostics. But Google ranks you based on field data. Real users on real devices with real network conditions.&lt;/p&gt;

&lt;p&gt;Your Lighthouse score might be 90 because you tested on your MacBook with fiber internet. But your field data in Search Console might show real users on mid-range phones getting an LCP of 4.2 seconds.&lt;/p&gt;

&lt;p&gt;Field data is what matters. Check your Core Web Vitals report in Search Console. That's the real score.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Performance is not a task at the end. It's not a sprint before launch. It's not a plugin.&lt;/p&gt;

&lt;p&gt;It starts with the CMS you choose and how you set it up. It continues with how you handle scripts, images, fonts, and video. It lives in your component architecture, your loading strategy, your defaults.&lt;/p&gt;

&lt;p&gt;Some platforms make this easier. Some make it harder. Neither is inherently wrong, but you need to know the tradeoffs before you commit.&lt;/p&gt;

&lt;p&gt;By the time someone opens DevTools and says "why is this so slow," the answer is usually "because we never decided it should be fast."&lt;/p&gt;

&lt;p&gt;Make that decision early. Make it part of the architecture. Hold every choice accountable to it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://boostifyjs.com" rel="noopener noreferrer"&gt;Boostify.js&lt;/a&gt; is free and open source. All the loading patterns described in this article (deferred third-party scripts, scroll triggers, observer-based loading, video embed placeholders) are available out of the box. &lt;a href="https://github.com/andresclua/boostify" rel="noopener noreferrer"&gt;Check it out on GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>performance</category>
      <category>boostifyjs</category>
      <category>frontend</category>
      <category>marketing</category>
    </item>
    <item>
      <title>EmDash Is Not Your Next CMS. It Might Be the One After That</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Mon, 06 Apr 2026 17:20:55 +0000</pubDate>
      <link>https://dev.to/andresclua/emdash-is-not-your-next-cms-it-might-be-the-one-after-that-3358</link>
      <guid>https://dev.to/andresclua/emdash-is-not-your-next-cms-it-might-be-the-one-after-that-3358</guid>
      <description>&lt;p&gt;Everyone is talking about EmDash. Serverless, sandboxed plugins, AI-first. Cool. I get it.&lt;/p&gt;

&lt;p&gt;I work at a digital agency. We've been building on WordPress for years. Also Sanity, Contentful, Storyblok, Drupal, AEM. We've tried a lot. So when something new shows up, I don't get hyped that fast. I just want to know: can I use this for a real project?&lt;/p&gt;

&lt;h2&gt;
  
  
  We don't pick favorites
&lt;/h2&gt;

&lt;p&gt;There's no "best CMS." Every project is different.&lt;/p&gt;

&lt;p&gt;WordPress is a great tool, we build custom plugins, custom post types, REST API endpoints, tailored admin panels. It's not a blogging platform for us. It's a framework. So when people compare WordPress to newer tools by pointing at Gutenberg or starter themes... they're looking at one layer. That's not how we work with it.&lt;/p&gt;

&lt;p&gt;Sanity is great when content modeling gets complex. Storyblok has a really nice visual editor that clients actually enjoy using. Contentful works when the client needs a clean API-first layer and doesn't want to deal with hosting. Drupal taught us a lot about structured content before headless was even a thing.&lt;/p&gt;

&lt;p&gt;AEM is enterprise. Enterprise scale, enterprise money. It solves problems that other platforms don't even try to solve.&lt;/p&gt;

&lt;p&gt;We picked each one for a reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I like about EmDash
&lt;/h2&gt;

&lt;p&gt;There are some ideas in EmDash that are genuinely interesting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Serverless by default.&lt;/strong&gt; No more dealing with PHP memory limits or server configs. That's nice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sandboxed plugins.&lt;/strong&gt; Plugins run in isolation. No more "one plugin broke the whole site" at 2am. That alone is a big deal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI built into the core.&lt;/strong&gt; Not a plugin wrapping an API. AI is part of the architecture. That opens up real possibilities for content generation and automation.&lt;/p&gt;

&lt;p&gt;I like where this is going.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes me wait
&lt;/h2&gt;

&lt;p&gt;But when I think about using #EmDash for a client project right now, there are a few things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ecosystem is basically empty.&lt;/strong&gt; WordPress has thousands of plugins. Sanity and Contentful have mature integrations. EmDash is starting from zero. When you have a client deadline, that matters a lot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's developer-first.&lt;/strong&gt; TypeScript, Astro, edge functions. And look, I love Astro. I still smile every time I see a &lt;a href="https://featured.undp.org/digital-goals/" rel="noopener noreferrer"&gt;project we built for UNDP&lt;/a&gt; in the Astro showcase. My team can work with all of that. But we also need to hand things over to marketing teams. They need to update pages and publish content without calling us every time. WordPress and Contentful have been refining that for years. EmDash hasn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's tied to Cloudflare.&lt;/strong&gt; WordPress runs on any hosting. Sanity and Contentful are SaaS. EmDash depends on the Cloudflare ecosystem. That's a dependency I'd think twice about for client work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It takes longer to ship.&lt;/strong&gt; We can go from kickoff to a working WordPress site with custom content types in days. With a new platform, there's always a learning curve. When a client has a launch date, that time gap is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not about good or bad
&lt;/h2&gt;

&lt;p&gt;I'm not saying EmDash is bad. I think it's one of the most interesting things I've seen in the CMS space in a while. The architecture makes sense. The vision is clear.&lt;/p&gt;

&lt;p&gt;But picking a CMS for client work is not just about architecture. It's about ecosystem, team readiness, how easy it is for the client, and whether this thing will still work in three years. Those things take time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd try it
&lt;/h2&gt;

&lt;p&gt;Internal projects. A proof of concept. Something where we control the timeline and the scope. That's how we've always adopted new tools. We learn them in low-risk contexts first.&lt;/p&gt;

&lt;p&gt;If you care about developer experience, EmDash is worth exploring now. Not for your next client project. As a sandbox. Play with the serverless model, the plugin patterns, the AI workflows. The best time to learn a tool is before you need it on a deadline.&lt;/p&gt;

&lt;p&gt;If the ecosystem grows, I can see EmDash sitting next to WordPress, Sanity, and Contentful in our toolkit. The ideas are solid. It just needs time.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing
&lt;/h2&gt;

&lt;p&gt;If you haven't tried Sanity, Contentful, Storyblok, or Drupal, go try them. Seriously. Yeah, 80% of content modeling feels the same across platforms. But that other 20% is where you learn things you wouldn't learn otherwise. Each one gives you a different perspective on how content should work.&lt;/p&gt;

&lt;p&gt;AEM too, if you can afford the license. I'd say try it on something small, a personal project, just to see what's inside.&lt;/p&gt;

&lt;p&gt;And here's something that would have sounded crazy two years ago: building your own CMS is actually possible now. With AI and a small team of solid developers who know how to use it, you can build exactly what you need. Your integrations. Your workflows. Ship features when you want, not when a vendor decides.&lt;/p&gt;

&lt;p&gt;We're doing it. And once you have that freedom, it's hard to go back.&lt;/p&gt;

&lt;p&gt;Maybe EmDash gets there. Maybe WordPress keeps evolving. Or maybe you just build your own thing. The point is we have more options than ever. And that's a good place to be.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>WebGPU for Frontend Devs: A Practical First Project</title>
      <dc:creator>Andrés Clúa</dc:creator>
      <pubDate>Wed, 18 Mar 2026 21:16:48 +0000</pubDate>
      <link>https://dev.to/andresclua/webgpu-for-frontend-devs-a-practical-first-project-5768</link>
      <guid>https://dev.to/andresclua/webgpu-for-frontend-devs-a-practical-first-project-5768</guid>
      <description>&lt;p&gt;Most WebGPU tutorials go deep into theory or jump straight into 3D engines. If you are a frontend developer like me, that feels like learning to drive by reading the car manual.&lt;/p&gt;

&lt;p&gt;This article is the opposite. We will build something visual, step by step, with zero GPU knowledge. By the end, you will have a &lt;strong&gt;real-time animated gradient running on your GPU&lt;/strong&gt; — and you will understand every line.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is WebGPU (in 30 seconds)
&lt;/h2&gt;

&lt;p&gt;WebGPU lets your JavaScript talk directly to the GPU. Think about it like this:&lt;/p&gt;

&lt;p&gt;Imagine you need to paint a wall. Your &lt;strong&gt;CPU&lt;/strong&gt; is one very talented painter — fast, smart, and precise. But it paints one brushstroke at a time.&lt;/p&gt;

&lt;p&gt;Your &lt;strong&gt;GPU&lt;/strong&gt; is a team of 5,000 painters. Each one can only do simple strokes, but they all paint &lt;strong&gt;at the same time&lt;/strong&gt;. Give them a wall with 2 million pixels? They split the work and finish in one pass.&lt;/p&gt;

&lt;p&gt;WebGPU is the phone call that lets your browser hire that team of painters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser support (March 2026)
&lt;/h2&gt;

&lt;p&gt;Before we start — can your browser do this?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Browser&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chrome/Edge&lt;/td&gt;
&lt;td&gt;✅ Supported (since v113)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firefox&lt;/td&gt;
&lt;td&gt;✅ Supported (since v141)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safari&lt;/td&gt;
&lt;td&gt;✅ Supported (since 18.2)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Good news: all modern browsers support it now. For production, always check first:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WebGPU is not supported in your browser&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;h2&gt;
  
  
  What we are building
&lt;/h2&gt;

&lt;p&gt;A full-screen animated gradient where the GPU decides the color of every pixel, 60 times per second. It looks like a lava lamp made of math.&lt;/p&gt;

&lt;p&gt;No libraries. No frameworks. Just HTML, JavaScript, and a few lines of GPU code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: The HTML (simple but necessary)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;My First WebGPU Project&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;margin&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;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;canvas&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&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="m"&gt;100vw&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="m"&gt;100vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;canvas&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"canvas"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/canvas&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"main.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing new here — just a full-screen canvas. This is our "empty wall" that the GPU painters will fill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Connect to the GPU
&lt;/h2&gt;

&lt;p&gt;Before the painters can work, you need to call the agency, confirm they are available, and give them the address. That is what this step does.&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;// main.js&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Check support&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="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gpu&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;WebGPU not supported&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="c1"&gt;// 2. Get an adapter (your physical GPU)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestAdapter&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;adapter&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;No GPU adapter found&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="c1"&gt;// 3. Get a device (your logical connection to it)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;device&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;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestDevice&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. Connect the canvas&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHeight&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;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webgpu&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;format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPreferredCanvasFormat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;format&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="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&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;What just happened — using our painting analogy:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;adapter&lt;/code&gt; = finding the painting agency (your physical GPU)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;device&lt;/code&gt; = signing the contract with them (your app's private connection)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context&lt;/code&gt; = giving them the key to the room where the wall is (connecting canvas to GPU)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup code is the same for every WebGPU project. Write it once, copy it forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Write a shader (the fun part)
&lt;/h2&gt;

&lt;p&gt;Now we need to give instructions to our painters. In GPU world, these instructions are called &lt;strong&gt;shaders&lt;/strong&gt; — small programs written in WGSL (WebGPU Shading Language). The syntax looks like Rust, but simpler.&lt;/p&gt;

&lt;p&gt;Think of a shader like a recipe card you give to each painter. Every painter gets the &lt;strong&gt;same recipe&lt;/strong&gt;, but they each know their own position on the wall. So painter #4500 reads: "I am at position (200, 300). Based on this recipe, my color should be blue." All 5,000 painters follow the recipe at the same time, and the wall is painted in one pass.&lt;/p&gt;

&lt;p&gt;For our gradient, we need a &lt;strong&gt;fragment shader&lt;/strong&gt; — a recipe that takes a pixel position and returns a color.&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;shaderCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* wgsl */&lt;/span&gt;&lt;span class="s2"&gt;`

  // A uniform is data we send from JavaScript to the GPU
  @group(0) @binding(0) var&amp;lt;uniform&amp;gt; time: f32;
  @group(0) @binding(1) var&amp;lt;uniform&amp;gt; resolution: vec2f;

  // Vertex shader: positions a full-screen triangle
  @vertex
  fn vertexMain(@builtin(vertex_index) i: u32) -&amp;gt; @builtin(position) vec4f {
    // A clever trick: 3 vertices that cover the entire screen
    let pos = array&amp;lt;vec2f, 3&amp;gt;(
      vec2f(-1.0, -1.0),
      vec2f( 3.0, -1.0),
      vec2f(-1.0,  3.0)
    );
    return vec4f(pos[i], 0.0, 1.0);
  }

  // Fragment shader: runs once PER PIXEL
  @fragment
  fn fragmentMain(@builtin(position) pos: vec4f) -&amp;gt; @location(0) vec4f {
    // Normalize coordinates to 0.0 - 1.0
    let uv = pos.xy / resolution;

    // Animated color channels using sine waves
    let r = sin(uv.x * 3.0 + time) * 0.5 + 0.5;
    let g = sin(uv.y * 3.0 + time * 0.7) * 0.5 + 0.5;
    let b = sin((uv.x + uv.y) * 2.0 + time * 1.3) * 0.5 + 0.5;

    return vec4f(r, g, b, 1.0);
  }
`&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;Let me explain each part:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@vertex fn vertexMain&lt;/code&gt; — This draws a giant triangle that covers the entire screen. Think of it as stretching a canvas over the wall. It is a standard trick you will reuse in every full-screen effect.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@fragment fn fragmentMain&lt;/code&gt; — This is the recipe card. It runs &lt;strong&gt;once for every pixel&lt;/strong&gt;. Each pixel receives its position and returns a color.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sin()&lt;/code&gt; with &lt;code&gt;time&lt;/code&gt; — The &lt;code&gt;sin&lt;/code&gt; function creates smooth waves. Adding &lt;code&gt;time&lt;/code&gt; makes them move. Different speeds per color channel (red, green, blue) create that organic, shifting look.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uv&lt;/code&gt; — The pixel position, but normalized to a range of 0 to 1. Like a percentage: top-left is (0, 0), bottom-right is (1, 1). This way, the shader works on any screen size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have used CSS gradients before, the idea is similar — but this one updates 60 times per second and you control everything with math.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Set up the pipeline
&lt;/h2&gt;

&lt;p&gt;We have the painters (GPU), the wall (canvas), and the recipe (shader). Now we need to put everything together — like a manager organizing the work before it starts. That is what the &lt;strong&gt;pipeline&lt;/strong&gt; does.&lt;/p&gt;

&lt;p&gt;It tells the GPU: "Here is the recipe, and here is the data you will need."&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;function&lt;/span&gt; &lt;span class="nf"&gt;createPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shaderCode&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;shaderModule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createShaderModule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;shaderCode&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Create buffers for our uniform data (time + resolution)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createBuffer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// f32 = 4 bytes&lt;/span&gt;
    &lt;span class="na"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GPUBufferUsage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UNIFORM&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;GPUBufferUsage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COPY_DST&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;resolutionBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createBuffer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// vec2f = 2 × 4 bytes&lt;/span&gt;
    &lt;span class="na"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GPUBufferUsage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UNIFORM&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;GPUBufferUsage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COPY_DST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Bind group layout: tells the GPU what data to expect&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bindGroupLayout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createBindGroupLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;entries&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;binding&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="na"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GPUShaderStage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FRAGMENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;uniform&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;binding&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="na"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GPUShaderStage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FRAGMENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;uniform&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Bind group: connects our buffers to the shader's @binding slots&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bindGroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createBindGroup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bindGroupLayout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;entries&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;binding&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="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timeBuffer&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;binding&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="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resolutionBuffer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// The render pipeline&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createRenderPipeline&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createPipelineLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;bindGroupLayouts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bindGroupLayout&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;vertex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;shaderModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;entryPoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vertexMain&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;fragment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;shaderModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;entryPoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fragmentMain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nx"&gt;format&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bindGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timeBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolutionBuffer&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;Yes, this is a lot of code.&lt;/strong&gt; That is the most common complaint about WebGPU. But if you look closely, it is just connecting things — like plugging cables into the right ports:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compile the recipe → &lt;code&gt;shaderModule&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create two small envelopes (&lt;code&gt;buffers&lt;/code&gt;) to pass data to the painters: current time and screen size&lt;/li&gt;
&lt;li&gt;Label the envelopes so the painters know which is which (&lt;code&gt;bindGroup&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Put it all together in one package (&lt;code&gt;pipeline&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The good news? This wiring is almost the same for every project. The creative part is always the shader.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: The render loop
&lt;/h2&gt;

&lt;p&gt;Time to paint. But we do not paint once — we paint 60 times per second. Each frame, we update the time (so the colors move), tell the GPU to run the recipe, and repeat. It is like a flipbook: each page is slightly different, and together they create animation.&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;function&lt;/span&gt; &lt;span class="nf"&gt;startRenderLoop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bindGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timeBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolutionBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Send resolution to GPU (only needs to happen once, unless resized)&lt;/span&gt;
  &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;resolutionBuffer&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Float32Array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&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;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Update time (convert ms to seconds)&lt;/span&gt;
    &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;timeBuffer&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Float32Array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;timestamp&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="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Create the render command&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCommandEncoder&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;pass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beginRenderPass&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;colorAttachments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="na"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCurrentTexture&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;createView&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;loadOp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clear&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;storeOp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;store&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;pass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setBindGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bindGroup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&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="c1"&gt;// 3 vertices = our full-screen triangle&lt;/span&gt;
    &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Submit to GPU&lt;/span&gt;
    &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;

    &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&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;If you have used &lt;code&gt;requestAnimationFrame&lt;/code&gt; before for Canvas 2D animations, this is the same idea. The only difference: instead of drawing with &lt;code&gt;ctx.fillRect()&lt;/code&gt;, you write a command list and send it to the GPU. Think of &lt;code&gt;commandEncoder&lt;/code&gt; as writing a to-do list, and &lt;code&gt;device.queue.submit()&lt;/code&gt; as handing that list to the painters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Wire it all together
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&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;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bindGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timeBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolutionBuffer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nf"&gt;createPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shaderCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;startRenderLoop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bindGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timeBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolutionBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;index.html&lt;/code&gt; in your browser. You should see a smooth, colorful gradient filling your screen — every single pixel computed by the GPU in real time. All 5,000 painters working together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make it your own
&lt;/h2&gt;

&lt;p&gt;The shader is your playground. You can change the recipe and the painters will follow. Try replacing the color math inside &lt;code&gt;fragmentMain&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Circular waves:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let dist = distance(uv, vec2f(0.5, 0.5));
let wave = sin(dist * 20.0 - time * 3.0) * 0.5 + 0.5;
return vec4f(wave, wave * 0.5, 1.0 - wave, 1.0);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Checkerboard morph:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let scale = 10.0;
let checker = step(0.5, fract(uv.x * scale + sin(time))) *
              step(0.5, fract(uv.y * scale + cos(time)));
return vec4f(checker, 1.0 - checker, 0.5, 1.0);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Noise-like pattern:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let n = fract(sin(dot(uv + time * 0.1, vec2f(12.9898, 78.233))) * 43758.5453);
return vec4f(n * 0.8, n * 0.4, n, 1.0);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change one number, save, refresh. You will see the result immediately. This is the best way to learn — experiment and break things.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you just learned
&lt;/h2&gt;

&lt;p&gt;Here is a cheat sheet. Every WebGPU concept mapped to something you already know as a frontend developer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;WebGPU concept&lt;/th&gt;
&lt;th&gt;Analogy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;adapter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Finding which painting agency is available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;device&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Signing a contract with them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shaderModule&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The recipe card you give to every painter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;buffer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An envelope with data (time, screen size) for the painters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bindGroup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Labels on the envelopes so painters know which is which — like props in a component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pipeline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The full work plan: recipe + materials + instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;commandEncoder&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A to-do list you write before handing it to the painters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;device.queue.submit()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Handing the to-do list and saying "go"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same as always — the flipbook loop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When should you use WebGPU?
&lt;/h2&gt;

&lt;p&gt;After building this, you might want to use the GPU for everything. Do not do that.&lt;/p&gt;

&lt;p&gt;The GPU is like a factory: amazing for mass production, terrible for custom one-off work. Use the right tool for the job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use WebGPU when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Processing thousands of pixels, particles, or data points at once&lt;/li&gt;
&lt;li&gt;Running ML models in the browser&lt;/li&gt;
&lt;li&gt;Building visualizations with 100k+ data points&lt;/li&gt;
&lt;li&gt;Real-time image or video filters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stick with JavaScript when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DOM manipulation&lt;/li&gt;
&lt;li&gt;Business logic and API calls&lt;/li&gt;
&lt;li&gt;Anything that is not the same operation repeated thousands of times&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;You now have the foundation. The setup code stays the same — from here, the fun is changing the recipe (shader). Some ideas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add mouse interaction&lt;/strong&gt; — send mouse coordinates as another uniform, so the gradient reacts to your cursor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute shaders&lt;/strong&gt; — use the GPU to process data, not just pixels (great for simulations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Particle systems&lt;/strong&gt; — combine vertex and fragment shaders to move thousands of objects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://webgpu.github.io/webgpu-samples/" rel="noopener noreferrer"&gt;WebGPU Samples&lt;/a&gt;&lt;/strong&gt; — official examples with more advanced patterns&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Questions? Leave a comment — I will answer every one. If you built something cool by changing the shader, I would love to see it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webgpu</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
