<?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: Andrii Volynets</title>
    <description>The latest articles on DEV Community by Andrii Volynets (@volynetstyle).</description>
    <link>https://dev.to/volynetstyle</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%2F4060647%2Ff1ebf6aa-35e7-4194-8900-24836bde389a.png</url>
      <title>DEV Community: Andrii Volynets</title>
      <link>https://dev.to/volynetstyle</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/volynetstyle"/>
    <language>en</language>
    <item>
      <title>Why let Looked 7 Slower Than var: Closure Contexts and GC Thresholds in V8</title>
      <dc:creator>Andrii Volynets</dc:creator>
      <pubDate>Mon, 03 Aug 2026 12:43:31 +0000</pubDate>
      <link>https://dev.to/volynetstyle/why-let-looked-7x-slower-than-var-closure-contexts-and-gc-thresholds-in-v8-4b38</link>
      <guid>https://dev.to/volynetstyle/why-let-looked-7x-slower-than-var-closure-contexts-and-gc-thresholds-in-v8-4b38</guid>
      <description>&lt;p&gt;The first benchmark looked convincing: when creating a large number of closures, a loop using &lt;code&gt;let&lt;/code&gt; was sometimes &lt;strong&gt;6–8× slower&lt;/strong&gt; than one using &lt;code&gt;var&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That makes for an easy headline:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;let&lt;/code&gt; is much slower than &lt;code&gt;var&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The problem is that the two programs do not have the same semantics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;i&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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&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;All callbacks share one binding. After the loop, every function returns &lt;code&gt;N&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &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;i&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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&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 iteration has its own binding. The callbacks return values from &lt;code&gt;0&lt;/code&gt; through &lt;code&gt;N - 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first program preserves one shared mutable value. The second preserves &lt;code&gt;N&lt;/code&gt; independent values. The useful question is therefore not “how expensive is the &lt;code&gt;let&lt;/code&gt; keyword?” but:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What memory does V8 allocate when every escaping closure genuinely needs its own captured value, and can that memory predict the GC timing step before it is measured?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Separating syntax from semantics
&lt;/h2&gt;

&lt;p&gt;The experiment used six controls:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;State captured by the closure&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;capturedVar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one shared &lt;code&gt;var i&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[N, N, …, N]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;capturedLet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a separate &lt;code&gt;let i&lt;/code&gt; per iteration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[0, 1, …, N-1]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;factoryVar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a factory parameter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[0, 1, …, N-1]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;copiedVar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;const value = i&lt;/code&gt; inside a &lt;code&gt;var&lt;/code&gt; loop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[0, 1, …, N-1]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;noCaptureVar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the counter is not captured&lt;/td&gt;
&lt;td&gt;identical callbacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;noCaptureLet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the counter is not captured&lt;/td&gt;
&lt;td&gt;identical callbacks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;capturedLet&lt;/code&gt;, &lt;code&gt;factoryVar&lt;/code&gt;, and &lt;code&gt;copiedVar&lt;/code&gt; have the same required semantics despite different source shapes. &lt;code&gt;capturedVar&lt;/code&gt; is intentionally not equivalent.&lt;/p&gt;

&lt;p&gt;This distinguishes two explanations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the keyword is intrinsically expensive, a difference should remain without capture.&lt;/li&gt;
&lt;li&gt;If independent escaping state is expensive, the three correct implementations should retain similar heap topology.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Experimental design and provenance
&lt;/h2&gt;

&lt;p&gt;The measurements were made on August 3–4, 2026 with Node.js 25.2.0, V8 14.1.146.11-node.13, and Windows x64.&lt;/p&gt;

