<?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: Alex</title>
    <description>The latest articles on DEV Community by Alex (@phpner).</description>
    <link>https://dev.to/phpner</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%2F3421253%2F38019e86-0882-49d9-96f4-99c4eda5f59a.jpeg</url>
      <title>DEV Community: Alex</title>
      <link>https://dev.to/phpner</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/phpner"/>
    <language>en</language>
    <item>
      <title>The Background-Tab Bug I Missed in My JavaScript Polling Code</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Thu, 20 Aug 2026 10:50:13 +0000</pubDate>
      <link>https://dev.to/phpner/the-background-tab-bug-i-missed-in-my-javascript-polling-code-3ol3</link>
      <guid>https://dev.to/phpner/the-background-tab-bug-i-missed-in-my-javascript-polling-code-3ol3</guid>
      <description>&lt;p&gt;I thought replacing &lt;code&gt;setInterval()&lt;/code&gt; with recursive &lt;code&gt;setTimeout()&lt;/code&gt; had made a polling loop safe. Only one request would run at a time, and I cleared the next timeout when the page became hidden.&lt;/p&gt;

&lt;p&gt;It still had a race.&lt;/p&gt;

&lt;p&gt;The timeout was gone. The request already in flight was not.&lt;/p&gt;

&lt;p&gt;If the page became visible again before that old request finished, a new polling run could start. The old response could then arrive last and overwrite fresher state. Nothing in &lt;code&gt;clearTimeout()&lt;/code&gt; protected the UI from it.&lt;/p&gt;

&lt;p&gt;That was more dangerous than the timer drift I had started investigating. A late counter is visible. A stale response can look completely normal.&lt;/p&gt;

&lt;p&gt;Fixing it required three separate decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;timestamps measure elapsed time;&lt;/li&gt;
&lt;li&gt;visibility changes trigger a resynchronisation;&lt;/li&gt;
&lt;li&gt;cancellation and a run identifier prevent stale requests from updating state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser is allowed to delay background work. The application still has to know which work is current.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, prove that a timer is not a clock
&lt;/h2&gt;

&lt;p&gt;Before changing the polling code, I wanted a small test that separated elapsed time from callbacks. It asks for an interval callback every second and displays four values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;elapsed wall time;&lt;/li&gt;
&lt;li&gt;callbacks actually delivered;&lt;/li&gt;
&lt;li&gt;the difference between those two counters;&lt;/li&gt;
&lt;li&gt;the largest gap between callbacks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am deliberately not publishing one “background tabs delay callbacks by exactly N seconds” result. The policy depends on the browser, version, operating system, power state, audio, WebRTC, and resource pressure. A number from one machine would be easy to repeat and easy to misuse.&lt;/p&gt;

&lt;p&gt;This is the smallest useful version of the test. Save it as &lt;code&gt;timer-test.html&lt;/code&gt; and open it in a normal browser tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Background timer test&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"start"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Start&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;pre&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"output"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Press Start&lt;span class="nt"&gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&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;#output&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;startButton&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;#start&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;intervalId&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;startedAt&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastCallbackAt&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;callbacks&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;largestGapMs&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&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;elapsedSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;callbackLag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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;elapsedSeconds&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nx"&gt;output&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="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;`visibility: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visibilityState&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="s2"&gt;`wall time: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;elapsedSeconds&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;`callbacks: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;callbacks&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="s2"&gt;`counter lag: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;callbackLag&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;`largest callback gap: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;largestGapMs&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="s2"&gt;s`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&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;startButton&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;click&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="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intervalId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;lastCallbackAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;callbacks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;largestGapMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="nx"&gt;intervalId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;largestGapMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;largestGapMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastCallbackAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;lastCallbackAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;callbacks&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="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="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="s2"&gt;visibilitychange&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="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;startedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it, keep the page visible for a few seconds, switch to another tab for several minutes, then return and wait for the next callback. Compare wall time with the callback count and largest gap.&lt;/p&gt;

&lt;p&gt;Do not expect one universal number. If the counters barely diverge, that is still a valid result for that browser and that run. Record the browser version, hidden duration, power state, and whether the page was playing audio before comparing it with somebody else’s result.&lt;/p&gt;

&lt;h2&gt;
  
  
  An interval is a request, not a clock
&lt;/h2&gt;

&lt;p&gt;This counter looks harmless:&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;let&lt;/span&gt; &lt;span class="nx"&gt;secondsSinceUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;secondsSinceUpdate&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="nf"&gt;renderLastUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secondsSinceUpdate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not measure time. It counts callbacks.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;setInterval(callback, 1000)&lt;/code&gt; asks the browser not to run the callback before the delay has passed. It does not guarantee that the main thread will be available at that moment, or that a hidden page will keep receiving callbacks on the same schedule.&lt;/p&gt;

&lt;p&gt;Browsers have good reasons for this. Background pages consume CPU and battery without producing anything the user can see. Chrome documents several levels of timer throttling, including more aggressive treatment of chained timers after a page has been hidden for a while. The exact conditions include visibility, recent audio, WebRTC use, the timer chain, and how long the page has been hidden.&lt;/p&gt;

&lt;p&gt;That policy can change. Application correctness should not depend on reproducing one exact delay.&lt;/p&gt;

