<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pop Watch</title>
    <description>The latest articles on DEV Community by Pop Watch (@pop_watch_ac45504c97f6c29).</description>
    <link>https://dev.to/pop_watch_ac45504c97f6c29</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3224322%2F2eacf08a-53b2-4a90-a958-342b90fb028d.png</url>
      <title>DEV Community: Pop Watch</title>
      <link>https://dev.to/pop_watch_ac45504c97f6c29</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pop_watch_ac45504c97f6c29"/>
    <language>en</language>
    <item>
      <title>Resource Timing Is an API Boundary: Measure Third-Party Cost Without Leaking Cross-Origin Detail</title>
      <dc:creator>Pop Watch</dc:creator>
      <pubDate>Fri, 21 Aug 2026 05:07:35 +0000</pubDate>
      <link>https://dev.to/pop_watch_ac45504c97f6c29/resource-timing-is-an-api-boundary-measure-third-party-cost-without-leaking-cross-origin-detail-4p5n</link>
      <guid>https://dev.to/pop_watch_ac45504c97f6c29/resource-timing-is-an-api-boundary-measure-third-party-cost-without-leaking-cross-origin-detail-4p5n</guid>
      <description>&lt;p&gt;Your performance dashboard says a third-party script is slow. Then every phase in its &lt;code&gt;PerformanceResourceTiming&lt;/code&gt; entry is zero: DNS, connection, request, response, even sizes. That is not a broken browser and it is not a CORS failure. It is the Resource Timing privacy model working as designed.&lt;/p&gt;

&lt;p&gt;This matters because third-party JavaScript, fonts, images, analytics, and CDNs are usually where a useful performance investigation starts. The tempting response is to treat &lt;code&gt;performance.getEntriesByType("resource")&lt;/code&gt; as a packet trace. It is not. It is a browser API with deliberately limited cross-origin disclosure.&lt;/p&gt;

&lt;p&gt;This article builds a small, reviewable resource-cost reporter, explains the zeroes, and shows the narrow server-side change that enables detailed timing when you actually control the resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Resource Timing records
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.w3.org/TR/resource-timing/" rel="noopener noreferrer"&gt;Resource Timing specification&lt;/a&gt; defines entries for HTTP(S) resources fetched by a document. Entries can cover markup-driven loads such as scripts, stylesheets, images, iframes, audio, and video, as well as &lt;code&gt;fetch()&lt;/code&gt; and XHR. Cached resources and attempted network fetches can also produce entries, so an entry is evidence that the browser processed a fetch—not proof of a fresh transfer.&lt;/p&gt;

&lt;p&gt;Each &lt;code&gt;PerformanceResourceTiming&lt;/code&gt; entry has a URL, an &lt;code&gt;initiatorType&lt;/code&gt;, a duration, and fields such as &lt;code&gt;transferSize&lt;/code&gt;, &lt;code&gt;encodedBodySize&lt;/code&gt;, &lt;code&gt;requestStart&lt;/code&gt;, and &lt;code&gt;responseEnd&lt;/code&gt;. A useful first pass is to group entries by origin and initiator rather than immediately ranking individual URLs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resourceSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntriesByType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resource&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; | &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initiatorType&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;initiator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initiatorType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;durationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;transferredBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;opaque&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requests&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;durationMs&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transferredBytes&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transferSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transferSize&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestStart&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;responseStart&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opaque&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;durationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;durationMs&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;transferredKiB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transferredBytes&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;durationMs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;durationMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;resourceSummary&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an investigation aid, not a universal accounting system. A resource can be served from cache; one network fetch can satisfy more than one consumer; an iframe owns timing for its own subresources; and summing durations double-counts concurrent work. Treat the table as a way to decide what to inspect next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why cross-origin details disappear
&lt;/h2&gt;

&lt;p&gt;The browser may expose an entry for a cross-origin resource while masking its detailed timestamps and sizes. The relevant gate is &lt;strong&gt;not&lt;/strong&gt; the request's CORS mode. It is the resource server's &lt;code&gt;Timing-Allow-Origin&lt;/code&gt; response header.&lt;/p&gt;

&lt;p&gt;For example, a document at &lt;code&gt;https://app.example&lt;/code&gt; can load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://static.vendor.example/widget.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser can still tell the page that the resource existed and how long the overall operation took. But revealing DNS, connection, request, response, and size information could expose properties of another origin's infrastructure or a user's network path. The &lt;a href="https://www.w3.org/TR/resource-timing/#privacy-considerations" rel="noopener noreferrer"&gt;Resource Timing privacy considerations&lt;/a&gt; explicitly address this cross-origin information boundary.&lt;/p&gt;

&lt;p&gt;That is why a client-side change such as adding &lt;code&gt;crossorigin&lt;/code&gt; to the script tag does not grant timing visibility. &lt;code&gt;crossorigin&lt;/code&gt; controls how the element fetches and whether a response can be used under CORS rules; it does not opt the server into detailed Resource Timing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server opt-in: Timing-Allow-Origin
&lt;/h2&gt;

&lt;p&gt;If you operate the resource origin and want a specific site to receive detailed timings, return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Timing-Allow-Origin: https://app.example
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a resource intentionally measurable from any public origin, the header can be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Timing-Allow-Origin: *
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header is documented in &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Timing-Allow-Origin" rel="noopener noreferrer"&gt;MDN's Timing-Allow-Origin reference&lt;/a&gt;, and its semantics are defined by the Resource Timing specification. It is independent from &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;; deployment commonly needs both headers only when the browser also needs to read the response body via CORS.&lt;/p&gt;

&lt;p&gt;For an Nginx-served static asset, a deliberately narrow configuration might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/assets/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Timing-Allow-Origin&lt;/span&gt; &lt;span class="s"&gt;"https://app.example"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the narrowest origin list that supports your diagnostic goal. &lt;code&gt;*&lt;/code&gt; is convenient, but it makes detailed timing available to every embedding site. Timing alone is less sensitive than response content, yet it can still reveal operational characteristics. Do not add it by habit to a vendor integration you do not control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observe future entries instead of taking one snapshot
&lt;/h2&gt;

&lt;p&gt;A single call to &lt;code&gt;getEntriesByType()&lt;/code&gt; misses resources loaded later by client-side navigation, lazy loading, or user interaction. A &lt;code&gt;PerformanceObserver&lt;/code&gt; makes the reporter useful in a real application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entryType&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resource&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;first-party resource&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initiatorType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;transferSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transferSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resource&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;buffered: true&lt;/code&gt; option includes entries that were already recorded before the observer started. Keep the callback cheap: queue summaries and ship them at controlled points, rather than posting every resource entry from a hot path.&lt;/p&gt;

&lt;p&gt;There is also a finite resource-timing buffer. Long-lived single-page apps should listen for &lt;code&gt;resourcetimingbufferfull&lt;/code&gt;, process the entries they need, and call &lt;code&gt;performance.clearResourceTimings()&lt;/code&gt;. Clearing is local to the page's timeline; it does not clear browser history, caches, or network logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design a performance budget that respects the boundary
&lt;/h2&gt;

&lt;p&gt;Start with signals you can defend:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Count third-party origins and requests.&lt;/li&gt;
&lt;li&gt;Track elapsed duration by origin, while remembering concurrency makes totals non-additive.&lt;/li&gt;
&lt;li&gt;Track transferred bytes only for resources whose origin has granted timing access.&lt;/li&gt;
&lt;li&gt;Keep raw resource URLs out of routine analytics when query strings may contain identifiers or tokens.&lt;/li&gt;
&lt;li&gt;Agree with vendors on a temporary, narrow &lt;code&gt;Timing-Allow-Origin&lt;/code&gt; rollout when phase-level diagnosis is necessary.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last point is usually the fastest route to an answer. Do not try to infer hidden connection phases, and do not treat zero byte fields as a network failure. Ask the resource owner whether it is appropriate to expose timing to your application origin, then verify the response header in a normal browser request.&lt;/p&gt;

&lt;h2&gt;
  
  
  A boundary, not a blind spot
&lt;/h2&gt;

&lt;p&gt;Resource Timing is powerful precisely because it is constrained. It can show that a third-party origin is involved in a slow experience without automatically turning a visitor's browser into a cross-origin network probe.&lt;/p&gt;

&lt;p&gt;Use the API to identify ownership and high-level cost. Use &lt;code&gt;Timing-Allow-Origin&lt;/code&gt; only where there is an explicit operational reason and the resource owner agrees. That produces better measurements—and a performance practice that does not quietly trade away privacy for a few extra timestamps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Primary references
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/resource-timing/" rel="noopener noreferrer"&gt;W3C Resource Timing specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming" rel="noopener noreferrer"&gt;MDN: PerformanceResourceTiming&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Timing-Allow-Origin" rel="noopener noreferrer"&gt;MDN: Timing-Allow-Origin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
    </item>
    <item>
      <title>Treat postMessage Like an API Boundary: A Safer Cross-Window Protocol</title>
      <dc:creator>Pop Watch</dc:creator>
      <pubDate>Mon, 10 Aug 2026 16:03:24 +0000</pubDate>
      <link>https://dev.to/pop_watch_ac45504c97f6c29/treat-postmessage-like-an-api-boundary-a-safer-cross-window-protocol-omo</link>
      <guid>https://dev.to/pop_watch_ac45504c97f6c29/treat-postmessage-like-an-api-boundary-a-safer-cross-window-protocol-omo</guid>
      <description>&lt;p&gt;Modern web apps routinely embed a payment step, an identity provider, an editor, or a support widget from another origin. The browser's same-origin policy prevents those documents from directly reading each other's DOM, but &lt;code&gt;window.postMessage()&lt;/code&gt; gives them a deliberate communication channel.&lt;/p&gt;