&lt;p&gt;The original object measurements are in &lt;a href="//results/raw-results.json"&gt;&lt;code&gt;results/raw-results.json&lt;/code&gt;&lt;/a&gt;. The quantitative GC work is split into explicit training and holdout artifacts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="//results/threshold-calibration.json"&gt;&lt;code&gt;results/threshold-calibration.json&lt;/code&gt;&lt;/a&gt; and &lt;a href="//results/threshold-analysis.json"&gt;&lt;code&gt;results/threshold-analysis.json&lt;/code&gt;&lt;/a&gt;: five training semi-space settings and the regression;&lt;/li&gt;
&lt;li&gt;
&lt;a href="//results/growth-diagnostic.json"&gt;&lt;code&gt;results/growth-diagnostic.json&lt;/code&gt;&lt;/a&gt;: max-only young-generation telemetry;&lt;/li&gt;
&lt;li&gt;
&lt;a href="//results/prediction-plan.json"&gt;&lt;code&gt;results/prediction-plan.json&lt;/code&gt;&lt;/a&gt; / &lt;a href="//results/prediction-result.json"&gt;&lt;code&gt;results/prediction-result.json&lt;/code&gt;&lt;/a&gt;: the first frozen holdout, including its failed prediction;&lt;/li&gt;
&lt;li&gt;
&lt;a href="//results/prediction-plan-2.json"&gt;&lt;code&gt;results/prediction-plan-2.json&lt;/code&gt;&lt;/a&gt; / &lt;a href="//results/prediction-result-2.json"&gt;&lt;code&gt;results/prediction-result-2.json&lt;/code&gt;&lt;/a&gt;: the revised, still out-of-sample holdout;&lt;/li&gt;
&lt;li&gt;
&lt;a href="//results/barrier-topology.json"&gt;&lt;code&gt;results/barrier-topology.json&lt;/code&gt;&lt;/a&gt;, &lt;a href="//results/barrier-constant.json"&gt;&lt;code&gt;results/barrier-constant.json&lt;/code&gt;&lt;/a&gt;, and &lt;a href="//results/barrier-constant-analysis.json"&gt;&lt;code&gt;results/barrier-constant-analysis.json&lt;/code&gt;&lt;/a&gt;: the SMI/reference control;&lt;/li&gt;
&lt;li&gt;
&lt;a href="//results/equivalence.json"&gt;&lt;code&gt;results/equivalence.json&lt;/code&gt;&lt;/a&gt; and &lt;a href="//results/equivalence-analysis.json"&gt;&lt;code&gt;results/equivalence-analysis.json&lt;/code&gt;&lt;/a&gt;: the application-like TOST.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every threshold decision used five one-trial fresh processes. A grid point counted as crossing when at least three of five runs contained an attributed minor GC. A majority-directed binary search located a 2,000-closure bracket; the reported threshold is its midpoint. Holdout predictions were serialized before the corresponding result file existed, and the runner refuses to overwrite a result.&lt;/p&gt;

&lt;h2&gt;
  
  
  A configured maximum is not the current semi-space
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--max-semi-space-size&lt;/code&gt; is a maximum, not a request to begin at that size. The &lt;a href="https://nodejs.org/api/cli.html#--max-semi-space-sizesize-in-mib" rel="noopener noreferrer"&gt;Node CLI documentation&lt;/a&gt; says exactly that. &lt;code&gt;v8.getHeapStatistics().total_available_size&lt;/code&gt; is also the wrong diagnostic: here it was about 4.3 GB because it describes the whole heap.&lt;/p&gt;

&lt;p&gt;The experiment instead recorded all fields returned by &lt;a href="https://nodejs.org/api/v8.html#v8getheapspacestatistics" rel="noopener noreferrer"&gt;&lt;code&gt;v8.getHeapSpaceStatistics()&lt;/code&gt;&lt;/a&gt; for &lt;code&gt;new_space&lt;/code&gt; and &lt;code&gt;new_large_object_space&lt;/code&gt; before and after every trial. Node also warns that the availability and interpretation of these spaces can change with V8 versions, so no single field is silently renamed “the actual semi-space.”&lt;/p&gt;

&lt;p&gt;The max-only diagnostic makes the growth visible. These are medians from three fresh &lt;code&gt;capturedLet(250000)&lt;/code&gt; processes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configured maximum&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;new_space.space_size&lt;/code&gt; before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;th&gt;Minor GCs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2 MiB&lt;/td&gt;
&lt;td&gt;3.75 MiB&lt;/td&gt;
&lt;td&gt;4.00 MiB&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 MiB&lt;/td&gt;
&lt;td&gt;7.00 MiB&lt;/td&gt;
&lt;td&gt;8.00 MiB&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 MiB&lt;/td&gt;
&lt;td&gt;6.75 MiB&lt;/td&gt;
&lt;td&gt;9.50 MiB&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 MiB&lt;/td&gt;
&lt;td&gt;5.75 MiB&lt;/td&gt;
&lt;td&gt;23.25 MiB&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32 MiB&lt;/td&gt;
&lt;td&gt;6.75 MiB&lt;/td&gt;
&lt;td&gt;23.00 MiB&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A “32 MiB semi-space” did not begin with 32 MiB committed, and two processes with different caps could begin in very similar states.&lt;/p&gt;