&lt;p&gt;For a “last updated” label, the reliable source is the timestamp of the update:&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;updatedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderAge&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;elapsedMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;renderLastUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elapsedMs&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renderAge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interval still decides when the display gets another chance to repaint. It no longer decides how much time passed. When the callback finally runs, the value catches up immediately.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Date.now()&lt;/code&gt; is wall-clock time and can move if the system clock changes. That is appropriate when comparing an event timestamp with the current clock. For measuring a duration inside one active session, use the monotonic &lt;code&gt;performance.now()&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the page a return path
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API" rel="noopener noreferrer"&gt;Page Visibility API&lt;/a&gt; exposes the transition we care about:&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="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="s2"&gt;visibilitychange&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="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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;pauseNonEssentialWork&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;refreshVisibleState&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;I do not use this event to fight throttling. I use it to make returning predictable.&lt;/p&gt;

&lt;p&gt;When the page becomes visible again, it should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;derive displayed time from timestamps;&lt;/li&gt;
&lt;li&gt;fetch data that may have changed;&lt;/li&gt;
&lt;li&gt;check whether real-time connections are still useful;&lt;/li&gt;
&lt;li&gt;render from current state instead of replaying missed visual steps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A background page is allowed to become stale. The bug is returning without noticing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The race I nearly missed
&lt;/h2&gt;

&lt;p&gt;After fixing the visible counter, I returned to the polling loop. Replacing &lt;code&gt;setInterval()&lt;/code&gt; with recursive &lt;code&gt;setTimeout()&lt;/code&gt; prevented overlapping schedules, and clearing the timeout prevented the next request. It still did not cancel the request already in flight.&lt;/p&gt;

&lt;p&gt;There is an awkward sequence hidden inside that simpler code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a request starts;&lt;/li&gt;
&lt;li&gt;the page becomes hidden;&lt;/li&gt;
&lt;li&gt;polling is stopped, but the request continues;&lt;/li&gt;
&lt;li&gt;the page becomes visible and starts a new request;&lt;/li&gt;
&lt;li&gt;the old response arrives last and overwrites the newer state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the controller needs both cancellation and a run identifier:&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;let&lt;/span&gt; &lt;span class="nx"&gt;timerId&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;activeRequest&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;runId&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentRun&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;running&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;currentRun&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;runId&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;request&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;activeRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/status&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;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&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="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;`Status request failed: &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="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;running&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;currentRun&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;runId&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;request&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="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;updateStatus&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="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;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AbortError&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;currentRun&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;reportPollingError&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;span class="k"&gt;finally&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;activeRequest&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;activeRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;running&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;currentRun&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;runId&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;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;timerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentRun&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;5000&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startPolling&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;stopPolling&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentRun&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// refresh immediately&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;stopPolling&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;runId&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="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;activeRequest&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="nx"&gt;activeRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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;visibilitychange&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;stopPolling&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;startPolling&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This version still needs application-specific retry and backoff rules. The important properties are easier to state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one active polling run;&lt;/li&gt;
&lt;li&gt;one in-flight request;&lt;/li&gt;
&lt;li&gt;stale results cannot update the page;&lt;/li&gt;
&lt;li&gt;returning to the page triggers an immediate refresh.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In production I would test cancellation, stale-result suppression, and the hidden-page guard because this is exactly the kind of lifecycle code that looks correct until two events happen close together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Animation frames should render state
&lt;/h2&gt;

&lt;p&gt;Most browsers stop sending &lt;code&gt;requestAnimationFrame()&lt;/code&gt; callbacks to hidden pages. That is useful: there is nothing to draw.&lt;/p&gt;

&lt;p&gt;It becomes a problem only when frame count is also application state:&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;let&lt;/span&gt; &lt;span class="nx"&gt;progress&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;drawProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Derive the visual value from elapsed time instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;durationMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&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;function&lt;/span&gt; &lt;span class="nf"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&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;progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;durationMs&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="nf"&gt;drawProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;After the tab returns, the next frame renders the current position. It does not replay frames nobody saw.&lt;/p&gt;

&lt;p&gt;For a deadline that must survive device sleep or be shared between clients, use a server timestamp or another durable wall-clock source. An animation clock and a business deadline are different jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebSockets need resynchronisation too
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;socket.readyState === WebSocket.OPEN&lt;/code&gt; tells us the browser has not marked the socket closed. It does not prove that the UI received every event while the laptop slept, the network changed, or the page was frozen.&lt;/p&gt;

&lt;p&gt;On return I prefer this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;compare the last confirmed server message with an expected freshness window;&lt;/li&gt;
&lt;li&gt;reconnect if the connection is stale;&lt;/li&gt;
&lt;li&gt;request events or state after the last confirmed cursor;&lt;/li&gt;
&lt;li&gt;apply events idempotently.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Reconnecting without backfilling gives us a fresh connection to potentially incomplete state.&lt;/p&gt;

&lt;p&gt;A Web Worker can move CPU work off the main thread, but it does not turn a browser tab into a durable process. A Service Worker is event-driven and may be terminated between events. If something must happen at a specific real-world time—billing, an email, an expiry, a scheduled job—it belongs on durable infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would ship
&lt;/h2&gt;

&lt;p&gt;The timer drift was expected. The stale response was the part I nearly missed.&lt;/p&gt;

&lt;p&gt;The practical rule I now use for this failure mode is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;timestamps measure time;&lt;/li&gt;
&lt;li&gt;timers request an opportunity to work;&lt;/li&gt;
&lt;li&gt;the server owns durable truth;&lt;/li&gt;
&lt;li&gt;visibility changes trigger resynchronisation;&lt;/li&gt;
&lt;li&gt;cancellation prevents yesterday’s request from overwriting today’s state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser is not breaking a promise when a background callback arrives late. My mistake was treating an old callback—and an old request—as if it still belonged to the current page state.&lt;/p&gt;