&lt;p&gt;That channel is useful precisely because it crosses an origin boundary. It should therefore be designed like an API boundary, not like a convenient callback. A message listener that trusts any sender, accepts any object, and performs work immediately turns a small integration into an input-validation and authorization surface.&lt;/p&gt;

&lt;p&gt;This article builds a small request protocol for a parent page and an embedded frame. It is not a DRM, access-control, or authentication bypass technique; it is a defensive pattern for applications that already control both ends of a legitimate integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three questions every message must answer
&lt;/h2&gt;

&lt;p&gt;When a &lt;code&gt;message&lt;/code&gt; event arrives, answer these questions before acting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Who sent it?&lt;/strong&gt; Check &lt;code&gt;event.origin&lt;/code&gt; against an exact allowlist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which window sent it?&lt;/strong&gt; When the relationship is known, check &lt;code&gt;event.source&lt;/code&gt; against the expected iframe window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is this a message we understand?&lt;/strong&gt; Validate its shape, type, and values before using them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The HTML Standard says authors should check both the origin and the expected data format. It also warns against using &lt;code&gt;"*"&lt;/code&gt; as &lt;code&gt;targetOrigin&lt;/code&gt; for confidential information. Those are separate checks: an approved origin does not make arbitrary JSON safe, and a well-formed payload does not make an unknown sender trustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a narrow protocol
&lt;/h2&gt;

&lt;p&gt;Suppose the parent embeds an account-widget frame at &lt;code&gt;https://widget.example&lt;/code&gt;. The parent wants to tell the frame which &lt;em&gt;non-secret&lt;/em&gt; display theme to use. The frame later reports that it is ready.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt;
  &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"account-widget"&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://widget.example/embed"&lt;/span&gt;
  &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"Account settings"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not send configuration as soon as the iframe element exists. Navigation and listener setup are asynchronous. The HTML Standard specifically recommends a readiness message from a newly created child document before the parent begins posting messages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WIDGET_ORIGIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://widget.example&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#account-widget&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isWidgetMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;WIDGET_ORIGIN&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentWindow&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isWidgetMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;widget.ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;widget.configure&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;WIDGET_ORIGIN&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact target origin matters. With &lt;code&gt;WIDGET_ORIGIN&lt;/code&gt;, the browser discards the message if the target window is no longer at that origin. With &lt;code&gt;"*"&lt;/code&gt;, it would deliver regardless of origin. The latter may be necessary for an opaque origin such as a &lt;code&gt;data:&lt;/code&gt; URL, but that is an architectural exception to document and isolate—not a default for ordinary hosted frames.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate the payload as data, not as intention
&lt;/h2&gt;

&lt;p&gt;The helper above only proves that &lt;code&gt;type&lt;/code&gt; is a string. Production code needs a validator for each command. The validator should create a plain, minimal internal value rather than passing the received object through the application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseWidgetEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;widget.ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;widget.ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;widget.resize&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;widget.resize&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;WIDGET_ORIGIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentWindow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseWidgetEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;widget.resize&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This has useful properties: unknown commands fail closed, range checks are explicit, and the application never interprets a received string as HTML or JavaScript. Avoid patterns such as &lt;code&gt;Object.assign(state, event.data)&lt;/code&gt;, dynamic property dispatch, or feeding received values into &lt;code&gt;innerHTML&lt;/code&gt;. They blur the boundary that the protocol is meant to enforce.&lt;/p&gt;

&lt;p&gt;Schema libraries can make larger protocols more maintainable, but they do not replace origin and source checks. Keep the protocol small enough that its accepted messages can be reviewed as a list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat replies and lifecycle events carefully
&lt;/h2&gt;

&lt;p&gt;A common mistake is to reply with &lt;code&gt;event.source.postMessage(reply, "*")&lt;/code&gt;. The standard's own example replies to &lt;code&gt;event.origin&lt;/code&gt;, which preserves the origin constraint that was observed for that event.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is still a lifecycle edge case: a WindowProxy can survive a navigation while the document inside it changes. On every incoming event, check &lt;code&gt;event.origin&lt;/code&gt; again; do not treat an earlier handshake as permanent authorization. For actions with security consequences, require a fresh, explicit request and enforce authorization on the server or other trusted authority. A browser message is a transport signal, not proof of user intent or permission.&lt;/p&gt;

&lt;p&gt;Also bound the work triggered by messages. The HTML Standard notes that a page accepting messages from any origin can be exposed to denial-of-service if each message causes expensive computation or network traffic. Even when you use an allowlist, coalesce resize events, cap payload sizes, and rate-limit operations that can initiate work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transferables are about ownership
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;postMessage&lt;/code&gt; uses structured cloning. Some values, including &lt;code&gt;ArrayBuffer&lt;/code&gt;, can be transferred instead of cloned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;widget.bytes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;WIDGET_ORIGIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// bytes.buffer is now detached in this window.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transfer can avoid copying large data, but it changes ownership: the sender can no longer use the transferred object. Only transfer buffers you are prepared to relinquish, and keep message size limits so an integration cannot turn memory pressure into a reliability problem. For simple UI coordination, structured, small data is usually the clearer choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  A review checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping a cross-window integration, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every send uses an exact &lt;code&gt;targetOrigin&lt;/code&gt; unless an opaque-origin exception is documented.&lt;/li&gt;
&lt;li&gt;Every receive checks exact &lt;code&gt;event.origin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Known relationships also verify &lt;code&gt;event.source&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Every command has a small, explicit parser with type and range checks.&lt;/li&gt;
&lt;li&gt;Unknown messages do nothing; received content is never executed or injected as HTML.&lt;/li&gt;
&lt;li&gt;Startup uses a readiness handshake rather than timing assumptions.&lt;/li&gt;
&lt;li&gt;High-frequency or expensive commands are bounded.&lt;/li&gt;
&lt;li&gt;Authorization for sensitive actions is enforced outside the message channel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key idea is simple: &lt;code&gt;postMessage&lt;/code&gt; is not just a browser event. It is a cross-origin protocol. Give it the same allowlists, schemas, lifecycle rules, and failure modes you would require of any other public API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://html.spec.whatwg.org/multipage/web-messaging.html" rel="noopener noreferrer"&gt;HTML Standard — Cross-document messaging&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://html.spec.whatwg.org/multipage/comms.html#dom-messageevent-origin" rel="noopener noreferrer"&gt;HTML Standard — &lt;code&gt;MessageEvent.origin&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross_Window_Communication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Cross Window Communication Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Fetch Metadata: A Practical Cross-Site Request Policy for Node.js</title>
      <dc:creator>Pop Watch</dc:creator>
      <pubDate>Tue, 04 Aug 2026 02:03:30 +0000</pubDate>
      <link>https://dev.to/pop_watch_ac45504c97f6c29/fetch-metadata-a-practical-cross-site-request-policy-for-nodejs-1mh5</link>
      <guid>https://dev.to/pop_watch_ac45504c97f6c29/fetch-metadata-a-practical-cross-site-request-policy-for-nodejs-1mh5</guid>
      <description>&lt;p&gt;Most web security controls answer a narrow question. CORS decides whether browser JavaScript may read a cross-origin response. CSRF tokens bind a state-changing request to an application session. Content Security Policy constrains what a page can load.&lt;/p&gt;

&lt;p&gt;Fetch Metadata answers a useful question earlier: &lt;strong&gt;what kind of browser context caused this request?&lt;/strong&gt; It gives a server request headers such as &lt;code&gt;Sec-Fetch-Site&lt;/code&gt;, &lt;code&gt;Sec-Fetch-Mode&lt;/code&gt;, and &lt;code&gt;Sec-Fetch-Dest&lt;/code&gt;. That is enough context to reject many implausible cross-site requests before they reach application code.&lt;/p&gt;