&lt;p&gt;For the threshold regression, both &lt;code&gt;--min-semi-space-size=S&lt;/code&gt; and &lt;code&gt;--max-semi-space-size=S&lt;/code&gt; were set. V8 still committed pages lazily, so telemetry was retained, but this removed the most obvious max-only ambiguity. The regression and holdouts apply to this exact protocol; they are not a formula for an arbitrary already-running Node process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thresholds scale linearly with the configured semi-space
&lt;/h2&gt;

&lt;p&gt;The measured first-GC brackets were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;S&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;capturedLet&lt;/code&gt; threshold&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;capturedVar&lt;/code&gt; threshold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2 MiB&lt;/td&gt;
&lt;td&gt;41,000&lt;/td&gt;
&lt;td&gt;101,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 MiB&lt;/td&gt;
&lt;td&gt;55,000&lt;/td&gt;
&lt;td&gt;135,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 MiB&lt;/td&gt;
&lt;td&gt;95,000&lt;/td&gt;
&lt;td&gt;189,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 MiB&lt;/td&gt;
&lt;td&gt;177,000&lt;/td&gt;
&lt;td&gt;317,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32 MiB&lt;/td&gt;
&lt;td&gt;335,000&lt;/td&gt;
&lt;td&gt;579,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each midpoint has a grid half-width of 1,000 closures. Ordinary least squares with an intercept gives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;capturedLet:
  N_threshold = 17,750 + 9,907.26 × S_MiB
  slope SE = 111.20 closures/MiB
  slope 95% CI = [9,553.36, 10,261.15]
  R² = 0.99962

capturedVar:
  N_threshold = 66,833 + 15,916.67 × S_MiB
  slope SE = 212.55 closures/MiB
  slope 95% CI = [15,240.25, 16,593.09]
  R² = 0.99947
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;S&lt;/code&gt; in bytes, the reciprocal slope estimates effective nursery bytes per iteration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;capturedLet: 105.84 B/iteration  (95% CI 102.19–109.76)
capturedVar:  65.88 B/iteration  (95% CI  63.19–68.80)
difference:   39.96 B/iteration  (95% CI  35.26–44.66)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The confidence intervals use five fitted midpoints (&lt;code&gt;df = 3&lt;/code&gt;) and treat them as observed responses; they do not add the ±1,000 grid uncertainty. The difference interval uses a delta-method calculation that treats the two slope estimates as independent. &lt;code&gt;R²&lt;/code&gt; is strong evidence of linear scaling in this controlled protocol, not a universal law of V8 heaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  An independent heap method predicts the same extra bytes
&lt;/h2&gt;

&lt;p&gt;The threshold fit above did not use heap-snapshot sizes. A GC-only pilot selected broad search brackets, but only fresh threshold runs entered the fit.&lt;/p&gt;

&lt;p&gt;Now compare its 39.96 B estimate with the heap graph. After forced full GC, 10,000 live closures produced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Closures&lt;/th&gt;
&lt;th&gt;Unique direct contexts&lt;/th&gt;
&lt;th&gt;Closure &lt;code&gt;self_size&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Context &lt;code&gt;self_size&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;heapUsed&lt;/code&gt; increase/item&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;capturedVar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;64 B&lt;/td&gt;
&lt;td&gt;40 B&lt;/td&gt;
&lt;td&gt;72.03 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;capturedLet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;64 B&lt;/td&gt;
&lt;td&gt;40 B&lt;/td&gt;
&lt;td&gt;112.02 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;factoryVar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;64 B&lt;/td&gt;
&lt;td&gt;40 B&lt;/td&gt;
&lt;td&gt;112.02 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;copiedVar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;64 B&lt;/td&gt;
&lt;td&gt;40 B&lt;/td&gt;
&lt;td&gt;111.95 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;noCaptureVar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;64 B&lt;/td&gt;
&lt;td&gt;48 B&lt;/td&gt;
&lt;td&gt;72.01 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;noCaptureLet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;64 B&lt;/td&gt;
&lt;td&gt;48 B&lt;/td&gt;
&lt;td&gt;72.02 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;capturedLet&lt;/code&gt; therefore retains one additional 40-byte &lt;code&gt;system / Context&lt;/code&gt; per item. The process-level delta independently agrees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;112.02152 B - 72.02624 B = 39.99528 B per callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The threshold-only estimate was &lt;strong&gt;39.96 B&lt;/strong&gt;, just &lt;strong&gt;0.04 B (0.1%)&lt;/strong&gt; below the snapshot result, and 40 B lies inside its 95% confidence interval.&lt;/p&gt;