&lt;p&gt;If you run the same test in Safari or Firefox, I would be interested in the browser version, hidden duration, and largest gap—not just “it drifted.” That would make the comparison useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API" rel="noopener noreferrer"&gt;Page Visibility API — MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/blog/timer-throttling-in-chrome-88" rel="noopener noreferrer"&gt;Heavy throttling of chained JavaScript timers — Chrome for Developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/web-platform/page-lifecycle-api" rel="noopener noreferrer"&gt;Page Lifecycle API — Chrome for Developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame" rel="noopener noreferrer"&gt;&lt;code&gt;requestAnimationFrame()&lt;/code&gt; — MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers" rel="noopener noreferrer"&gt;Using Web Workers — MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>performance</category>
      <category>frontend</category>
    </item>
    <item>
      <title>PHP Profiling: What the Numbers Actually Tell Us</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Sat, 16 Aug 2025 12:09:39 +0000</pubDate>
      <link>https://dev.to/phpner/php-profiling-from-problem-to-solution-with-metrics-2epf</link>
      <guid>https://dev.to/phpner/php-profiling-from-problem-to-solution-with-metrics-2epf</guid>
      <description>&lt;p&gt;A profiler is most useful when it proves your first idea wrong.&lt;/p&gt;

&lt;p&gt;I recently went back to a small PHP example that normalizes a large list of email addresses. The first version of this article had a neat story: replace &lt;code&gt;array_unique()&lt;/code&gt;, remove repeated string operations, switch from &lt;code&gt;filter_var()&lt;/code&gt; to a regular expression, and watch execution time fall from 12.8 seconds to 2.1 seconds.&lt;/p&gt;

&lt;p&gt;There was one problem: the repository did not support that story.&lt;/p&gt;

&lt;p&gt;The baseline already called &lt;code&gt;trim()&lt;/code&gt; and &lt;code&gt;strtolower()&lt;/code&gt; only once. The generated addresses were all unique, which made the deduplication comparison weak. The repository contained no Cachegrind output from which the quoted function percentages could be reproduced. Its peak-memory number also included the fixture built before the measured function ran.&lt;/p&gt;

&lt;p&gt;So I rebuilt the example and measured it again. The result is less dramatic, but much more useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are we trying to improve?
&lt;/h2&gt;

&lt;p&gt;The example does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;normalize each address with &lt;code&gt;trim()&lt;/code&gt; and &lt;code&gt;strtolower()&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;validate it according to a declared rule;&lt;/li&gt;
&lt;li&gt;return unique normalized strings in first-seen order.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In simplified form, the reference implementation collects accepted addresses and deduplicates them at the end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;normalizeWithArrayUnique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;callable&lt;/span&gt; &lt;span class="nv"&gt;$validator&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$emails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rows&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&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="nv"&gt;$email&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$validator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$emails&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;array_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;array_unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$emails&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SORT_STRING&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 alternative uses normalized addresses as keys and therefore deduplicates while iterating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;normalizeWithSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;callable&lt;/span&gt; &lt;span class="nv"&gt;$validator&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$set&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rows&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&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="nv"&gt;$email&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$validator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;array_keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$set&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks promising, but appearance is not evidence. We need a profile to find expensive work and a benchmark to determine whether a change actually helps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Profile one case deliberately
&lt;/h2&gt;

&lt;p&gt;For CLI profiling with Xdebug 3, I use a trigger instead of profiling every PHP command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;xdebug.mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;profile&lt;/span&gt;
&lt;span class="py"&gt;xdebug.start_with_request&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;trigger&lt;/span&gt;
&lt;span class="py"&gt;xdebug.output_dir&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/tmp/xdebug&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output directory must exist and be writable. I can then profile one implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;XDEBUG_TRIGGER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 php examples/emails/run.php baseline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Xdebug writes a &lt;code&gt;cachegrind.out.*&lt;/code&gt; file that can be opened in a Cachegrind-compatible viewer. Self time shows work done inside a function. Inclusive time also includes its callees.&lt;/p&gt;

&lt;p&gt;A profile is excellent for forming a hypothesis, but it is a poor place to take final wall-clock numbers. Instrumentation adds overhead, so the benchmark runs separately with Xdebug disabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;XDEBUG_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;off composer bench:record
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://xdebug.org/docs/profiler" rel="noopener noreferrer"&gt;Xdebug profiler documentation&lt;/a&gt; describes trigger values and output-name settings if several profiles need to be kept apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fixture that exercises the code
&lt;/h2&gt;

&lt;p&gt;The revised fixture is deterministic and contains 200,000 interleaved rows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Rows&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unique valid addresses&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate valid addresses&lt;/td&gt;
&lt;td&gt;40,000&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty values&lt;/td&gt;
&lt;td&gt;20,000&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalid values&lt;/td&gt;
&lt;td&gt;20,000&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accepted only by the simple shape check&lt;/td&gt;
&lt;td&gt;20,000&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is still synthetic data. It does not claim to represent every import. It simply makes validation, rejection, and deduplication observable instead of filling the input with unique valid addresses only.&lt;/p&gt;

&lt;p&gt;The last category is there for a specific reason: it exposes a behavior change that a timing table could otherwise hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark protocol
&lt;/h2&gt;