&lt;p&gt;This is not a replacement for authentication, authorization, CSRF defenses, or input validation. It is a small, server-side policy layer that makes unsafe request paths less reachable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal a server actually receives
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.w3.org/TR/fetch-metadata/" rel="noopener noreferrer"&gt;Fetch Metadata specification&lt;/a&gt; defines four request headers. The most useful starting point is &lt;code&gt;Sec-Fetch-Site&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;same-origin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Initiator and target have the same origin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;same-site&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;They are different origins within the same site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cross-site&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The initiator is on another site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;none&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A direct user-agent navigation, such as the address bar or a bookmark&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The browser sets these headers for trustworthy requests. The &lt;code&gt;Sec-&lt;/code&gt; prefix matters: JavaScript cannot set or modify them, so a malicious page cannot simply forge &lt;code&gt;Sec-Fetch-Site: same-origin&lt;/code&gt; with &lt;code&gt;fetch()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two companion headers add context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Sec-Fetch-Mode&lt;/code&gt; distinguishes &lt;code&gt;navigate&lt;/code&gt;, &lt;code&gt;cors&lt;/code&gt;, &lt;code&gt;no-cors&lt;/code&gt;, &lt;code&gt;same-origin&lt;/code&gt;, and &lt;code&gt;websocket&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Sec-Fetch-Dest&lt;/code&gt; says whether the target is a &lt;code&gt;document&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt;, &lt;code&gt;script&lt;/code&gt;, &lt;code&gt;iframe&lt;/code&gt;, or the empty destination used by &lt;code&gt;fetch()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, an image embedded by another site commonly arrives as &lt;code&gt;cross-site&lt;/code&gt; / &lt;code&gt;no-cors&lt;/code&gt; / &lt;code&gt;image&lt;/code&gt;. That is very different from a same-origin API call. The specification also defines &lt;code&gt;Sec-Fetch-User: ?1&lt;/code&gt; for user-activated navigations, but it is only present for such navigations; do not use its absence as proof that a request is automated or hostile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is not CORS
&lt;/h2&gt;

&lt;p&gt;A common mistake is treating CORS as a request firewall. It is not. CORS governs whether browser code can read a response. A cross-site form submission or image request can still reach your server, and ambient credentials may still accompany a request depending on cookie policy and context.&lt;/p&gt;

&lt;p&gt;Fetch Metadata lets the server decide whether to service the request at all. A policy can reject a &lt;code&gt;cross-site&lt;/code&gt; request to a private JSON endpoint even if the caller would never be allowed to read the response. That reduces exposure to cross-site probing and cuts unnecessary work.&lt;/p&gt;

&lt;p&gt;CORS still has a job: explicitly define which origins may read API responses. CSRF protections still have a job: protect cookie-authenticated state changes, including clients that do not send Fetch Metadata. Keep both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a conservative Node.js policy
&lt;/h2&gt;

&lt;p&gt;The following example is framework-neutral middleware for a Node &lt;code&gt;http&lt;/code&gt; server. It blocks cross-site requests by default, while preserving a narrowly scoped public image route. Missing headers are allowed initially for compatibility: older clients, non-browser clients, and some embedded environments may not send them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchMetadataPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sec-fetch-site&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sec-fetch-mode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sec-fetch-dest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="c1"&gt;// Compatibility first: observe clients without this signal.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isCrossSite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cross-site&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isPublicImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/public-images/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-cors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;dest&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isCrossSite&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isPublicImage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/plain; charset=utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Vary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sec-Fetch-Site, Sec-Fetch-Mode, Sec-Fetch-Dest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cross-site request rejected by policy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Place this before routes that parse bodies, query databases, or invoke expensive downstream services. At a CDN or reverse proxy, the same decision can be even cheaper—but only after you have verified that the proxy forwards these request headers and that the policy is identical across origins.&lt;/p&gt;

&lt;p&gt;The example deliberately does &lt;strong&gt;not&lt;/strong&gt; say “block every non-same-origin request.” Many real applications legitimately need same-site subdomains, OAuth callbacks, payment-provider returns, webhooks, mobile clients, or public assets. A simplistic allowlist can break them silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy in two phases
&lt;/h2&gt;

&lt;p&gt;A good policy is discovered from traffic, not guessed from a diagram.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Classify endpoints.&lt;/strong&gt; Mark routes as private APIs, state-changing actions, navigations, public assets, webhooks, or intentionally embeddable resources. Only private and state-changing routes should begin with a restrictive default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observe first.&lt;/strong&gt; Log the method, route class, and the three &lt;code&gt;Sec-Fetch-*&lt;/code&gt; values. Avoid logging cookies, authorization headers, request bodies, or query values. The point is to learn shapes, not collect user data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add explicit exceptions.&lt;/strong&gt; For each expected cross-site path, record why it exists, its allowed method and destination, and an owner. “It fixed a 403 once” is not an exception rationale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce on a small route group.&lt;/strong&gt; Return a clear 403, monitor failures, then expand coverage. Treat a missing header separately from a hostile value until your client inventory supports a stricter mode.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The specification notes that redirects are significant: once a redirect chain crosses sites, &lt;code&gt;Sec-Fetch-Site&lt;/code&gt; can remain &lt;code&gt;cross-site&lt;/code&gt; even if it later returns to your domain. Test login and payment return flows rather than assuming their final URL tells the whole story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache and browser-extension boundaries
&lt;/h2&gt;

&lt;p&gt;If a response changes based on Fetch Metadata, caches must not confuse variants. The specification calls out using &lt;code&gt;Vary&lt;/code&gt; for the relevant request header, for example &lt;code&gt;Vary: Sec-Fetch-Site&lt;/code&gt;. For a denial response, &lt;code&gt;Cache-Control: no-store&lt;/code&gt; avoids accidental reuse. Do not add &lt;code&gt;Vary&lt;/code&gt; mechanically to every response: it can fragment caches. Add it exactly where representation or status differs by the header.&lt;/p&gt;

&lt;p&gt;Browser extensions are another boundary worth testing. Extensions may have permissions that change how requests are represented. Do not broadly exempt extension traffic because it is inconvenient; define a separate authenticated integration path if your product needs one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Fetch Metadata cannot prove
&lt;/h2&gt;

&lt;p&gt;These headers describe browser request context, not identity or intent. They cannot authorize a user, validate a webhook signature, protect a non-browser client, or make an unsafe endpoint safe. They also do not defend against a malicious script running on your own origin.&lt;/p&gt;

&lt;p&gt;Use Fetch Metadata as defense in depth alongside server-side authorization, CSRF tokens or origin checks where appropriate, secure cookie attributes, rate limits, and careful response design. Its real value is architectural: it gives infrastructure a cheap, standardized signal for rejecting requests that should never have reached the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/fetch-metadata/" rel="noopener noreferrer"&gt;W3C: Fetch Metadata Request Headers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site" rel="noopener noreferrer"&gt;MDN: Sec-Fetch-Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS" rel="noopener noreferrer"&gt;MDN: Cross-Origin Resource Sharing (CORS)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Debug UI Jank with the Long Animation Frames API</title>
      <dc:creator>Pop Watch</dc:creator>
      <pubDate>Tue, 28 Jul 2026 01:56:47 +0000</pubDate>
      <link>https://dev.to/pop_watch_ac45504c97f6c29/debug-ui-jank-with-the-long-animation-frames-api-2he9</link>
      <guid>https://dev.to/pop_watch_ac45504c97f6c29/debug-ui-jank-with-the-long-animation-frames-api-2he9</guid>
      <description>&lt;p&gt;A page can feel slow even when its network waterfall looks fine. A click appears to do nothing, a scroll hitches, or an animation skips. The difficult part is often not proving that the main thread was busy; it is identifying what occupied the frame that prevented the next paint.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.w3.org/TR/long-animation-frames/" rel="noopener noreferrer"&gt;Long Animation Frames API&lt;/a&gt; (LoAF) gives browser code a structured way to observe animation frames that take 50 ms or longer. Unlike a task-only view, it reports a frame as a unit: script execution, rendering work, and the time during which the frame blocked responsiveness. That makes it useful for debugging—provided we treat it as evidence, not as a synthetic performance score.&lt;/p&gt;

&lt;p&gt;This article builds a small, privacy-conscious LoAF probe, explains how to interpret it, and shows how to turn one bad frame into a reproducible DevTools investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What LoAF measures
&lt;/h2&gt;

&lt;p&gt;A LoAF entry represents one animation frame whose duration is at least 50 ms. It exposes a duration, a blockingDuration, a rendering start time, and a list of scripts that ran in that frame. Script records can include the script's source URL, function name, invoker type, and timing details.&lt;/p&gt;

&lt;p&gt;That frame-level boundary matters. A single user interaction can be delayed by a handler, framework rendering, style calculation, layout, paint preparation, or several of those in sequence. Looking only for a long JavaScript task can hide the rest of the frame. LoAF does not replace a trace, but it is a compact signal that tells you which timeframe deserves a trace.&lt;/p&gt;

&lt;p&gt;LoAF is related to responsiveness, not identical to &lt;a href="https://web.dev/articles/inp" rel="noopener noreferrer"&gt;Interaction to Next Paint (INP)&lt;/a&gt;. INP is an interaction-centric field metric: it follows an interaction until the next paint. LoAF is frame-centric diagnostic data. A long frame can occur without a user interaction, and a poor interaction can contain work that LoAF does not fully explain. Keep those two questions separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How did users experience the interaction?&lt;/strong&gt; Measure INP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What occupied a suspicious frame?&lt;/strong&gt; Inspect LoAF and then a performance trace.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Start with feature detection
&lt;/h2&gt;

&lt;p&gt;The API is not a portable analytics primitive. Support is browser-dependent, and the standard remains a W3C Working Draft. Do not make application behavior depend on it. Use feature detection and preserve a no-op path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;supportsLoAF&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;supportedEntryTypes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;long-animation-frame&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;supportsLoAF&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LoAF diagnostics are available&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking supportedEntryTypes is preferable to assuming that a browser version supports the entry type. It also keeps the diagnostic code safe to ship behind a development flag or an explicit opt-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small local diagnostic probe
&lt;/h2&gt;