&lt;p&gt;The estimands are not identical: one is effective traffic at a nursery threshold and the other is retained &lt;code&gt;self_size&lt;/code&gt; after full GC. Their numerical agreement supports a specific explanation: the main differential allocation in this code shape is the one additional context.&lt;/p&gt;

&lt;p&gt;The sampling allocation profile is directionally consistent but less exact. Its main-stack difference was 44.83 B/item, about 12% above 40 B. That is acceptable for a sampled profile; it should not be presented as an exact object-size measurement.&lt;/p&gt;

&lt;p&gt;This claim remains version- and shape-specific:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In V8 14.1, for this escaping-capture pattern, every per-item value was represented by a distinct 40-byte context.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is not a JavaScript language guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  A predictive wall-time model
&lt;/h2&gt;

&lt;p&gt;The threshold equation alone predicts only whether a step occurs. To predict wall time, the training runs fitted three coefficients after GC attribution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;M_let(N) = 0.0472 + 0.0000423015 × N ms     (R² = 0.854)
M_var(N) = 2.4996 + 0.0000208576 × N ms     (R² = 0.834)
G_let,2(N) = -2.6180 + 0.000116553 × N ms   (R² = 0.760)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;M&lt;/code&gt; was fitted only from fresh runs with no observed GC. &lt;code&gt;G_let,2&lt;/code&gt; was fitted from runs with exactly two attributed minor collections. The snapshot-constrained threshold model was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;N̂_let(S) = 15,577 + S_bytes / (64 B + 40 B)
N̂_var(S) = 61,038 + S_bytes / 64 B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intercepts were estimated from the five training thresholds; the 64 B closure and additional 40 B context came from the independent snapshot.&lt;/p&gt;

&lt;h3&gt;
  
  
  The first holdout falsified part of the model
&lt;/h3&gt;

&lt;p&gt;The first frozen holdout used an unmeasured 12 MiB setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;predicted let threshold: 136,567
regression cross-check:  136,637
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plan predicted no minor GC at 130,000, two at 145,000, and no GC for &lt;code&gt;capturedVar(145000)&lt;/code&gt;. Across 21 fresh processes per target:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Predicted minor GCs&lt;/th&gt;
&lt;th&gt;Observed&lt;/th&gt;
&lt;th&gt;Predicted wall&lt;/th&gt;
&lt;th&gt;Observed mean&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;let&lt;/code&gt;, 130k&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 in 21/21&lt;/td&gt;
&lt;td&gt;5.55 ms&lt;/td&gt;
&lt;td&gt;4.78 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;let&lt;/code&gt;, 145k&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1 in 21/21&lt;/td&gt;
&lt;td&gt;20.46 ms&lt;/td&gt;
&lt;td&gt;10.74 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;var&lt;/code&gt;, 145k&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 in 21/21&lt;/td&gt;
&lt;td&gt;5.52 ms&lt;/td&gt;
&lt;td&gt;4.94 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The byte model correctly placed the boundary and predicted the direction of the timing jump. The event-count submodel failed: this V8 state produced one minor GC, not two, so the wall-time magnitude was overpredicted by almost 2×.&lt;/p&gt;

&lt;p&gt;That failure is part of the result. It shows why semi-space growth and post-collection state cannot be hidden inside the phrase “GC time.”&lt;/p&gt;

&lt;h3&gt;
  
  
  A revised holdout predicted the step out of sample
&lt;/h3&gt;