&lt;p&gt;The runner uses one warm-up and ten measured runs for each implementation. Every sample gets a fresh PHP process, and the order of the implementations rotates between iterations.&lt;/p&gt;

&lt;p&gt;Fixture generation happens before the timer starts. On PHP 8.2 or newer, peak accounting is reset immediately before the measured code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EmailFixture&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;gc_collect_cycles&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;memory_reset_peak_usage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$baselineMemory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;memory_get_usage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hrtime&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="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rows&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$elapsedSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hrtime&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="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$startedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$peakDelta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;memory_get_peak_usage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$baselineMemory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hrtime(true)&lt;/code&gt; is monotonic, so a system clock adjustment cannot distort the duration. Resetting the peak keeps fixture construction out of the incremental memory figure.&lt;/p&gt;

&lt;p&gt;The runner also checks the output. The two strict implementations must have the same row count and SHA-256 checksum on every run. If they differ, the benchmark stops instead of comparing non-equivalent code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;I recorded these results on PHP 8.5.9 CLI, Darwin arm64, with a 512 MiB memory limit. OPcache for CLI was off, JIT was disabled, and Xdebug was not loaded.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;th&gt;Median time&lt;/th&gt;
&lt;th&gt;Time range&lt;/th&gt;
&lt;th&gt;Median incremental peak&lt;/th&gt;
&lt;th&gt;Output rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;array_unique()&lt;/code&gt; + &lt;code&gt;FILTER_VALIDATE_EMAIL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0.178650 s&lt;/td&gt;
&lt;td&gt;0.169635–0.183735 s&lt;/td&gt;
&lt;td&gt;24.45 MiB&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set + &lt;code&gt;FILTER_VALIDATE_EMAIL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0.183060 s&lt;/td&gt;
&lt;td&gt;0.178105–0.189811 s&lt;/td&gt;
&lt;td&gt;11.60 MiB&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set + simple shape check&lt;/td&gt;
&lt;td&gt;0.039487 s&lt;/td&gt;
&lt;td&gt;0.037182–0.043289 s&lt;/td&gt;
&lt;td&gt;11.60 MiB&lt;/td&gt;
&lt;td&gt;120,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The raw samples and environment metadata are committed in &lt;a href="https://github.com/phpner/php-profiling-example/blob/main/results/benchmark.json" rel="noopener noreferrer"&gt;&lt;code&gt;results/benchmark.json&lt;/code&gt;&lt;/a&gt;. These numbers describe this machine, PHP build, fixture, and protocol. They are not universal constants.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ji90acv8alx5gmzno67.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ji90acv8alx5gmzno67.png" alt="Recorded output from the PHP benchmark, including environment, protocol, timing ranges, incremental peak memory, and output row counts" width="799" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The recorded output is rendered directly from the committed benchmark result. The editable SVG source is stored next to the PNG in the repository.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What did the set actually improve?
&lt;/h2&gt;

&lt;p&gt;Replacing &lt;code&gt;array_unique()&lt;/code&gt; with a set reduced the median incremental peak from 24.45 MiB to 11.60 MiB. The output count and checksum matched the baseline.&lt;/p&gt;

&lt;p&gt;It did not make this run faster. Its median was slightly higher, and the measured ranges overlap. With ten small samples, the responsible conclusion is not that the set is categorically slower. It is that this benchmark shows a clear memory improvement and no demonstrated time improvement.&lt;/p&gt;

&lt;p&gt;That distinction matters. If memory pressure is the problem, the set is useful here. If latency is the problem, this change has not solved it.&lt;/p&gt;

&lt;p&gt;The result may change with a different duplicate ratio, input order, PHP version, or allocator behavior. That is why the fixture and environment belong next to the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fast regex is not the same validator
&lt;/h2&gt;

&lt;p&gt;The third implementation uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;hasSimpleEmailShape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/^[^\s@]+@[^\s@]+\.[^\s@]+$/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;)&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is much faster in this fixture, but it returns 120,000 rows rather than 100,000. It is not a drop-in optimization for &lt;code&gt;FILTER_VALIDATE_EMAIL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, the simple expression accepts values that PHP's validator rejects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a..b@example.com
.alice@example.com
alice@example..com
alice@-example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The regex only checks for a broad &lt;code&gt;something@something.something&lt;/code&gt; shape. That may be a valid product rule. A signup form that sends a verification message may deliberately prefer a permissive initial check. Another system may require a stricter local policy.&lt;/p&gt;

&lt;p&gt;But that is a requirements decision, not a free performance win. A profiler can show the cost of the current validator; it cannot decide which input the application is allowed to accept. No syntax validator proves that a mailbox exists, either.&lt;/p&gt;

&lt;p&gt;The same care applies to lowercasing the full address. It can be a reasonable application identity rule, but it should be documented as behavior rather than presented as a performance detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the original progression was misleading
&lt;/h2&gt;

&lt;p&gt;One of the old steps split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into multiple assignments. Both forms still invoked &lt;code&gt;trim()&lt;/code&gt; and &lt;code&gt;strtolower()&lt;/code&gt; once for non-empty input. There was no repeated normalization to remove, so the supposed optimization did not correspond to the committed baseline.&lt;/p&gt;

&lt;p&gt;The previous memory figures had a different problem. Calling &lt;code&gt;memory_get_peak_usage()&lt;/code&gt; after building the entire fixture reports the highest point reached anywhere in the process. Without resetting peak accounting, that number cannot be described as the normalization function's incremental peak.&lt;/p&gt;