&lt;p&gt;Here is a deliberately modest observer. It prints a compact summary and limits the number of script records so a bad page does not generate an unusable console dump.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;observeLongAnimationFrames&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;maxScripts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;supportedEntryTypes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;long-animation-frame&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scripts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scripts&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxScripts&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sourceURL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inline or unavailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionName&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;invoker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invoker&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;}));&lt;/span&gt;

      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt;
        &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;blocking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blockingDuration&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;renderStart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;renderStart&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nx"&gt;scripts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;long-animation-frame&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stopLoAF&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;observeLongAnimationFrames&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Call stopLoAF() when the diagnostic session ends.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this locally or in a controlled test environment, then reproduce one concrete action: opening a menu, filtering a list, dragging a map, or submitting a form. Avoid “click around for a minute” as a test plan. A named action gives the timestamp and frame data a useful boundary.&lt;/p&gt;

&lt;p&gt;The buffered option asks the observer to receive relevant entries already recorded before the observer started. That is useful when the slow work happens during startup, but it is also a reason to attach the observer early and keep the session short.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the record without over-claiming
&lt;/h2&gt;

&lt;p&gt;Suppose a frame reports a duration near 180 ms and a large blocking duration. Do not immediately rewrite every function listed in scripts. First ask four narrower questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does the frame coincide with the action you reproduced?&lt;/strong&gt; Correlation is not causation. Record the action time or add a temporary console marker immediately before it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is script time dominant?&lt;/strong&gt; If script entries account for little of the frame, inspect rendering work in a DevTools trace rather than optimizing a random callback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the same source and function repeated?&lt;/strong&gt; Repetition across multiple reproductions is stronger evidence than one outlier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the cost first-party, third-party, or browser/framework work?&lt;/strong&gt; The remedy and the ownership differ.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The script list is a lead, not a flame chart. A function can be present because it triggered later work; its own duration may not be the entire cost of the frame. This is why the next step is a targeted trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn a LoAF signal into a trace
&lt;/h2&gt;

&lt;p&gt;Use the observer to find a repeatable slow action, then capture that same action in Chrome DevTools' Performance panel. Align the trace with the LoAF timestamp and inspect the long frame. Look for a concrete mechanism:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;synchronous JavaScript that can be split, deferred, or moved off the main thread;&lt;/li&gt;
&lt;li&gt;repeated layout reads and writes that force layout between updates;&lt;/li&gt;
&lt;li&gt;expensive DOM updates that can be batched or narrowed;&lt;/li&gt;
&lt;li&gt;a third-party callback running during an interaction;&lt;/li&gt;
&lt;li&gt;too much work scheduled in one animation frame.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only then choose an intervention. For example, if a click handler computes a large filter synchronously, moving the computation to a worker may help. If the trace instead shows repeated layout after DOM writes, a worker will not fix the rendering dependency. LoAF is valuable because it prevents this category error early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Respect privacy and operational boundaries
&lt;/h2&gt;

&lt;p&gt;Performance diagnostics can accidentally become telemetry. The script sourceURL may expose application paths, query strings, or third-party origins. Function names and timestamps can also reveal implementation details. Treat LoAF records as debug data.&lt;/p&gt;

&lt;p&gt;A conservative production policy is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep full records in local development or an authenticated internal diagnostic tool;&lt;/li&gt;
&lt;li&gt;if collecting aggregated field signals, minimize them to coarse durations and a first-party category—not raw URLs, function names, page content, or interaction text;&lt;/li&gt;
&lt;li&gt;sample deliberately, set a retention limit, document the purpose, and honor your product's consent and privacy requirements;&lt;/li&gt;
&lt;li&gt;never use this API to fingerprint users or infer behavior from timing traces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The API is also not a substitute for accessibility testing, real-device testing, or user-facing metrics. A 50 ms frame threshold is an engineering signal, not a promise that the page feels good.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical debugging loop
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Add the observer behind a debug flag.&lt;/li&gt;
&lt;li&gt;Reproduce one named interaction three times.&lt;/li&gt;
&lt;li&gt;Group the frames by action, source, and rough duration.&lt;/li&gt;
&lt;li&gt;Capture one matching DevTools trace.&lt;/li&gt;
&lt;li&gt;Fix the dominant mechanism, then repeat the same interaction.&lt;/li&gt;
&lt;li&gt;Remove or disable the probe when the investigation ends.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This loop is intentionally boring. It avoids the common failure mode of treating every performance entry as an optimization mandate. Good debugging begins with a precise observation, confirms the mechanism in a trace, and changes only the work that is actually on the critical path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/long-animation-frames/" rel="noopener noreferrer"&gt;W3C: Long Animation Frames API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/web-platform/long-animation-frames" rel="noopener noreferrer"&gt;Chrome for Developers: Long Animation Frames API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.dev/articles/inp" rel="noopener noreferrer"&gt;web.dev: Interaction to Next Paint&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
    <item>
      <title>Stop Letting Stale Requests Win: A Practical AbortController Pattern</title>
      <dc:creator>Pop Watch</dc:creator>
      <pubDate>Tue, 21 Jul 2026 02:14:58 +0000</pubDate>
      <link>https://dev.to/pop_watch_ac45504c97f6c29/stop-letting-stale-requests-win-a-practical-abortcontroller-pattern-nfa</link>
      <guid>https://dev.to/pop_watch_ac45504c97f6c29/stop-letting-stale-requests-win-a-practical-abortcontroller-pattern-nfa</guid>
      <description>&lt;p&gt;Modern interfaces make requests constantly: type-ahead search, filters, route changes, live validation, refresh buttons. The hard part is not starting a request. It is deciding which result is still allowed to change the UI.&lt;/p&gt;

&lt;p&gt;A familiar bug looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A user types &lt;code&gt;ca&lt;/code&gt;, then quickly types &lt;code&gt;cat&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The request for &lt;code&gt;cat&lt;/code&gt; returns first and renders the correct results.&lt;/li&gt;
&lt;li&gt;The older &lt;code&gt;ca&lt;/code&gt; request returns later and overwrites them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is a correctness problem, not merely a performance problem. A loading spinner can be accurate while the screen is wrong.&lt;/p&gt;

&lt;p&gt;The platform answer is &lt;code&gt;AbortController&lt;/code&gt;. It gives an operation an &lt;code&gt;AbortSignal&lt;/code&gt;; APIs that support the signal can stop their work when the controller is aborted. Fetch accepts a signal, and the DOM Standard also defines signals as a general cancellation mechanism—not a fetch-only feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule: one controller per current intent
&lt;/h2&gt;

&lt;p&gt;Treat a controller as belonging to one user intent. When intent changes, abort the old controller before creating the next one. Do not share one controller across unrelated operations: once aborted, its signal stays aborted.&lt;/p&gt;