&lt;p&gt;After that failure, the two-event rule was restricted to the power-of-two protocol represented by all five training settings. A second plan was frozen for the never-measured &lt;code&gt;(min=max=64 MiB, N=630k/680k)&lt;/code&gt; workload—an extrapolation beyond the 32 MiB training maximum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;snapshot-constrained threshold: 660,855
unconstrained regression:       651,815
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before running it, the plan predicted:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Minor GCs&lt;/th&gt;
&lt;th&gt;Wall time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;let&lt;/code&gt;, 630k&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;26.70 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;let&lt;/code&gt;, 680k&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;105.45 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;var&lt;/code&gt;, 680k&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;16.68 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 21-process holdout produced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Observed minor GCs&lt;/th&gt;
&lt;th&gt;Observed mean wall&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;let&lt;/code&gt;, 630k&lt;/td&gt;
&lt;td&gt;0 in 21/21&lt;/td&gt;
&lt;td&gt;23.89 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;let&lt;/code&gt;, 680k&lt;/td&gt;
&lt;td&gt;2 in 21/21&lt;/td&gt;
&lt;td&gt;106.36 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;var&lt;/code&gt;, 680k&lt;/td&gt;
&lt;td&gt;0 in 21/21&lt;/td&gt;
&lt;td&gt;17.21 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For the above-threshold &lt;code&gt;let&lt;/code&gt; target, the wall-time error was &lt;strong&gt;0.9%&lt;/strong&gt;. The predicted jump was 78.75 ms versus 82.48 ms observed, a 4.7% error. The predicted &lt;code&gt;let/var&lt;/code&gt; ratio was 6.32× versus 6.18× observed, a 2.3% error.&lt;/p&gt;

&lt;p&gt;This is the missing out-of-sample check: snapshot bytes plus a known semi-space setting and &lt;code&gt;N&lt;/code&gt; predicted the GC side of the boundary and approximately how large the wall-time step would be before that workload was run.&lt;/p&gt;

&lt;p&gt;It is still a local model. It predicts the first step for this fresh-process protocol, Node/V8 version, and code shape—not arbitrary GC histories.&lt;/p&gt;

&lt;h2&gt;
  
  
  GC attribution must be delayed
&lt;/h2&gt;

&lt;p&gt;Node delivers GC &lt;code&gt;PerformanceEntry&lt;/code&gt; objects asynchronously. Reading the observer immediately after a timing window can report zero even when collection occurred inside it.&lt;/p&gt;

&lt;p&gt;The corrected workers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;save every trial’s start and end time;&lt;/li&gt;
&lt;li&gt;allow pending entries to arrive on later &lt;code&gt;setImmediate&lt;/code&gt; turns;&lt;/li&gt;
&lt;li&gt;match each entry by its own &lt;code&gt;startTime&lt;/code&gt; and &lt;code&gt;duration&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;record &lt;code&gt;detail.kind&lt;/code&gt;, so only minor events define the threshold.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This procedure is implemented in &lt;a href="//src/gc-trial-worker.mjs"&gt;&lt;code&gt;src/gc-trial-worker.mjs&lt;/code&gt;&lt;/a&gt;. The event timestamps, durations, and young-space telemetry are retained per run rather than summarized away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The residual is not the pure cost of a binding
&lt;/h2&gt;

&lt;p&gt;After attributed GC time is removed, a mutator-time difference remains. Allocation profiles cannot decompose CPU time into context allocation, initialization, closure linking, generated code, tiering, and barrier paths.&lt;/p&gt;

&lt;p&gt;One candidate can at least be bounded experimentally. The original captured value is an SMI, which does not require a heap-reference store. A matched control used either one prebuilt SMI or one prebuilt boxed object, outside the timed window, through the same factory path.&lt;/p&gt;

&lt;p&gt;Heap snapshots verified identical topology in both arms: 10,000 closures, 10,000 unique direct contexts, and 40 B per context. The timing protocol used 30 independent processes per arm, nine GC-free trials per process, and the process median. Before seeing the data, practical equivalence was defined as a boxed/SMI ratio in &lt;code&gt;[0.90, 1.10]&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;geometric mean SMI:    2.045 ms
geometric mean boxed:  2.028 ms
boxed / SMI:           0.992
90% CI:                [0.906, 1.085]
Welch TOST p-value:    0.038
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The confidence interval lies inside the equivalence bounds, so the tagged-reference initialization path is equivalent to the SMI path within ±10% in this kernel. It does not explain the residual at a practically large scale here.&lt;/p&gt;