&lt;p&gt;Finally, function percentages from a profiler need the corresponding profile, environment, and workload. Without that evidence, exact values such as “42% self time” look precise but cannot be checked. I removed them rather than inventing a reconstruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to reproduce it
&lt;/h2&gt;

&lt;p&gt;The companion repository contains the fixture, implementations, tests, runner, and recorded raw results:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/phpner/php-profiling-example" rel="noopener noreferrer"&gt;github.com/phpner/php-profiling-example&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer &lt;span class="nb"&gt;install
&lt;/span&gt;composer &lt;span class="nb"&gt;test
&lt;/span&gt;&lt;span class="nv"&gt;XDEBUG_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;off composer bench
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests verify fixture composition, first-seen order, equality of the strict implementations, and the intentional difference in the shape-check policy.&lt;/p&gt;

&lt;p&gt;For a production import, I would investigate one more thing before micro-optimizing this loop: whether the input can be streamed. This example materializes all 200,000 rows before normalization. Reading a CSV or database cursor incrementally can remove a much larger memory cost, although the deduplication set will still grow with the number of unique addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The corrected example does not produce a smooth sequence of wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the set saves incremental peak memory but does not demonstrate a speedup;&lt;/li&gt;
&lt;li&gt;rearranging the same string calls is not an optimization;&lt;/li&gt;
&lt;li&gt;the simple regex is faster because it implements a different acceptance policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is what honest profiling often looks like. Use the profile to choose a question. Change one thing. Verify the output. Benchmark outside the profiler. If behavior changes, describe it as a tradeoff rather than hiding it behind a faster number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://xdebug.org/docs/profiler" rel="noopener noreferrer"&gt;Xdebug profiler documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/function.filter-var.php" rel="noopener noreferrer"&gt;PHP manual: &lt;code&gt;filter_var&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/filter.constants.php" rel="noopener noreferrer"&gt;PHP manual: filter constants&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/function.hrtime.php" rel="noopener noreferrer"&gt;PHP manual: &lt;code&gt;hrtime&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/function.memory-get-peak-usage.php" rel="noopener noreferrer"&gt;PHP manual: &lt;code&gt;memory_get_peak_usage&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/function.memory-reset-peak-usage.php" rel="noopener noreferrer"&gt;PHP manual: &lt;code&gt;memory_reset_peak_usage&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>performance</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Lazy Evaluation in PHP: Real‑World Memory Savings with Generators</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Fri, 08 Aug 2025 16:33:47 +0000</pubDate>
      <link>https://dev.to/phpner/lazy-evaluation-in-php-how-generators-and-iterators-save-memory-and-speed-up-code-3529</link>
      <guid>https://dev.to/phpner/lazy-evaluation-in-php-how-generators-and-iterators-save-memory-and-speed-up-code-3529</guid>
      <description>&lt;p&gt;Generators are often introduced with a neat demo: build an array with a million values, replace it with &lt;code&gt;yield&lt;/code&gt;, and print the memory usage. The conclusion is usually correct, but the benchmark often is not.&lt;/p&gt;

&lt;p&gt;If the array is released before memory is measured, or both implementations run in the same PHP process, the result can be misleading. I rebuilt the example around a real CSV file and ran each strategy in an isolated process.&lt;/p&gt;

&lt;p&gt;The short version: for this workload, the eager implementation added &lt;strong&gt;386.81 MiB&lt;/strong&gt; of peak memory. The generator added &lt;strong&gt;10.69 KiB&lt;/strong&gt;. The generator was also about 13% faster in this particular test, although that part should not be treated as a general rule.&lt;/p&gt;

&lt;p&gt;The dependable benefit of lazy iteration is bounded memory growth. Speed still depends on the work being done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What “lazy” means here
&lt;/h2&gt;

&lt;p&gt;An eager function prepares its complete result before the caller can use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getNumbersArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$numbers&lt;/span&gt; &lt;span class="o"&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="nv"&gt;$number&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="nv"&gt;$number&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$number&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="nv"&gt;$numbers&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$numbers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getNumbersArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&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;By the time &lt;code&gt;foreach&lt;/code&gt; starts, all five values already exist in the array. With five integers that is irrelevant. With a large CSV result, it can decide whether the process finishes at all.&lt;/p&gt;

&lt;p&gt;A generator exposes the same sequence without first building the complete collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getNumbersGenerator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Generator&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="nv"&gt;$number&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="nv"&gt;$number&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$number&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;yield&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getNumbersGenerator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&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;Calling &lt;code&gt;getNumbersGenerator()&lt;/code&gt; returns a &lt;code&gt;Generator&lt;/code&gt; object. Execution starts when iteration starts, pauses at &lt;code&gt;yield&lt;/code&gt;, and resumes when the consumer asks for the next value.&lt;/p&gt;

&lt;p&gt;The useful distinction is not how many values PHP can process. It is how many of them must be retained at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming a CSV safely
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;file()&lt;/code&gt; reads a complete file into an array. &lt;code&gt;fgetcsv()&lt;/code&gt; reads the next CSV record from an already open stream, which makes it a natural fit for a generator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;readCsv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Generator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;fopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'rb'&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="nv"&gt;$handle&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Cannot open file: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="si"&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;fgetcsv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;enclosure&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;escape&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&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;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nv"&gt;$row&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;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;fclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$handle&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;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;readCsv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'data.csv'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;processRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&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 are easy to miss here.&lt;/p&gt;