&lt;p&gt;Here is a small search component with no framework assumptions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#results&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// The new input makes the previous result obsolete.&lt;/span&gt;
  &lt;span class="nx"&gt;currentController&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceChildren&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;currentController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`/api/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// A defensive final check: only the current intent may render.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentController&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceChildren&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;li&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;li&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;li&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;li&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Search failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details make this dependable.&lt;/p&gt;

&lt;p&gt;First, cancellation is not an error state for the user. The catch block deliberately ignores the request that this component chose to supersede. Log and present actual failures separately.&lt;/p&gt;

&lt;p&gt;Second, cancellation is cooperative. Calling &lt;code&gt;abort()&lt;/code&gt; tells a participating API to stop; it does not revoke bytes that may already have reached a server, undo a completed mutation, or make a non-signal-aware library stop. That is why this pattern is ideal for read requests and UI work, but it is not a substitute for server-side idempotency or authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a deadline without tangled timers
&lt;/h2&gt;

&lt;p&gt;A request can become obsolete because the user changed their mind, or because it has taken too long. Where supported, &lt;code&gt;AbortSignal.timeout(ms)&lt;/code&gt; provides the latter. Combine the two reasons with &lt;code&gt;AbortSignal.any()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userSignal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;userSignal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caller still owns the controller for “the user navigated away”; the function owns its eight-second service expectation. That separation makes cleanup easier to reason about.&lt;/p&gt;

&lt;p&gt;Do not write timeout code that races &lt;code&gt;fetch()&lt;/code&gt; against a promise and then forgets the underlying fetch. A promise race decides what your &lt;code&gt;await&lt;/code&gt; observes; it does not automatically stop network work. A signal passed to &lt;code&gt;fetch&lt;/code&gt; expresses cancellation to the fetch operation itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use signals beyond fetch
&lt;/h2&gt;

&lt;p&gt;Signals also help with event listener cleanup. The DOM Standard permits an &lt;code&gt;AbortSignal&lt;/code&gt; in listener options. A modal or temporary view can register several listeners with one signal, then remove them as a group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mountDialog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Escape&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially useful when a component has a clear lifetime but its listeners live on &lt;code&gt;document&lt;/code&gt;, &lt;code&gt;window&lt;/code&gt;, or another long-lived target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundaries that cancellation does not solve
&lt;/h2&gt;

&lt;p&gt;Cancellation is easy to over-credit. Keep these boundaries explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It does not secure an endpoint. The server must authenticate and authorize every request even if the UI cancels it.&lt;/li&gt;
&lt;li&gt;It does not roll back writes. For create, payment, or update operations, design server-side idempotency and explicit user-visible state.&lt;/li&gt;
&lt;li&gt;It does not guarantee a server saw nothing. An abort can occur after request transmission has begun.&lt;/li&gt;
&lt;li&gt;It does not replace input validation, output encoding, or rate limiting.&lt;/li&gt;
&lt;li&gt;It does not make every API cancelable. Check an API’s documentation before assuming it accepts &lt;code&gt;signal&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For accessibility, avoid announcing an aborted background request as a failure. Keep a visible loading state tied to the active controller, and preserve keyboard focus when replacing results. For privacy, do not put sensitive text in query strings merely because a request is short-lived; cancellation does not erase URL logs, browser history, or intermediary records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the behaviour, not the timing
&lt;/h2&gt;

&lt;p&gt;You do not need a slow production API to test this pattern. In an integration test, make two controlled promises for the transport layer. Resolve the second one first, then resolve the first one. The assertion is that only the second result reaches the renderer. Separately assert that replacing an active intent calls &lt;code&gt;abort()&lt;/code&gt;, and that an aborted path does not show an error toast.&lt;/p&gt;

&lt;p&gt;Avoid tests that say “wait 500 ms and hope the order reverses.” They are timing tests, so they become flaky under load. Control completion order instead. If your application wraps &lt;code&gt;fetch&lt;/code&gt;, inject that wrapper in the test and capture the &lt;code&gt;signal&lt;/code&gt; argument. You can assert &lt;code&gt;signal.aborted&lt;/code&gt; after a new search begins without depending on a real network.&lt;/p&gt;

&lt;p&gt;Also test cleanup on navigation or component unmount. A page may no longer be visible when a response settles; the old task must not mutate a detached view or announce stale content to assistive technology. This is a lifecycle contract, not a micro-optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short review checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping a cancellation path, ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What user intent owns this controller?&lt;/li&gt;
&lt;li&gt;Which newer action makes it obsolete?&lt;/li&gt;
&lt;li&gt;Is an abort quiet while a real network or HTTP failure remains observable?&lt;/li&gt;
&lt;li&gt;Can an old response still render after parsing?&lt;/li&gt;
&lt;li&gt;Are writes protected by server-side idempotency rather than client cancellation?&lt;/li&gt;
&lt;li&gt;Does the target browser support the signal helpers you use, or is there a documented fallback?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The useful mental model is simple: a request is not “the latest” because it started last. It is latest only while the intent that created it is still current. &lt;code&gt;AbortController&lt;/code&gt; lets your code model that fact directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dom.spec.whatwg.org/#aborting-ongoing-activities" rel="noopener noreferrer"&gt;DOM Standard: aborting ongoing activities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fetch.spec.whatwg.org/#abort-fetch" rel="noopener noreferrer"&gt;Fetch Standard: aborting a fetch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dom.spec.whatwg.org/#dictdef-addeventlisteneroptions" rel="noopener noreferrer"&gt;DOM Standard: event listener options and AbortSignal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dom.spec.whatwg.org/#abortsignal-composition" rel="noopener noreferrer"&gt;DOM Standard: AbortSignal.timeout() and AbortSignal.any()&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>HLS Is Not DRM: How Adaptive Streaming and Encrypted Playback Actually Fit Together</title>
      <dc:creator>Pop Watch</dc:creator>
      <pubDate>Mon, 20 Jul 2026 06:40:33 +0000</pubDate>
      <link>https://dev.to/pop_watch_ac45504c97f6c29/hls-is-not-drm-how-adaptive-streaming-and-encrypted-playback-actually-fit-together-5hh2</link>
      <guid>https://dev.to/pop_watch_ac45504c97f6c29/hls-is-not-drm-how-adaptive-streaming-and-encrypted-playback-actually-fit-together-5hh2</guid>
      <description>&lt;p&gt;Open DevTools on a modern video site and you may see an &lt;code&gt;.m3u8&lt;/code&gt; playlist, dozens of small media requests, and a player that changes quality without reloading the page. It is tempting to describe all of that as “DRM.” That shortcut causes a lot of confusion.&lt;/p&gt;

&lt;p&gt;HLS and DRM solve different problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HLS delivers media efficiently.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Encryption protects media bytes.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DRM controls whether an authorized playback session receives usable keys.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Understanding those layers makes video-player bugs much easier to diagnose—and explains why finding a playlist is not the same as being able to play or save its media.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic HLS pipeline
&lt;/h2&gt;

&lt;p&gt;HTTP Live Streaming (HLS) was designed to deliver live and on-demand media over ordinary HTTP infrastructure. Instead of transferring one large video file, the server exposes playlists that point to a sequence of smaller media segments.&lt;/p&gt;

&lt;p&gt;A minimal media playlist looks roughly 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;#EXTM3U
#EXT-X-TARGETDURATION:6
#EXTINF:6.0,
segment-001.ts
#EXTINF:6.0,
segment-002.ts
#EXTINF:4.5,
segment-003.ts
#EXT-X-ENDLIST
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The player downloads the playlist, requests the listed segments, and feeds them to its media pipeline in order. Live playlists are refreshed so the player can discover newly added segments.&lt;/p&gt;

&lt;p&gt;HLS commonly uses two playlist levels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;master playlist&lt;/strong&gt; describes alternative renditions, such as 360p, 720p, and 1080p streams.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;media playlist&lt;/strong&gt; lists the segments for one specific rendition.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That separation enables adaptive bitrate streaming. The player can choose a lower-bitrate variant when bandwidth drops, then move back to a higher-quality variant when conditions improve. Apple describes this ability to adapt dynamically to network conditions as one of HLS's core features.&lt;/p&gt;

&lt;p&gt;The media itself may be packaged as MPEG-2 Transport Stream (&lt;code&gt;.ts&lt;/code&gt;) segments or fragmented MP4 (&lt;code&gt;fMP4&lt;/code&gt;) segments. With fMP4, an initialization section supplies track metadata, while later fragments contain timed media samples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where encryption enters the picture
&lt;/h2&gt;

&lt;p&gt;HLS can deliver unencrypted or encrypted segments. RFC 8216 defines the &lt;code&gt;EXT-X-KEY&lt;/code&gt; tag, which tells the client how encryption applies to following media segments.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#EXT-X-KEY:METHOD=AES-128,URI="https://media.example.com/key"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tag does not mean that the playlist itself contains the key. It tells the player which method is in use and where the relevant key information may be obtained. Access to that URI may still depend on cookies, authorization headers, signed URLs, short expiration windows, or other server-side checks.&lt;/p&gt;

&lt;p&gt;This is an important distinction: &lt;strong&gt;encryption is a cryptographic property of the media; authorization is a policy decision about who receives what is needed to decrypt it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some HLS encryption setups are relatively direct. Others are part of a full DRM system such as FairPlay, Widevine, or PlayReady. Those systems add a license protocol, device and browser integration, output restrictions, and policy enforcement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What DRM adds
&lt;/h2&gt;

&lt;p&gt;In a browser, protected playback is commonly coordinated through the W3C Encrypted Media Extensions (EME) API. EME extends the normal HTML media element with a standardized way for a web application to interact with a key system.&lt;/p&gt;

&lt;p&gt;A simplified flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The media contains encryption initialization data.&lt;/li&gt;
&lt;li&gt;The player detects that data and creates a media-key session.&lt;/li&gt;
&lt;li&gt;A Content Decryption Module (CDM) produces a license challenge.&lt;/li&gt;
&lt;li&gt;The application sends that challenge to a license server.&lt;/li&gt;
&lt;li&gt;The license server evaluates authentication, entitlement, device, and policy information.&lt;/li&gt;
&lt;li&gt;If authorized, it returns a license that the CDM can use for playback.&lt;/li&gt;
&lt;li&gt;The CDM decrypts media inside the protected playback path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The JavaScript application coordinates the exchange, but it does not necessarily receive raw content keys. That separation is one of the main reasons DRM is not equivalent to “an encrypted &lt;code&gt;.m3u8&lt;/code&gt; file.”&lt;/p&gt;

&lt;p&gt;The same HLS transport concepts—playlists, variants, and segments—can exist with or without DRM. DRM sits alongside the delivery pipeline and governs protected playback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a playlist URL may stop working
&lt;/h2&gt;

&lt;p&gt;Developers often reproduce a media request outside the original page and get &lt;code&gt;401&lt;/code&gt;, &lt;code&gt;403&lt;/code&gt;, an expired response, or segments that cannot be decoded. Several independent mechanisms can cause this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Signed URLs:&lt;/strong&gt; playlist or segment URLs may expire quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session cookies:&lt;/strong&gt; requests may only work inside an authenticated browser session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request headers:&lt;/strong&gt; the CDN may validate an authorization header, origin, or referrer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token rotation:&lt;/strong&gt; refreshed playlists may contain newly signed segment URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encryption:&lt;/strong&gt; downloaded bytes may be unusable without the correct key or license.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DRM policy:&lt;/strong&gt; the license may restrict duration, device class, output, or offline use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codec/container mismatch:&lt;/strong&gt; a valid segment is not automatically playable in every environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These failures look similar from the outside, but they occur at different layers. A network authorization failure should not be debugged as a codec problem, and a CDM license failure should not be treated as a missing playlist.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical debugging model
&lt;/h2&gt;

&lt;p&gt;When you own or are authorized to test the stream, debug it layer by layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Manifest layer
&lt;/h3&gt;

&lt;p&gt;Confirm whether you are looking at a master playlist or media playlist. Inspect variant bandwidths, codecs, resolution attributes, target duration, media sequence numbers, and discontinuity tags.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Network layer
&lt;/h3&gt;

&lt;p&gt;Check playlist and segment status codes, redirects, content types, CORS headers, cache behavior, and URL expiration. For live playback, verify that playlist reloads continue to expose new segments.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Media layer
&lt;/h3&gt;

&lt;p&gt;Verify that initialization data is available, timestamps are continuous, and the codec string matches what the browser supports. Discontinuities must be signaled correctly or playback can stall.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Encryption and DRM layer
&lt;/h3&gt;

&lt;p&gt;Look for &lt;code&gt;EXT-X-KEY&lt;/code&gt;, encryption initialization data, EME events, license requests, and CDM errors. Treat the license exchange as a separate authenticated service, not as an ordinary media request.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Application layer
&lt;/h3&gt;

&lt;p&gt;Finally, inspect player state transitions, buffer health, quality-switch decisions, and error handling. A player can fail even when every individual HTTP request returns &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This layered model is faster than treating “video will not play” as one undifferentiated problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The legal and ethical boundary
&lt;/h2&gt;

&lt;p&gt;Being able to inspect browser requests does not grant permission to copy or redistribute media. Developers should test streams they own, public test vectors, or content they are explicitly authorized to access. Do not bypass access controls, extract protected keys, or defeat DRM policy.&lt;/p&gt;

&lt;p&gt;There are legitimate reasons to work with HLS media: operating your own streaming service, debugging a player, creating accessibility workflows, producing authorized archives, or supporting offline access that the publisher permits. The technical design should preserve the same authorization boundaries as the original service.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key takeaway
&lt;/h2&gt;

&lt;p&gt;HLS is a delivery protocol, not a DRM system. It organizes media into playlists and segments and lets players adapt quality to network conditions. Encryption protects segment bytes. DRM adds license decisions and a protected decryption path.&lt;/p&gt;

&lt;p&gt;Once those responsibilities are separated, the streaming stack becomes much less mysterious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HLS playlists and segments
        ↓
HTTP/CDN delivery and authorization
        ↓
Encryption metadata
        ↓
EME + CDM + license server (when DRM is used)
        ↓
Authorized media playback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8216.html" rel="noopener noreferrer"&gt;RFC 8216: HTTP Live Streaming&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/documentation/HTTP-Live-Streaming" rel="noopener noreferrer"&gt;Apple Developer: HTTP Live Streaming&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/encrypted-media-2/" rel="noopener noreferrer"&gt;W3C: Encrypted Media Extensions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Encrypted_Media_Extensions_API" rel="noopener noreferrer"&gt;MDN: Encrypted Media Extensions API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Local AI Email Generation That Actually Respects Privacy</title>
      <dc:creator>Pop Watch</dc:creator>
      <pubDate>Mon, 07 Jul 2025 07:46:27 +0000</pubDate>
      <link>https://dev.to/pop_watch_ac45504c97f6c29/local-ai-email-generation-that-actually-respects-privacy-192h</link>
      <guid>https://dev.to/pop_watch_ac45504c97f6c29/local-ai-email-generation-that-actually-respects-privacy-192h</guid>
      <description>&lt;p&gt;The Problem with Current AI Email Tools&lt;br&gt;
Most AI email generators are black boxes that send your sensitive data to third-party servers. As developers, we know this creates privacy risks and compliance headaches, especially for B2B communications.&lt;br&gt;
Our Solution: Local Processing + Smart Integration&lt;br&gt;
EffiMail processes everything locally on your device. No data leaves your machine, making it truly GDPR/CCPA compliant by design.&lt;br&gt;
Technical Highlights:&lt;br&gt;
🔒 Privacy-First Architecture&lt;br&gt;
All AI processing happens client-side&lt;br&gt;
Zero data transmission to external servers&lt;br&gt;
Local storage for all email templates and tracking data&lt;br&gt;
⚡ Seamless Integration&lt;br&gt;
Native Gmail/Outlook API integration&lt;br&gt;
CRM connectors (Salesforce, HubSpot)&lt;br&gt;
RESTful APIs for custom workflows&lt;br&gt;
🎯 Invisible Tracking&lt;br&gt;
No visible signatures or tracking pixels&lt;br&gt;
Comprehensive engagement analytics&lt;br&gt;
Real-time notifications without compromising professionalism&lt;br&gt;
🤖 Advanced Prompt Engineering&lt;br&gt;
Proprietary system for brand voice consistency&lt;br&gt;
Context-aware personalization&lt;br&gt;
A/B testing capabilities built-in&lt;br&gt;
Why Developers Love It&lt;br&gt;
Zero setup friction - Works out of the box&lt;br&gt;
API-first design - Easy to integrate into existing workflows&lt;br&gt;
Local-first approach - No vendor lock-in concerns&lt;br&gt;
Enterprise-grade security - Perfect for sensitive communications&lt;br&gt;
The Results&lt;br&gt;
4.8/5 rating from 1500+ users. Core features completely free.&lt;br&gt;
What's Next?&lt;br&gt;
We're working on open-source components and better API documentation. Would love feedback from the dev community on what integrations you'd find most useful.&lt;br&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%2F63w9kz3t911y2gm2zcso.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%2F63w9kz3t911y2gm2zcso.png" alt=" " width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How AI-Powered Content Filtering Helped 50K+ Users Overcome Digital Addiction</title>
      <dc:creator>Pop Watch</dc:creator>
      <pubDate>Mon, 07 Jul 2025 07:44:08 +0000</pubDate>
      <link>https://dev.to/pop_watch_ac45504c97f6c29/how-ai-powered-content-filtering-helped-50k-users-overcome-digital-addiction-3e5n</link>
      <guid>https://dev.to/pop_watch_ac45504c97f6c29/how-ai-powered-content-filtering-helped-50k-users-overcome-digital-addiction-3e5n</guid>
      <description>&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%2Fnjavt979shok6ru3iwx2.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%2Fnjavt979shok6ru3iwx2.png" alt=" " width="800" height="356"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." 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/..." alt="Uploading image" width="800" height="400"&gt;&lt;/a&gt;#ai #productivity #webdev #opensource&lt;br&gt;
Content:&lt;br&gt;
As developers, we often build tools to solve our own problems. StopX started as my personal battle against digital addiction and evolved into a comprehensive platform serving 50,000+ users worldwide.&lt;br&gt;
The Problem 🎯&lt;br&gt;
Digital addiction affects millions, but existing solutions were either too basic (static blocklists) or too invasive (complete internet blocking). I needed something smarter.&lt;br&gt;
The Solution 🚀&lt;br&gt;
StopX combines AI-powered content filtering with holistic recovery tools:&lt;br&gt;
Technical Architecture&lt;br&gt;
AI Content Analysis: Real-time filtering with 99.7% accuracy using machine learning&lt;br&gt;
Cross-Platform Sync: Works across Chrome, Firefox, Android, iOS, Windows, Mac&lt;br&gt;
Next.js 14 + TypeScript: Modern web stack with server-side rendering&lt;br&gt;
Multi-language Support: 7 languages with i18n optimization&lt;br&gt;
Privacy-First: End-to-end encryption, no data tracking&lt;br&gt;
Key Features&lt;br&gt;
Apply&lt;br&gt;
;&lt;br&gt;
Free NoFap Tracker: Progress analytics with streak visualization&lt;br&gt;
Behavioral Pattern Recognition: AI learns individual triggers&lt;br&gt;
Evidence-Based Resources: 90-day recovery timeline with scientific backing&lt;br&gt;
Accountability System: Optional partner monitoring&lt;br&gt;
Results 📊&lt;br&gt;
92% success rate within 3 months&lt;br&gt;
50K+ active users across 120 countries&lt;br&gt;
1M+ monthly page views on educational content&lt;br&gt;
84% retention rate after 6 months&lt;br&gt;
Technical Challenges &amp;amp; Solutions 🔧&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Real-Time Content Analysis
Challenge: Analyzing web content without blocking legitimate sites
Solution: Multi-layer AI model with contextual understanding&lt;/li&gt;
&lt;li&gt;Cross-Platform Synchronization
Challenge: Seamless protection across all devices
Solution: WebSocket-based real-time sync with offline support&lt;/li&gt;
&lt;li&gt;Privacy vs. Functionality
Challenge: Effective filtering while maintaining user privacy
Solution: Local processing with encrypted cloud backup
Open Source Components 🌟
While the core AI model is proprietary, we've open-sourced several components:
Content analysis utilities
Cross-platform sync libraries
Privacy-preserving analytics tools
Lessons Learned 💡
AI isn't magic - Required extensive training on edge cases
Privacy matters - Users won't adopt tools that feel invasive
Community drives success - User feedback shaped 80% of features
Free tier is crucial - Accessibility leads to better outcomes
What's Next 🔮
Mobile Apps: Native iOS/Android with enhanced AI
Wearable Integration: Smartwatch notifications and tracking
API Platform: Third-party integrations for wellness apps
Research Partnerships: Academic collaborations on digital wellness
Try It Out 🎮
The platform offers both free and premium tiers. The free NoFap tracker alone has helped thousands start their recovery journey.
Tech Stack: Next.js 14, TypeScript, Tailwind CSS, AI/ML APIs
Performance: &amp;lt;2s load times globally, 99.9% uptime
Scale: Handles 10M+ daily requests
Discussion Questions 💬
How do you handle digital wellness in your development workflow?
What's your experience with AI-powered content filtering?
Any suggestions for improving cross-platform synchronization?
Building StopX taught me that the best developer tools solve real human problems. Sometimes the most impactful code isn't the most complex - it's the most compassionate.
What developer tools have you built to solve your own challenges?&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
    <item>
      <title>stopx</title>
      <dc:creator>Pop Watch</dc:creator>
      <pubDate>Mon, 07 Jul 2025 07:43:00 +0000</pubDate>
      <link>https://dev.to/pop_watch_ac45504c97f6c29/stopx-2pef</link>
      <guid>https://dev.to/pop_watch_ac45504c97f6c29/stopx-2pef</guid>
      <description></description>
    </item>
    <item>
      <title>🚀 Introducing StopX: A Comprehensive Digital Wellness Platform for Breaking Harmful Habits</title>
      <dc:creator>Pop Watch</dc:creator>
      <pubDate>Tue, 24 Jun 2025 04:29:55 +0000</pubDate>
      <link>https://dev.to/pop_watch_ac45504c97f6c29/introducing-stopx-a-comprehensive-digital-wellness-platform-for-breaking-harmful-habits-12n4</link>
      <guid>https://dev.to/pop_watch_ac45504c97f6c29/introducing-stopx-a-comprehensive-digital-wellness-platform-for-breaking-harmful-habits-12n4</guid>
      <description>&lt;p&gt;🎯 What is StopX?&lt;/p&gt;

&lt;p&gt;StopX is a free, open-source digital wellness platform designed to help individuals overcome&lt;br&gt;
  pornography addiction and build healthier digital habits. Built with modern web technologies, it&lt;br&gt;
  provides evidence-based resources, practical tools, and community support for recovery.&lt;/p&gt;

&lt;p&gt;🛠️ Tech Stack&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend: Next.js 15, TypeScript, Tailwind CSS&lt;/li&gt;
&lt;li&gt;Internationalization: Multi-language support (EN/ZH/JA)&lt;/li&gt;
&lt;li&gt;Architecture: Server-side rendering with dynamic content loading&lt;/li&gt;
&lt;li&gt;Performance: Optimized for speed and accessibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✨ Key Features&lt;/p&gt;

&lt;p&gt;📚 Educational Resources&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Comprehensive NoFap Guide: 90-day recovery timeline with scientific backing&lt;/li&gt;
&lt;li&gt;Flatline Management: Detailed guidance for handling withdrawal symptoms&lt;/li&gt;
&lt;li&gt;Recovery Strategies: Evidence-based coping mechanisms and lifestyle changes&lt;/li&gt;
&lt;li&gt;Myth-busting Articles: Separating facts from fiction in addiction recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🛡️ Practical Tools&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NoFap Tracker: Progress monitoring with streak counting and milestone celebrations&lt;/li&gt;
&lt;li&gt;Content Blocker Reviews: Detailed comparisons of browser extensions and apps&lt;/li&gt;
&lt;li&gt;Habit Builder: Systematic approach to replacing harmful habits with positive ones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🌍 Accessibility&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-language Support: Available in English, Chinese, and Japanese&lt;/li&gt;
&lt;li&gt;Mobile-first Design: Responsive across all devices&lt;/li&gt;
&lt;li&gt;SEO Optimized: Easy to discover when people need help&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;// Type-safe translations&lt;br&gt;
  interface ContentData {&lt;br&gt;
    metadata: ArticleMetadata;&lt;br&gt;
    content: ContentSection[];&lt;br&gt;
    relatedArticles: RelatedArticle[];&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;🎨 Design Philosophy&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User-Centric Approach&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every feature is designed with the user's recovery journey in mind, providing actionable guidance&lt;br&gt;
  rather than just theory.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Evidence-Based Content&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All articles and recommendations are backed by scientific research and community-validated&lt;br&gt;
  experiences.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Privacy-First&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No user tracking, no data collection - just helpful resources when you need them.&lt;/p&gt;

&lt;p&gt;🚀 What's Next?&lt;/p&gt;

&lt;p&gt;Upcoming Features&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Community Forum: Peer support and discussion platform&lt;/li&gt;
&lt;li&gt;Mobile App: Native iOS/Android applications&lt;/li&gt;
&lt;li&gt;Advanced Analytics: Personal recovery insights and trends&lt;/li&gt;
&lt;li&gt;Professional Resources: Tools for therapists and counselors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open Source Contributions&lt;/p&gt;

&lt;p&gt;We welcome contributions from the developer community:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Content translations&lt;/li&gt;
&lt;li&gt;Feature enhancements&lt;/li&gt;
&lt;li&gt;Bug fixes and optimizations&lt;/li&gt;
&lt;li&gt;UI/UX improvements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💻 For Developers&lt;/p&gt;

&lt;p&gt;If you're interested in digital wellness technology or want to contribute to a meaningful cause:&lt;/p&gt;

&lt;p&gt;# Clone the repository&lt;br&gt;
  git clone &lt;a href="https://github.com/stopx-org/website" rel="noopener noreferrer"&gt;https://github.com/stopx-org/website&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;# Install dependencies&lt;br&gt;
  npm install&lt;/p&gt;

&lt;p&gt;# Start development server&lt;br&gt;
  npm run dev&lt;/p&gt;

&lt;p&gt;Key areas for contribution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React/Next.js components&lt;/li&gt;
&lt;li&gt;Internationalization improvements&lt;/li&gt;
&lt;li&gt;Performance optimizations&lt;/li&gt;
&lt;li&gt;Accessibility enhancements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔗 Links&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stopx.today/" rel="noopener noreferrer"&gt;https://stopx.today/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💭 Final Thoughts&lt;/p&gt;

&lt;p&gt;Building StopX has been an incredible journey of combining technology with genuine human impact. In&lt;br&gt;
  an era where digital wellness is more important than ever, we believe that open-source,&lt;br&gt;
  evidence-based solutions can make a real difference.&lt;/p&gt;

&lt;p&gt;Have you worked on similar digital wellness projects? What challenges did you face, and how did you &lt;br&gt;
  solve them? I'd love to hear your experiences in the comments!&lt;/p&gt;




&lt;p&gt;Tags: #digitalwellness #nextjs #typescript #opensource #mentalhealth #webdev #react &lt;/p&gt;

</description>
    </item>
    <item>
      <title>Building a Privacy-First Content Filtering Platform: The StopX Story</title>
      <dc:creator>Pop Watch</dc:creator>
      <pubDate>Wed, 18 Jun 2025 06:50:19 +0000</pubDate>
      <link>https://dev.to/pop_watch_ac45504c97f6c29/building-a-privacy-first-content-filtering-platform-the-stopx-story-54nk</link>
      <guid>https://dev.to/pop_watch_ac45504c97f6c29/building-a-privacy-first-content-filtering-platform-the-stopx-story-54nk</guid>
      <description>&lt;p&gt;&lt;a href="https://stopx.today/" rel="noopener noreferrer"&gt;https://stopx.today/&lt;/a&gt;&lt;br&gt;
In an era where digital wellness has become paramount, building effective content filtering software presents unique engineering challenges. Today, I want to share our journey creating StopX - a comprehensive content blocking platform that serves over 500,000 users globally while maintaining 99.7% filtering accuracy and zero-log privacy standards.&lt;br&gt;
The Technical Challenge&lt;br&gt;
When we started building StopX, we faced several critical requirements:&lt;br&gt;
Real-time content analysis across millions of web pages&lt;br&gt;
Cross-platform synchronization between browsers, mobile apps, and desktop applications&lt;br&gt;
Multilingual support for global accessibility (5+ languages)&lt;br&gt;
Privacy-first architecture that processes content locally&lt;br&gt;
Bypass-resistant protection that maintains effectiveness&lt;br&gt;
These requirements pushed us to architect a solution that balances technical sophistication with user accessibility.&lt;br&gt;
Architecture Overview&lt;br&gt;
Our tech stack prioritizes performance, scalability, and developer experience:&lt;br&gt;
Frontend: Next.js 15 with TypeScript for robust type safety&lt;br&gt;
Internationalization: next-intl for comprehensive multilingual support&lt;br&gt;
UI Components: Radix UI primitives with Tailwind CSS&lt;br&gt;
Content Management: JSON-based system enabling rapid iteration&lt;br&gt;
AI Processing: Custom WebShield™ technology combining computer vision and NLP&lt;br&gt;
The AI-Powered Filtering Engine&lt;br&gt;
The core innovation lies in our proprietary WebShield™ technology, which processes content through multiple analysis layers:&lt;br&gt;
Apply to complete-met...&lt;br&gt;
}&lt;br&gt;
Our filtering system operates on four distinct layers:&lt;br&gt;
URL Pattern Analysis: ML models trained on millions of URLs identify explicit content patterns, even on previously unknown domains.&lt;br&gt;
Computer Vision Processing: Real-time image analysis detects explicit visual content with sub-second response times.&lt;br&gt;
Natural Language Processing: Contextual analysis identifies adult content through semantic understanding rather than keyword matching.&lt;br&gt;
Behavioral Pattern Recognition: Analyzes DOM structure and interaction patterns to identify explicit content delivery mechanisms.&lt;br&gt;
Cross-Platform Architecture Challenge&lt;br&gt;
One of StopX's key differentiators is seamless operation across platforms:&lt;br&gt;
Browser Extensions: Chrome and Firefox extensions built with Manifest V3&lt;br&gt;
Mobile Applications: React Native apps for iOS and Android&lt;br&gt;
Desktop Applications: Electron-based apps for Windows and macOS&lt;br&gt;
Synchronization Layer: Real-time encrypted sync across all devices&lt;br&gt;
The synchronization challenge was particularly complex. We needed to ensure that a user's blocking preferences, progress tracking, and custom rules remain consistent across all their devices while maintaining our zero-log privacy policy.&lt;br&gt;
Internationalization at Scale&lt;br&gt;
Supporting global users required architecting internationalization from the ground up:&lt;br&gt;
Apply to complete-met...&lt;br&gt;
;&lt;br&gt;
Our content management system supports:&lt;br&gt;
Dynamic content loading based on user locale&lt;br&gt;
SEO-optimized URLs for each language variant&lt;br&gt;
Cultural adaptation beyond simple translation&lt;br&gt;
RTL language support for Arabic and Hebrew markets&lt;br&gt;
Privacy-First Design Decisions&lt;br&gt;
User privacy fundamentally shaped our architectural decisions:&lt;br&gt;
Local Processing: All content analysis occurs on-device; no browsing data leaves the user's machine&lt;br&gt;
Zero-Log Policy: No browsing history, blocked attempts, or user activity stored on servers&lt;br&gt;
Encrypted Sync: User preferences sync using end-to-end encryption&lt;br&gt;
Anonymous Analytics: Usage statistics employ differential privacy techniques&lt;br&gt;
This privacy-first approach required significant engineering investment but was non-negotiable for our user base.&lt;br&gt;
Performance Optimization&lt;br&gt;
Achieving millisecond response times for content filtering required aggressive optimization:&lt;br&gt;
Client-Side Caching: Intelligent caching of filtering rules reduces server requests by 85%&lt;br&gt;
Edge Computing: Cloudflare Workers handle initial content analysis&lt;br&gt;
Progressive Loading: Critical filtering functionality loads first&lt;br&gt;
Memory Optimization: Browser extensions maintain minimal footprint (&amp;lt;50MB)&lt;br&gt;
Content Management at Scale&lt;br&gt;
Managing thousands of articles, tools, and resources across multiple languages required a scalable content architecture:&lt;br&gt;
Apply to complete-met...&lt;br&gt;
}&lt;br&gt;
Our content system provides version control, automated SEO optimization, A/B testing infrastructure, and Core Web Vitals monitoring.&lt;br&gt;
Real-World Impact &amp;amp; Metrics&lt;br&gt;
The technical decisions have yielded measurable results:&lt;br&gt;
99.7% filtering accuracy across 50+ content categories&lt;br&gt;
&amp;lt;100ms average response time for content classification&lt;br&gt;
86% user-reported productivity improvement within 2 weeks&lt;br&gt;
99.9% uptime for cross-platform sync&lt;br&gt;
Full compliance with GDPR, CCPA, and other privacy regulations&lt;br&gt;
Lessons Learned&lt;br&gt;
Building StopX taught us several key lessons:&lt;br&gt;
AI Model Training: Content filtering models require continuous retraining as new patterns emerge. We retrain weekly using anonymized data.&lt;br&gt;
UX vs Security Balance: Robust security with user convenience required extensive user research and iterative design.&lt;br&gt;
Cultural Sensitivity: Content filtering varies significantly across regions, requiring localized filtering rules.&lt;br&gt;
Performance Trade-offs: Real-time filtering demands careful optimization of AI model complexity versus response time.&lt;br&gt;
Future Technical Directions&lt;br&gt;
We're exploring several technical frontiers:&lt;br&gt;
Federated Learning: Improving filtering accuracy through privacy-preserving ML across user devices&lt;br&gt;
WebAssembly Integration: Moving computationally intensive operations to WASM&lt;br&gt;
Advanced Behavioral Analysis: Incorporating biometric feedback for sophisticated intervention&lt;br&gt;
Open Source Contributions&lt;br&gt;
While StopX core remains proprietary, we've open-sourced several components:&lt;br&gt;
Content classification models for academic research&lt;br&gt;
Privacy-preserving analytics tools&lt;br&gt;
Cross-platform sync libraries for developer use&lt;br&gt;
Developer Takeaways&lt;br&gt;
For developers building digital wellness applications:&lt;br&gt;
Privacy must be architectural, not an afterthought&lt;br&gt;
Cross-platform consistency requires significant upfront investment&lt;br&gt;
AI model maintenance is an ongoing operational concern&lt;br&gt;
Cultural sensitivity in global products goes beyond translation&lt;br&gt;
Performance optimization is critical for real-time filtering applications&lt;br&gt;
Conclusion&lt;br&gt;
Building effective content filtering software requires treating it as both an engineering challenge and a human problem. StopX's success stems from prioritizing user privacy, ensuring cross-platform consistency, and maintaining focus on the human problems technology aims to solve.&lt;br&gt;
The platform demonstrates that modern web technologies, when thoughtfully applied, can create meaningful positive impact at scale. Our journey continues as we expand globally and serve new communities seeking digital wellness solutions.&lt;br&gt;
StopX serves over 500,000 users globally and maintains active open-source contributions to the digital wellness community. Learn more at stopx.today&lt;/p&gt;

</description>
    </item>
    <item>
      <title>StopX: A Comprehensive Digital Wellness Solution</title>
      <dc:creator>Pop Watch</dc:creator>
      <pubDate>Wed, 11 Jun 2025 05:43:56 +0000</pubDate>
      <link>https://dev.to/pop_watch_ac45504c97f6c29/stopx-a-comprehensive-digital-wellness-solution-2pl9</link>
      <guid>https://dev.to/pop_watch_ac45504c97f6c29/stopx-a-comprehensive-digital-wellness-solution-2pl9</guid>
      <description>&lt;p&gt;&lt;a href="https://stopx.today/" rel="noopener noreferrer"&gt;https://stopx.today/&lt;/a&gt;&lt;br&gt;
Just discovered StopX while researching content filtering solutions, and I'm impressed by their approach to digital addiction recovery. Here's what makes it stand out from a developer's perspective:&lt;br&gt;
What StopX Actually Does&lt;br&gt;
StopX is a digital wellness platform focused on porn blocking and addiction recovery, not just generic content filtering. Their WebShield™ technology uses AI image recognition combined with behavioral analytics to achieve 99.7% filtering accuracy across all devices.&lt;br&gt;
Technical Architecture&lt;br&gt;
Cross-platform sync: Chrome extension, mobile apps (iOS/Android), and desktop clients all managed from one dashboard&lt;br&gt;
Real-time AI detection: Goes beyond static blocklists to identify explicit content on new/unlisted sites&lt;br&gt;
Incognito mode protection: Works in private browsing to prevent bypass attempts&lt;br&gt;
Uninstall protection: Includes safeguards against impulsive removal&lt;br&gt;
Recovery-Focused Features&lt;br&gt;
What's unique is their holistic approach:&lt;br&gt;
Progress tracking with streak counters and analytics&lt;br&gt;
Recovery dashboard with personalized insights&lt;br&gt;
Optional accountability partner notifications&lt;br&gt;
Community support for users on similar journeys&lt;br&gt;
User Base &amp;amp; Results&lt;br&gt;
With 250K+ active users and 92% reporting productivity improvements within 30 days, they've clearly found product-market fit in the digital wellness space.&lt;br&gt;
Developer Takeaways&lt;br&gt;
The combination of AI-powered content detection with behavioral psychology principles creates an effective solution for a real problem. Their freemium model with lifetime plan option shows sustainable monetization.&lt;br&gt;
Worth checking out if you're working on similar wellness tech or interested in AI-powered content filtering implementations.&lt;br&gt;
Anyone else building in the digital wellness space? What technical challenges have you encountered?&lt;/p&gt;

&lt;h1&gt;
  
  
  digitalwellness #ai #contentfiltering #addiction #webdev
&lt;/h1&gt;

</description>
    </item>
  </channel>
</rss>