&lt;p&gt;This does &lt;strong&gt;not&lt;/strong&gt; measure “all write barriers.” The context is newly allocated, so V8 may skip or fast-path a generational remembered-set update. A promoted context storing a young object would be a different experiment. The conclusion is deliberately narrow: pointer-valued context initialization in this code shape is not a remaining &amp;gt;10% explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  “Overlapping distributions” is not equivalence
&lt;/h2&gt;

&lt;p&gt;The original application-like medians—3.63, 3.60, and 3.23 ms—were accompanied by overlapping intervals. That supports only “no stable multi-fold difference was established.” It does not prove equality.&lt;/p&gt;

&lt;p&gt;The replacement experiment used 60 fresh processes. Each process created and drained 100,000 correct callbacks; all six execution orders were repeated ten times. The estimand was the paired mean log wall-time ratio. Equivalence was fixed in advance at ±10%—the largest difference this microbenchmark would call practically interchangeable and far below the original multi-fold claim—with two primary comparisons and Bonferroni-adjusted &lt;code&gt;alpha = 0.025&lt;/code&gt; per comparison.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparison&lt;/th&gt;
&lt;th&gt;Geometric mean ratio&lt;/th&gt;
&lt;th&gt;95% CI&lt;/th&gt;
&lt;th&gt;TOST p&lt;/th&gt;
&lt;th&gt;Equivalent?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;factoryVar / capturedLet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.872&lt;/td&gt;
&lt;td&gt;[0.710, 1.072]&lt;/td&gt;
&lt;td&gt;0.618&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;copiedVar / capturedLet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.870&lt;/td&gt;
&lt;td&gt;[0.727, 1.040]&lt;/td&gt;
&lt;td&gt;0.648&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The formal test did &lt;strong&gt;not&lt;/strong&gt; establish ±10% equivalence. The data remain compatible with a moderate advantage for the alternatives, and GC occurrence still varied between otherwise balanced fresh processes. The defensible claim is therefore weaker than “they are the same”:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This experiment found no stable multi-fold advantage among the semantically correct implementations, but it did not establish practical equivalence within ±10%.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction is exactly what a TOST is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;var&lt;/code&gt; is not an optimization
&lt;/h2&gt;

&lt;p&gt;Replacing &lt;code&gt;let&lt;/code&gt; with one shared &lt;code&gt;var&lt;/code&gt; removes thousands of contexts by removing the requirement to preserve thousands of values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;capturedLet(10000): [0, 5000, 9999]
capturedVar(10000): [10000, 10000, 10000]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not a faster implementation of the same program. It is a different program.&lt;/p&gt;

&lt;p&gt;A factory or local copy preserves the behavior, but it also preserves the main allocation cost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;i&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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&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="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;value&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 optimization target is not the keyword. It is the retained topology: how many closures escape, how many independent environments they require, how much state they retain, and whether allocation occurs in one latency-sensitive batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the experiment establishes
&lt;/h2&gt;

&lt;p&gt;For this Node/V8 version and code shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the first-GC threshold scales linearly with configured semi-space under the controlled protocol (&lt;code&gt;R² &amp;gt; 0.999&lt;/code&gt; for both cases);&lt;/li&gt;
&lt;li&gt;threshold arithmetic independently estimates 39.96 additional B/iteration, while the heap graph measures one additional 40 B context;&lt;/li&gt;
&lt;li&gt;a frozen second holdout correctly predicts &lt;code&gt;0 → 2&lt;/code&gt; minor GCs and 106.36 ms wall time from a 105.45 ms point prediction;&lt;/li&gt;
&lt;li&gt;constant SMI and boxed captures are equivalent within a predeclared ±10% bound for this initialization path;&lt;/li&gt;
&lt;li&gt;the application-like data do not establish ±10% equivalence among the three correct source forms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not establish that &lt;code&gt;let&lt;/code&gt; is generally slow, that retained bytes always equal allocation traffic, that &lt;code&gt;--max-semi-space-size&lt;/code&gt; is the current young-generation size, or that every V8 state will produce the same number of scavenges.&lt;/p&gt;

&lt;p&gt;The final lesson is more specific—and more useful—than the original benchmark headline:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Independent semantic identity requires independent state. In this V8 pattern, that state costs one 40-byte context per escaping closure; enough contexts move the program across a predictable GC boundary, where a moderate allocation difference can become a multi-fold wall-time step.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>productivity</category>
      <category>node</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