&lt;p&gt;First, the &lt;code&gt;finally&lt;/code&gt; block closes the file after normal completion or an exception. It also handles cleanup when a generator is eventually destroyed after an early exit.&lt;/p&gt;

&lt;p&gt;Second, passing &lt;code&gt;escape: ''&lt;/code&gt; explicitly avoids relying on the deprecated default CSV escape behavior in PHP 8.4 and later.&lt;/p&gt;

&lt;p&gt;This code does not make the entire PHP process use only a few kilobytes. The process still retains the stream buffer, parser state, generator frame, current record, and anything held by &lt;code&gt;processRow()&lt;/code&gt;. What it avoids is memory growth proportional to the total number of CSV records.&lt;/p&gt;

&lt;p&gt;That advantage disappears if the consumer immediately rebuilds the collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;readCsv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'data.csv'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$rows&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$row&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 producer is lazy, but the overall pipeline is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the usual memory benchmark is misleading
&lt;/h2&gt;

&lt;p&gt;Consider this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$array&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;range&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;1_000_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$array&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;memory_get_usage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bigGenerator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Consume the values.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nb"&gt;memory_get_peak_usage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This measures the wrong thing for two separate reasons.&lt;/p&gt;

&lt;p&gt;The first is timing: measuring current memory after a function returns tells us little about the peak reached while its array was alive.&lt;/p&gt;

&lt;p&gt;The second is process-wide state. &lt;code&gt;memory_get_peak_usage()&lt;/code&gt; remembers the highest point reached by the process. If the eager case runs first, a later generator case may report a peak increase of zero simply because it never exceeds the old array peak.&lt;/p&gt;

&lt;p&gt;For the repository benchmark, I used a separate child process for every measured run. Each child resets peak accounting immediately before the workload, keeps the eager collection alive until measurement, and returns machine-readable results to the parent process.&lt;/p&gt;

&lt;p&gt;Both modes must also prove that they did the same work. In this run they processed the same number of records and produced the same checksum.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measured result
&lt;/h2&gt;

&lt;p&gt;The input was a deterministic CSV fixture with 1,000,000 data records plus its header:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file size: 36.92 MiB;&lt;/li&gt;
&lt;li&gt;rows processed: 1,000,001;&lt;/li&gt;
&lt;li&gt;PHP: 8.5.9;&lt;/li&gt;
&lt;li&gt;platform: Darwin arm64;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory_limit&lt;/code&gt;: 1 GiB;&lt;/li&gt;
&lt;li&gt;OPcache for CLI: off;&lt;/li&gt;
&lt;li&gt;JIT: disabled;&lt;/li&gt;
&lt;li&gt;one warm-up per mode;&lt;/li&gt;
&lt;li&gt;ten measured runs per mode;&lt;/li&gt;
&lt;li&gt;alternating eager/lazy execution order;&lt;/li&gt;
&lt;li&gt;warm filesystem cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two implementations produced the same checksum: &lt;code&gt;37,711,230&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;Method&lt;/th&gt;
&lt;th&gt;Median time&lt;/th&gt;
&lt;th&gt;Time range&lt;/th&gt;
&lt;th&gt;Median incremental peak memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Array (eager)&lt;/td&gt;
&lt;td&gt;0.759376 s&lt;/td&gt;
&lt;td&gt;0.745849–0.829022 s&lt;/td&gt;
&lt;td&gt;386.81 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generator (lazy)&lt;/td&gt;
&lt;td&gt;0.658925 s&lt;/td&gt;
&lt;td&gt;0.642476–0.679049 s&lt;/td&gt;
&lt;td&gt;10.69 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

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

&lt;p&gt;The memory result is the important one. The eager version materializes every parsed record as a PHP array, including all the per-element hash-table and &lt;code&gt;zval&lt;/code&gt; overhead. The generator retains only the state needed for the current step and whatever the consumer keeps.&lt;/p&gt;

&lt;p&gt;The timing result needs more restraint. In this run the generator completed about 13% faster, but &lt;code&gt;yield&lt;/code&gt; is not inherently faster than array iteration. A generator suspends and resumes its execution frame for every value. On another workload, an already-built array may be faster to traverse.&lt;/p&gt;

&lt;p&gt;Here, avoiding hundreds of MiB of allocation and cleanup outweighed the generator overhead. Different input, storage, PHP version, transformations, or consumer work can change that balance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generator, Iterator, or IteratorAggregate?
&lt;/h2&gt;

&lt;p&gt;These APIs overlap, but I do not use them interchangeably.&lt;/p&gt;

&lt;p&gt;I reach for a generator when the iteration is forward-only and fits naturally in one function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;activeUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;iterable&lt;/span&gt; &lt;span class="nv"&gt;$users&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Generator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$user&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="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same &lt;code&gt;Generator&lt;/code&gt; object cannot be rewound for another complete pass after it has advanced. Calling the generator function again creates a fresh generator.&lt;/p&gt;

&lt;p&gt;I use &lt;code&gt;IteratorAggregate&lt;/code&gt; when an object should be traversable more than once while keeping the iteration logic out of the object itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NumberRange&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;IteratorAggregate&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$end&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;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getIterator&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Traversable&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="nv"&gt;$number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$number&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;yield&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$range&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;NumberRange&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;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$range&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&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;Each &lt;code&gt;foreach&lt;/code&gt; call asks the object for a new iterator, so this design is naturally reusable.&lt;/p&gt;

&lt;p&gt;A custom &lt;code&gt;Iterator&lt;/code&gt; is useful when cursor operations such as &lt;code&gt;current()&lt;/code&gt;, &lt;code&gt;next()&lt;/code&gt;, &lt;code&gt;key()&lt;/code&gt;, &lt;code&gt;valid()&lt;/code&gt;, and &lt;code&gt;rewind()&lt;/code&gt; are part of the abstraction. It is more code, and I would not choose it merely to avoid writing a generator.&lt;/p&gt;

&lt;p&gt;My practical rule is simple:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Default choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One-pass stream or transformation pipeline&lt;/td&gt;
&lt;td&gt;Generator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reusable domain object that can be traversed&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IteratorAggregate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit cursor behavior is meaningful&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Iterator&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Random access or repeated indexed lookup&lt;/td&gt;
&lt;td&gt;Array or another collection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Paginated APIs are lazy only between pages
&lt;/h2&gt;

&lt;p&gt;A generator also works well around pagination:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;fetchCars&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Generator&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="nv"&gt;$page&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="nv"&gt;$page&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="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apiRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cars'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'page'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="nv"&gt;$items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'items'&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$items&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;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$items&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$car&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nv"&gt;$car&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents all pages from accumulating in one large array. It does not necessarily reduce memory to one car at a time: &lt;code&gt;apiRequest()&lt;/code&gt; may buffer the complete HTTP response, and the decoded &lt;code&gt;$items&lt;/code&gt; array remains alive while that page is being yielded.&lt;/p&gt;

&lt;p&gt;The actual memory bound is therefore closer to one decoded page plus client and parser buffers. Page size still matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where generators do not help
&lt;/h2&gt;

&lt;p&gt;Generators are a poor fit when the consumer needs random access, frequent rewinds, sorting of the complete data set, or several passes over an expensive one-shot source.&lt;/p&gt;

&lt;p&gt;They also cannot make a buffered source incremental. Wrapping a fully loaded array in a generator changes the interface, not the memory profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;pretendToStream&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Generator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loadEveryRowIntoAnArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rows&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nv"&gt;$row&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;Database access has the same caveat. Yielding rows from a buffered query does not remove the driver's result buffer. To bound memory, the database driver and query mode must support incremental fetching as well.&lt;/p&gt;

&lt;p&gt;Finally, laziness changes when errors occur. A generator function can be called successfully and then fail only after iteration begins. That is useful, but callers need to understand it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the benchmark
&lt;/h2&gt;

&lt;p&gt;The complete benchmark, fixture generator, CSV and NDJSON examples, chart renderer, and tests are available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/phpner/phpner-php-lazy-evaluation-demo" rel="noopener noreferrer"&gt;github.com/phpner/phpner-php-lazy-evaluation-demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The recorded run can be reproduced with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer &lt;span class="nb"&gt;install
&lt;/span&gt;php bin/make_sample.php &lt;span class="nt"&gt;--rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000000
composer bench:record
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect the timing numbers to move between machines and runs. The repository records the environment, row count, checksum, median, and range so those differences remain visible instead of being hidden behind one convenient number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The benchmark changed how I phrase the recommendation. I do not choose a generator because &lt;code&gt;yield&lt;/code&gt; is supposed to be fast. I choose it when the complete data set does not need to exist in memory at once.&lt;/p&gt;

&lt;p&gt;For the measured CSV workload, that decision reduced incremental peak memory from 386.81 MiB to 10.69 KiB. The generator happened to be faster too, but memory behavior is the result I would expect to carry over to other genuinely streaming workloads.&lt;/p&gt;

&lt;p&gt;The producer, parser, and consumer all have to participate. If any stage buffers the full data set, adding a generator somewhere in the middle will not fix the underlying problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/language.generators.overview.php" rel="noopener noreferrer"&gt;PHP manual: Generators overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/language.generators.syntax.php" rel="noopener noreferrer"&gt;PHP manual: Generator syntax&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/language.generators.comparison.php" rel="noopener noreferrer"&gt;PHP manual: Comparing generators with Iterator objects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/function.fgetcsv.php" rel="noopener noreferrer"&gt;PHP manual: &lt;code&gt;fgetcsv&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/function.memory-get-peak-usage.php" rel="noopener noreferrer"&gt;PHP manual: &lt;code&gt;memory_get_peak_usage&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/mysqlinfo.concepts.buffering.php" rel="noopener noreferrer"&gt;PHP manual: Buffered and unbuffered MySQL queries&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>backenddevelopment</category>
      <category>php</category>
      <category>performance</category>
    </item>
    <item>
      <title>The Hidden Path to Computers — Through the Lens of “Code” by Charles Petzold</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Fri, 08 Aug 2025 08:51:48 +0000</pubDate>
      <link>https://dev.to/phpner/the-hidden-path-to-computers-through-the-lens-of-code-by-charles-petzold-593p</link>
      <guid>https://dev.to/phpner/the-hidden-path-to-computers-through-the-lens-of-code-by-charles-petzold-593p</guid>
      <description>&lt;h1&gt;
  
  
  📖 About the Book
&lt;/h1&gt;

&lt;p&gt;Charles Petzold is an author who managed to explain the complex world of computer technology through compelling storytelling. His book &lt;em&gt;Code: The Hidden Language of Computer Hardware and Software&lt;/em&gt; takes us on a journey from the earliest methods of communication to the modern digital computer — without dry technical jargon, but with real-world examples, metaphors, and historical facts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Computers are not magic. They are the result of a long human journey that began long before the invention of electronics."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. How Humans Began to Communicate Information
&lt;/h2&gt;

&lt;p&gt;Petzold begins by showing that people have always had the need to send messages across distances. At first, this meant using signal fires on hills, drums in the jungle, or flags on ships. Each of these methods had limits: in speed, distance, or the number of symbols available.&lt;/p&gt;

&lt;p&gt;Eventually, we arrive at Morse code.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Morse Code: The First “Zeros” and “Ones”
&lt;/h2&gt;

&lt;p&gt;Samuel Morse invented a system of &lt;strong&gt;short (dot)&lt;/strong&gt; and &lt;strong&gt;long (dash)&lt;/strong&gt; electrical signals. It was the first practical digital code — not literally zeros and ones, but conceptually very close: &lt;strong&gt;only two distinct signals&lt;/strong&gt; used in combination to represent any letter or number.&lt;/p&gt;

&lt;h3&gt;
  
  
  📌 Key features:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Efficiency&lt;/strong&gt;: The most common letters (like E and T) were assigned the shortest codes.&lt;/li&gt;
&lt;li&gt;Messages were sent by completing or breaking an electrical circuit.&lt;/li&gt;
&lt;li&gt;Telegraph operators could &lt;em&gt;hear&lt;/em&gt; the code in real time and interpret it by ear.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Morse code was the first system to show that any kind of information could be transmitted using just two types of signals.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. From Signals to Electricity
&lt;/h2&gt;

&lt;p&gt;The telegraph was revolutionary for communication, but it &lt;strong&gt;only transmitted&lt;/strong&gt;, not processed, information.  &lt;/p&gt;

&lt;p&gt;The next leap came with the realization that &lt;strong&gt;electric signals could do work&lt;/strong&gt; — they could control machines, and ultimately, perform logic.&lt;/p&gt;

&lt;p&gt;Here, Petzold introduces &lt;strong&gt;the relay&lt;/strong&gt; — a simple switch that opens or closes in response to electrical current. It too has &lt;strong&gt;two states&lt;/strong&gt;, which fits naturally into &lt;strong&gt;binary logic&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The Binary Number System
&lt;/h2&gt;

&lt;p&gt;For a machine to "understand" anything, it must be able to represent numbers and instructions.&lt;br&gt;&lt;br&gt;
The binary system (0 and 1) turned out to be ideal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;0 = off
&lt;/li&gt;
&lt;li&gt;1 = on
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Historically, the binary number system was described by &lt;strong&gt;Gottfried Wilhelm Leibniz&lt;/strong&gt; in the 17th century. But with the invention of electrical switches, it became practically useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Logic Gates and Circuits
&lt;/h2&gt;

&lt;p&gt;Petzold shows that using relays, you can build basic logic gates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AND&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OR&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NOT&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By combining these, you can construct &lt;strong&gt;any computation&lt;/strong&gt;. This leads to the central insight:  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A computer is essentially a massive network of binary switches arranged in logical combinations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  6. From Mechanical Devices to Electronics
&lt;/h2&gt;

&lt;p&gt;Before relays, there were &lt;strong&gt;mechanical calculators&lt;/strong&gt; — like Charles Babbage’s Analytical Engine. But they were slow, bulky, and unreliable.&lt;/p&gt;

&lt;p&gt;Eventually, &lt;strong&gt;vacuum tubes&lt;/strong&gt; and &lt;strong&gt;transistors&lt;/strong&gt; replaced relays, making computations &lt;strong&gt;faster and more compact&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Encoding Text, Images, and Sound
&lt;/h2&gt;

&lt;p&gt;Once you can handle numbers, you can represent anything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Text&lt;/strong&gt;: through &lt;strong&gt;ASCII&lt;/strong&gt;, where each letter has a unique binary number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images&lt;/strong&gt;: grids of black/white pixels, each encoded as 0 or 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sound&lt;/strong&gt;: sequences of numbers representing waveform amplitudes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This showed that &lt;strong&gt;all information&lt;/strong&gt; — visual, audio, textual — could be reduced to binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Programs and Memory
&lt;/h2&gt;

&lt;p&gt;After machines could store and manipulate binary data, they needed instructions — &lt;strong&gt;programs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Memory began to store both &lt;strong&gt;data and instructions&lt;/strong&gt;, which led to the concept of the &lt;strong&gt;Von Neumann architecture&lt;/strong&gt;, where program and data coexist in the same memory space.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. The Big Realization
&lt;/h2&gt;

&lt;p&gt;By the end of the book, the reader understands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A computer is not a magical box. It’s a machine built from &lt;strong&gt;simple components&lt;/strong&gt; (relays, switches, transistors) that operate on &lt;strong&gt;binary logic&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Complexity arises from &lt;strong&gt;the clever arrangement of simple building blocks&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“A computer is nothing more than a very fast and very obedient idiot doing exactly what we tell it to.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🎯 Why This Book Is Valuable
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It gives you an &lt;strong&gt;intuitive understanding&lt;/strong&gt; of how computers work — without formulas.
&lt;/li&gt;
&lt;li&gt;It shows a clear &lt;strong&gt;historical progression&lt;/strong&gt;:
&lt;em&gt;signal fires → Morse code → relays → transistors → processors&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;After reading it, you begin to see technology as a chain of &lt;strong&gt;brilliant simplifications&lt;/strong&gt;, rather than complexity.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
