<?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: Mara Averick</title>
    <description>The latest articles on DEV Community by Mara Averick (@dataandme).</description>
    <link>https://dev.to/dataandme</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%2F858%2F8ed854c1-3e73-4d1d-b25f-74aea30e669e.png</url>
      <title>DEV Community: Mara Averick</title>
      <link>https://dev.to/dataandme</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dataandme"/>
    <language>en</language>
    <item>
      <title>Write It Like C: What V8's Optimizing Compilers Explain About the Zen of stdlib</title>
      <dc:creator>Mara Averick</dc:creator>
      <pubDate>Mon, 24 Aug 2026 14:15:34 +0000</pubDate>
      <link>https://dev.to/dataandme/write-it-like-c-what-v8s-optimizing-compilers-explain-about-the-zen-of-stdlib-2hn7</link>
      <guid>https://dev.to/dataandme/write-it-like-c-what-v8s-optimizing-compilers-explain-about-the-zen-of-stdlib-2hn7</guid>
      <description>&lt;p&gt;JavaScript wasn't designed for numerical computing. There's one number type (a 64-bit float), no operator overloading, no control over memory layout, and a value model where every integer is, in principle, an object. And yet, on a modern V8, a tight loop over a &lt;code&gt;Float64Array&lt;/code&gt; lands within a small constant factor of equivalent C—sometimes at basically the same speed. That's not an accident, and it's not free. It's the cumulative result of fifteen years of compiler engineering specifically aimed at &lt;em&gt;recognizing&lt;/em&gt;, at runtime, that the dynamic-language source you wrote is in fact behaving like a statically-typed numerical kernel—and rewriting it as one.&lt;/p&gt;

&lt;p&gt;If your first association with "V8" is horsepower and max towing capacity, &lt;a href="https://blog.stdlib.io/zen-of-stdlib/" rel="noopener noreferrer"&gt;the Zen of stdlib&lt;/a&gt; is going to read as an elegant list of design principles you're supposed to memorize. And it kind of is one—about two dozen entries. Some are about people (&lt;em&gt;code is read more than it is written&lt;/em&gt;, &lt;em&gt;be kind to your future self&lt;/em&gt;, &lt;em&gt;tend to the garden&lt;/em&gt;), some are general engineering taste (&lt;em&gt;don't be clever&lt;/em&gt;, &lt;em&gt;complexity kills&lt;/em&gt;, &lt;em&gt;simple is beautiful&lt;/em&gt;). Two are unmistakably about machines: &lt;em&gt;write it like C&lt;/em&gt;, and &lt;em&gt;avoid polymorphism by default&lt;/em&gt;, which the long form spells out as "monomorphic is best, polymorphic is not great, megamorphic is terrible." Those last three terms are counting how many different kinds of thing one spot in your code has had to handle: one, a handful, or more than the engine will keep track of.&lt;/p&gt;

&lt;p&gt;What I didn't expect is how many of those middle entries are also about a machine. &lt;em&gt;Don't be clever&lt;/em&gt; means something specific once you know what happens when you try to outsmart a speculative optimizer. Same with &lt;em&gt;mistakes are infectious&lt;/em&gt;, &lt;em&gt;fix them early&lt;/em&gt;, and &lt;em&gt;stability is a feature&lt;/em&gt;. A lot of what looks like taste turns out to be doing real work.&lt;/p&gt;

&lt;p&gt;I'm not a compiler engineer, I've never written a numerical kernel, and I don't write C. My job is making stdlib's conventions legible to people arriving at the codebase, and I couldn't explain half of these without knowing what V8 actually does with a loop—so I went and found out. The useful discovery, for anyone else who doesn't write C either: &lt;em&gt;write it like C&lt;/em&gt; asks for less knowledge of C than you'd think. What it actually asks for is knowing what the optimizer is trying to prove. What follows is the map I wanted when I started, assembled from the V8 team's writing about their own pipeline and from asking stdlib maintainers why the code looks the way it does. You don't need to have gone spelunking inside V8 for it to make sense—I hadn't when I started.&lt;/p&gt;

&lt;p&gt;The part that surprised me most: the rules have barely moved in a decade, while the machinery enforcing them has been rebuilt three or four times. That's why they're worth internalizing rather than looking up.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;On other engines:&lt;/strong&gt; this post is about V8. JavaScriptCore in Safari and SpiderMonkey in Firefox ship their own compiler architectures, and circle many of the same principles by different routes. V8 gets the attention here because it runs Chrome &lt;em&gt;and&lt;/em&gt; it's the runtime under Node.js and Deno, so most stdlib code ends up executing on it. Where I could, I've stuck to general principles rather than V8-specific quirks, since those are the parts likely to travel.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A note on vocabulary
&lt;/h2&gt;

&lt;p&gt;This post throws around terminology—hidden classes, shapes, inline caches, elements kinds, deoptimization—and uses some of it before explaining it. Don't worry—the explanations arrive later, where the terms are doing more work. If you'd rather have a running start, two pieces cover most of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mathias Bynens — &lt;a href="https://mathiasbynens.be/notes/shapes-ics" rel="noopener noreferrer"&gt;JavaScript engine fundamentals: Shapes and Inline Caches&lt;/a&gt; — hidden classes and inline caches, which is most of the vocabulary here; and&lt;/li&gt;
&lt;li&gt;Mathias Bynens — &lt;a href="https://v8.dev/blog/elements-kinds" rel="noopener noreferrer"&gt;Elements kinds in V8&lt;/a&gt; — how V8 tracks what is inside an array.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feeling a little out of your depth here is fine—I &lt;em&gt;definitely&lt;/em&gt; did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a numerical JavaScript lens?
&lt;/h2&gt;

&lt;p&gt;Most V8-pipeline explainers target general-purpose JS frameworks, page-load metrics, or web-app responsiveness. The optimizations they highlight absolutely apply to numerical code—function inlining, hidden-class stability, megamorphic call sites all matter here. They just aren't the &lt;em&gt;whole&lt;/em&gt; story for a &lt;code&gt;Float64Array&lt;/code&gt;-bound inner loop.&lt;/p&gt;

&lt;p&gt;Keep a loop like this one in mind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kd"&gt;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;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing about it is clever (which is the point). Assume &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;out&lt;/code&gt; are &lt;code&gt;Float64Array&lt;/code&gt;s and that &lt;code&gt;scale&lt;/code&gt; gets called thousands of times.&lt;/p&gt;

&lt;p&gt;Four of the engine's questions matter much more for numerical work than they do for typical app code, and all four are about that loop. Each names something the optimizer is trying to prove, and each returns further down with the mechanism attached—so this is a map to come back to, not something to hold in your head now.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Boxing and unboxing.&lt;/strong&gt; Does &lt;code&gt;x[ i ]&lt;/code&gt; reach the multiply as a raw 64-bit float in a CPU register, or as a heap-allocated wrapper the engine reads through on every operation? Numerical code multiplies that difference by a million.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elements kind.&lt;/strong&gt; Is &lt;code&gt;x&lt;/code&gt;'s backing store (the memory the elements actually live in) a contiguous block of doubles, or a tagged array of generic JS values, where every slot has to carry its own type? V8 tracks this per array, and the loop reads through that store on every iteration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deoptimization.&lt;/strong&gt; The optimizer bets that &lt;code&gt;scale&lt;/code&gt; only ever sees &lt;code&gt;Float64Array&lt;/code&gt;. What happens on the call that breaks the bet—when V8 has to throw the specialized code away? That bet gets tested constantly, because one kernel gets reused against different inputs all day long.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiering.&lt;/strong&gt; V8 has four compilers rather than one, and they trade compile time against code quality. Does &lt;code&gt;scale&lt;/code&gt; want the last cycle squeezed out of peak code, or to &lt;em&gt;reach&lt;/em&gt; peak code fast enough that a Node service or notebook session spends most of its time there? That second case is the common one for numerical code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of V8's recent architectural work comes back to one or another of these four: Sparkplug, Maglev, the Turboshaft intermediate representation (IR), mutable heap numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 15-year ladder
&lt;/h2&gt;

&lt;p&gt;Today's V8 has four JavaScript execution tiers. It did not start there. Each addition responds to a specific gap left by the previous tier.&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%2Fi7az5pi1ts05vz5pfohu.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%2Fi7az5pi1ts05vz5pfohu.png" alt="Five snapshots of V8's execution pipeline, one per era. December 2010: Full-codegen feeding Crankshaft, two tiers. May 2017 (V8 v5.9): Ignition feeding TurboFan, two tiers, both new. 2021 (V8 v9.1): Sparkplug inserted between them, three tiers. 2023 (Chrome M117): Maglev inserted before TurboFan, four tiers. March 2025: the same four tiers, with TurboFan's internals rebuilt from sea-of-nodes onto Turboshaft—the same tier rebuilt inside, not a fifth tier." width="800" height="721"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Crankshaft (2010–2017)
&lt;/h3&gt;

&lt;p&gt;In December 2010, Google &lt;a href="https://blog.chromium.org/2010/12/new-crankshaft-for-v8.html" rel="noopener noreferrer"&gt;shipped Crankshaft&lt;/a&gt;, V8's first speculative optimizing compiler. Crankshaft was layered &lt;em&gt;above&lt;/em&gt; V8's existing baseline compiler, &lt;em&gt;Full-codegen&lt;/em&gt;: cold code ran there, and once a function got hot, V8 promoted it to Crankshaft for re-compilation against type assumptions. Crankshaft used two intermediate representations: a high-level static-single-assignment (SSA) graph called &lt;em&gt;Hydrogen&lt;/em&gt;, and a lower-level form called &lt;em&gt;Lithium&lt;/em&gt;. It leaned heavily on inline-cache feedback collected by the running baseline code.&lt;/p&gt;

&lt;p&gt;Crankshaft is the V8 that stdlib-shaped JavaScript was first written against. Its rules still apply today—monomorphic call sites, stable hidden classes, no megamorphism, no surprise types. They were never about Crankshaft specifically; they described the &lt;em&gt;underlying contract&lt;/em&gt; between dynamic source and a speculative optimizer. One of those terms is worth unpacking now: a &lt;em&gt;hidden class&lt;/em&gt; is V8's internal record of an object's layout—which properties it has, in what order, at what offsets. Objects built the same way share one; code that only ever sees a single hidden class is code V8 can specialize hard. That's what &lt;em&gt;monomorphic&lt;/em&gt; means (a fuller treatment comes below).&lt;/p&gt;

&lt;p&gt;What Crankshaft was bad at, and what eventually retired it: keeping pace with new ECMAScript features. Generators, classes, destructuring, &lt;code&gt;let&lt;/code&gt;/&lt;code&gt;const&lt;/code&gt;, async functions—every one was either grudgingly supported through baseline-only paths or left unoptimized. The team had to add every new feature to &lt;em&gt;both&lt;/em&gt; compilers—and to a third C++ runtime that the bytecode-less baseline compiler interacted with. That cost was suffocating the team.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ignition + TurboFan (2016–2017)
&lt;/h3&gt;

&lt;p&gt;Ignition shipped first, in 2016, as a &lt;a href="https://v8.dev/blog/ignition-interpreter" rel="noopener noreferrer"&gt;bytecode interpreter&lt;/a&gt;—V8 hadn't previously had one. Ignition was motivated by &lt;em&gt;memory&lt;/em&gt;, not speed: a bytecode interpreter's memory footprint is dramatically smaller than baseline-compiled machine code, which mattered enormously for the mobile devices Chrome was deploying on.&lt;sup id="fnref1"&gt;1&lt;/sup&gt; As a side effect, Ignition gave V8 a single canonical place where every JavaScript program existed in a stable, structured form before any compiler ever looked at it. That single canonical form is what made the next three tiers possible.&lt;/p&gt;

&lt;p&gt;TurboFan shipped next, and in May 2017 V8 v5.9 &lt;a href="https://v8.dev/blog/launching-ignition-and-turbofan" rel="noopener noreferrer"&gt;made the new pipeline default&lt;/a&gt;: Ignition for first execution, TurboFan for hot functions. &lt;a href="https://v8.dev/blog/v8-release-61" rel="noopener noreferrer"&gt;Crankshaft was then removed in v6.1&lt;/a&gt;, and &lt;a href="https://v8.dev/blog/v8-release-62" rel="noopener noreferrer"&gt;Full-codegen followed in v6.2&lt;/a&gt;—the latter release alone deleted more than 30,000 lines of code. TurboFan introduced two big ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;em&gt;layered&lt;/em&gt; architecture with explicit phases (typing, simplification, lowering, scheduling, machine-specific instruction selection). Each supported CPU architecture needed only a few thousand lines of architecture-specific code, against more than ten thousand per chip architecture for Crankshaft.&lt;sup id="fnref2"&gt;2&lt;/sup&gt; The layering made it tractable to add new optimizations and new ES features.&lt;/li&gt;
&lt;li&gt;A "sea of nodes" intermediate representation that lets the compiler reorder operations very freely, exposing more optimization opportunities than a strictly ordered control-flow graph would.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For numerical code, TurboFan was much better than Crankshaft at &lt;em&gt;getting out of the way&lt;/em&gt;: it could see through more closures, inline more deeply, eliminate redundant hidden-class checks, and unbox numbers more aggressively. If your code was already monomorphic and shape-stable, TurboFan generated meaningfully tighter machine code than Crankshaft did.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sparkplug (2021)
&lt;/h3&gt;

&lt;p&gt;For the next four years, V8 was a two-tier system: Ignition → TurboFan. The gap between them was real. Ignition's bytecode dispatch is fast as interpreters go but pays a per-instruction decode cost; TurboFan only kicks in when a function has been called enough times to justify its compilation expense. Functions in the middle—called often enough to matter, not often enough to optimize—were stuck paying the interpreter tax indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://v8.dev/blog/sparkplug" rel="noopener noreferrer"&gt;Sparkplug&lt;/a&gt;, shipped in V8 9.1 in 2021, fills that gap. It is a non-optimizing baseline JIT that compiles directly from Ignition bytecode in a single pass, with no IR, no type specialization, and no hidden-class-based fast paths. It defers most real work to the same builtins Ignition uses; what it eliminates is the per-bytecode dispatch overhead. Leszek Swirski's framing in the Sparkplug post is what made it click for me: a CPU is itself an interpreter for machine code, and Sparkplug is a "transpiler" from Ignition's bytecode to the CPU's. The function moves from running in an emulator to running natively, but the work it does in each instruction is roughly identical.&lt;/p&gt;

&lt;p&gt;Sparkplug's stack frames are bit-compatible with Ignition's. Debuggers, profilers, and exception handlers don't need to know Sparkplug exists. That compatibility is also what makes mid-loop tier promotion (on-stack replacement) trivial: because the layout matches, the engine can compile a Sparkplug version of a hot loop and patch the stack frame in place.&lt;/p&gt;

&lt;p&gt;For numerical code, Sparkplug mostly matters at the boundaries—for kernels that are called occasionally, and for code paths around the hot loop that handle argument validation and dispatch. The actual hot loop, if you wrote it well, will eventually get to TurboFan or Maglev. Sparkplug is what runs the rest while the type feedback accumulates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Maglev (2023)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://v8.dev/blog/maglev" rel="noopener noreferrer"&gt;Maglev&lt;/a&gt;, introduced in Chrome M117 in 2023, is a mid-tier optimizing compiler slotted between Sparkplug and TurboFan. It uses the same type feedback TurboFan does, performs many of the same specializations, and emits real optimized machine code. The difference: Maglev compiles roughly 10× faster than TurboFan and 10× slower than Sparkplug, with code quality calibrated accordingly.&lt;sup id="fnref3"&gt;3&lt;/sup&gt; Its IR is a traditional &lt;a href="https://en.wikipedia.org/wiki/Static_single-assignment_form" rel="noopener noreferrer"&gt;SSA-plus-CFG&lt;/a&gt; form—the program as an ordered graph of basic blocks (a control-flow graph, CFG) in which every value is assigned exactly once. TurboFan's &lt;a href="https://v8.dev/blog/leaving-the-sea-of-nodes" rel="noopener noreferrer"&gt;sea of nodes&lt;/a&gt; goes the other way on ordering—operations float free of any fixed order, and the compiler works out afterward when each one happens. Fixing the order up front is what keeps Maglev's register allocation simple, and simple register allocation is most of why it compiles fast.&lt;/p&gt;

&lt;p&gt;Maglev is the moment in the pipeline where speculative type specialization arrives. For numerical code that means two things become true simultaneously, and earlier in the warm-up curve than they used to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hidden-class checks get folded.&lt;/strong&gt; Where a site has only ever seen &lt;code&gt;Float64Array&lt;/code&gt; arguments, Maglev collapses the hidden-class guard and resolves the element-load to a direct memory offset. The same monomorphism rules that pay off at TurboFan now pay off at Maglev.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Representation selection runs.&lt;/strong&gt; Maglev has an explicit phase that picks the in-register representation for every value: integer, float, or boxed object. A &lt;code&gt;Float64Array&lt;/code&gt; element is unambiguously a float, so Maglev picks the right register kind immediately. A plain &lt;code&gt;Array&lt;/code&gt; of mixed numbers and strings forces a conservative boxed representation through the whole loop. This is the moment unboxing actually happens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A second consequence of Maglev shows up on the way back. The fall itself is unchanged: a deoptimization returns execution to Ignition, and the invocation that broke the bet finishes in the interpreter—off Maglev and off TurboFan alike, not one tier down. What Maglev changes is the recovery. Sparkplug's code is never discarded, so the next call starts in baseline machine code rather than in the interpreter, and the optimizing tier the function reaches on the way back up is Maglev rather than TurboFan. Older talks describe the deopt cliff as full machine code → interpreter, with a long crawl back. The drop is the same height; the climb out is much shorter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Turboshaft (2023–2025), not an execution tier
&lt;/h3&gt;

&lt;p&gt;Turboshaft is the one entry on this list that isn't a tier, which is why the count above stays at four. In 2023 V8 began an internal effort to migrate TurboFan off the sea-of-nodes IR onto a more conventional CFG-based form, &lt;a href="https://v8.dev/blog/leaving-the-sea-of-nodes" rel="noopener noreferrer"&gt;called Turboshaft&lt;/a&gt;. The reasons were prosaic and accumulated over a decade: sea of nodes is flexible but cache-unfriendly (related operations end up scattered in memory), bugs are harder to investigate, and rewrite passes had grown gnarly. The blog post describing the move shipped in March 2025; by then the entire JavaScript backend of TurboFan had migrated. Compile times roughly halved versus the sea-of-nodes era.&lt;sup id="fnref4"&gt;4&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Turboshaft is invisible at the source level—the optimizations TurboFan does are still the optimizations it does—but it shifts the engineering economics. Faster compilation means functions reach optimized code sooner, so a bigger share of any run executes in fast code rather than warming up. For long-running Node services and notebook-style scientific JS, that compounds. It also makes future optimization work cheaper, which means more of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the ladder back together
&lt;/h2&gt;

&lt;p&gt;A simplified schematic of the modern pipeline, including deopts:&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%2Fxm7wfmhbciybl7t46njd.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%2Fxm7wfmhbciybl7t46njd.png" alt="A schematic read top to bottom. Across the top, an intake chain: JS source feeds parse, which feeds AST. From AST a single arrow drops into a vertical ladder of four execution tiers. Ignition interprets bytecode and collects type feedback; an arrow down labelled warm leads to Sparkplug, which emits machine code with no specialization; an arrow labelled hotter leads to Maglev, a specializing optimizer with fast compilation; an arrow labelled hottest leads to TurboFan or Turboshaft, the peak optimizer. Two separate dashed arrows run back upward along the right-hand side, one leaving Maglev and one leaving TurboFan, both labelled deopt and both terminating at Ignition—a deoptimization returns execution to the interpreter, not to the tier below, and that invocation finishes there. The baseline code is never discarded, so the next call starts in Sparkplug." width="800" height="622"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every promotion is gated on type-feedback signals collected by the tiers below. Every deoptimization returns to the interpreter, whichever tier it left. The path each function takes is its own—there is no global compile pass—and a function's currently-running tier is observable with &lt;a href="https://www.thenodebook.com/node-arch/v8-engine-intro#informational-flags" rel="noopener noreferrer"&gt;&lt;code&gt;node --trace-opt&lt;/code&gt;&lt;/a&gt; and friends.&lt;/p&gt;

&lt;p&gt;For numerical code: given enough warmup, an inner loop you care about will end up in TurboFan/Turboshaft (or Maglev, if it's not hot enough for TurboFan). The supporting code around it—argument validation, dispatch, the body of any function called occasionally—will sit at one of the lower tiers indefinitely. The discipline you apply to the hot loop pays off most at the top of the ladder; the discipline you apply to the supporting code pays off mostly by keeping a megamorphic inline-cache site or a hidden-class change from &lt;em&gt;deoptimizing the hot loop&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;One boundary worth naming: V8 also compiles WebAssembly, through its own baseline and optimizing tiers. It's a separate pipeline with a separate story, and out of scope here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The representation primitives that make this work
&lt;/h2&gt;

&lt;p&gt;The compiler tiers are the layer most people think about, but the &lt;em&gt;value representation&lt;/em&gt; they operate on is doing just as much of the work. A handful of pieces show up in every V8 numerical-performance discussion.&lt;/p&gt;

&lt;h3&gt;
  
  
  SMI vs. HeapNumber
&lt;/h3&gt;

&lt;p&gt;V8 uses pointer tagging. A 64-bit slot whose low bit is zero is a &lt;em&gt;Small Integer&lt;/em&gt; (SMI)—a signed integer stored directly in the slot, no allocation, no indirection. The payload is 31-bit on builds with pointer compression and 32-bit on builds without. That's worth checking rather than assuming: Chrome enables pointer compression, official Node builds don't, so on Node &lt;code&gt;2**31 - 1&lt;/code&gt; is still an SMI. &lt;code&gt;node -p "process.config.variables.v8_enable_pointer_compression"&lt;/code&gt; answers it for your build. A slot whose low bit is one is a tagged pointer to a heap object. V8 stores any number that doesn't fit in the SMI range—floats included—as a &lt;strong&gt;HeapNumber&lt;/strong&gt;: a heap-allocated wrapper containing a 64-bit double, referenced from the slot by a tagged pointer.&lt;/p&gt;

&lt;p&gt;That's why the &lt;em&gt;boxed vs. unboxed&lt;/em&gt; distinction matters. In Ignition and Sparkplug, every floating-point arithmetic operation reads through a HeapNumber wrapper, possibly allocates a new one for the result, and adds garbage-collection pressure. In Maglev or TurboFan, once the optimizer has stable feedback that a value is always a float, it strips the wrapper: the raw 64-bit value lives in a CPU floating-point register, and arithmetic compiles down to a single floating-point instruction. A tight loop of thousands of float operations collapses to bare register arithmetic. Without unboxing, every number in your hot loop allocates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pointer compression (V8 8.0, 2019)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://v8.dev/blog/pointer-compression" rel="noopener noreferrer"&gt;Pointer compression&lt;/a&gt; shrinks every tagged slot from 64 bits to 32 by allocating all V8 objects within a 4 GB region and storing offsets rather than full pointers. The "base" half is shared per isolate; the "index" half is what gets stored. The headline number: heap memory dropped ~40%.&lt;sup id="fnref5"&gt;5&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The visible effect on a numerical workload is mostly cache behavior. Halving slot sizes means each cache line holds twice as many tagged values, and arrays of objects (rare in stdlib's hot loops, common around them) become half as cache-cold. Pointer compression also constrains what the compiler can do—for instance, every dereference now requires a base+offset compute—but the cache wins generally outweigh the new instruction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mutable heap numbers (V8, 2025)
&lt;/h3&gt;

&lt;p&gt;A subtler 2025 addition: &lt;a href="https://v8.dev/blog/mutable-heap-number" rel="noopener noreferrer"&gt;in-place updates of HeapNumbers in script-context slots&lt;/a&gt;. Previously, a script-level &lt;code&gt;let seed = ...; seed = next(seed);&lt;/code&gt; pattern would allocate a fresh HeapNumber on each reassignment, even when the optimizer could prove only a single number was being threaded through. Now the slot can &lt;em&gt;own&lt;/em&gt; its HeapNumber and mutate the underlying double in place. The case study in the post is &lt;code&gt;Math.random&lt;/code&gt;'s state. For numerical code that uses module-scoped accumulators or threaded random-number-generator state, this eliminates a previously-invisible per-update allocation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hidden classes and inline caches
&lt;/h3&gt;

&lt;p&gt;Every V8 object points to a &lt;strong&gt;hidden class&lt;/strong&gt; that records what properties it has, in what order, at what offsets, and with what attributes. The naming is genuinely a mess, and it isn't your fault if you've been tripped up by it. One concept, and every engine picked a different word for it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Calls them&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;V8&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Map&lt;/code&gt; — nothing to do with the JavaScript &lt;code&gt;Map&lt;/code&gt; builtin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SpiderMonkey&lt;/td&gt;
&lt;td&gt;Shape — widely borrowed, so you'll meet it outside Firefox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JavaScriptCore&lt;/td&gt;
&lt;td&gt;Structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chakra&lt;/td&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Hidden class&lt;/em&gt;&lt;/strong&gt; is the fifth name, and the one this post uses.&lt;sup id="fnref6"&gt;6&lt;/sup&gt; Academic papers use it—but so does V8, constantly, in its own documentation, whenever it's explaining rather than implementing. The heap doc introduces Maps as "also known as hidden classes or shapes" and heads the defining section "The Map (Hidden Class)"; V8's docs page on the subject is titled &lt;a href="https://v8.dev/docs/hidden-classes" rel="noopener noreferrer"&gt;&lt;em&gt;Maps (Hidden Classes) in V8&lt;/em&gt;&lt;/a&gt;. It isn't the obscure choice—it's the one term that travels across all four engines and back into V8's own prose.&lt;/p&gt;

&lt;p&gt;Two objects with the same hidden class are bit-compatible to V8's fast paths. Hidden classes are built incrementally: each added property transitions the object to a new one, which is why &lt;code&gt;{ x, y }&lt;/code&gt; and &lt;code&gt;{ y, x }&lt;/code&gt; end up different despite having the same property names.&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%2Fu2p4kxjfndlggaifb13c.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%2Fu2p4kxjfndlggaifb13c.png" alt="A transition tree. An empty object C0 branches two ways: adding x then y produces hidden class C2, holding { x: 1, y: 2 } at offsets x@0, y@1. Adding y then x produces a different hidden class C2-prime, holding { y: 2, x: 1 } at offsets y@0, x@1. Same keys, same values, different hidden classes." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you come to this from C or C++, Benedikt Meurer's &lt;a href="https://www.youtube.com/watch?v=IFWulQnM5E0" rel="noopener noreferrer"&gt;talk on types, classes, and maps&lt;/a&gt; offers the translation that tends to land hardest: a V8 hidden class plays roughly the role of a vtable pointer plus a field-offset table. "Keep the hidden class stable" then reads as the same discipline: don't reshape a struct at runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inline caches&lt;/strong&gt; (ICs) are per-call-site memoizations of the hidden classes the site has seen. The hot path replaces dictionary lookups with a hidden-class check plus a load from a known offset. As long as a site only sees one hidden class (&lt;em&gt;monomorphic&lt;/em&gt;) it stays fast. With two to four it becomes &lt;em&gt;polymorphic&lt;/em&gt;—a chain of compare-and-load attempts. Past that threshold V8 gives up and falls back to a generic dictionary lookup (&lt;em&gt;megamorphic&lt;/em&gt;), and the hot-loop assumption is over.&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%2Fciewdwskky8hnveer4iq.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%2Fciewdwskky8hnveer4iq.png" alt="One property-access site, a function that returns o.x, shown in three inline-cache states. Monomorphic, one hidden class: a single guard and a direct load, which inlines and lets repeat checks collapse. Polymorphic, two to four hidden classes: a compare-and-load chain, still inlinable but no longer able to collapse repeated checks. Megamorphic, five or more: no per-site list at all, just a probe into a global stub cache, with no inlining and no type information flowing downstream. The source code is identical in all three; only the hidden classes that reached the site differ. Cache state belongs to the call site, not to the caller: one caller passing a fifth hidden class moves the site for everyone using it—and nobody else did anything wrong." width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hidden-class stability buys more than a fast IC, though. As Vyacheslav Egorov works through in &lt;a href="https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html" rel="noopener noreferrer"&gt;&lt;em&gt;What's up with monomorphism?&lt;/em&gt;&lt;/a&gt;, a monomorphic site lets the optimizer treat repeated hidden-class checks as redundant and eliminate all but the first. A polymorphic site structurally cannot get that: each variant needs its own guard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ValidityCells&lt;/strong&gt; are the trick that lets prototype-based code stay fast. Each prototype's hidden class carries a single-bit "still valid?" flag; ICs that depend on a clean prototype chain can collapse all their lookup checks down to a read of that bit. A mutation anywhere on a prototype chain (including, catastrophically, on &lt;code&gt;Object.prototype&lt;/code&gt;) flips the cell, invalidating every IC that depended on it.&lt;/p&gt;

&lt;p&gt;Anything that participates in a hot dispatch path has to be shape-stable—argument-shape validators, factory functions for option objects, anything that builds an object on the way into the kernel. The cost of polymorphism is not an extra cycle per access; it is the &lt;em&gt;loss of the entire optimizer's bet on this call site.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Elements kinds
&lt;/h3&gt;

&lt;p&gt;For &lt;em&gt;array&lt;/em&gt; objects, V8 tracks a separate tag called the &lt;strong&gt;elements kind&lt;/strong&gt; that describes what's in the numerically-indexed slots. Plain &lt;code&gt;Array&lt;/code&gt; instances live on a six-cell lattice: &lt;code&gt;PACKED_SMI&lt;/code&gt; / &lt;code&gt;PACKED_DOUBLE&lt;/code&gt; / &lt;code&gt;PACKED_ELEMENTS&lt;/code&gt;, plus the three &lt;code&gt;HOLEY_*&lt;/code&gt; variants. The lattice is one-way. An array can be demoted from &lt;code&gt;PACKED_SMI&lt;/code&gt; to &lt;code&gt;PACKED_DOUBLE&lt;/code&gt; (by a single float write) to &lt;code&gt;PACKED_ELEMENTS&lt;/code&gt; (by a single string or object write), or to a &lt;code&gt;HOLEY_*&lt;/code&gt; variant (by &lt;code&gt;delete&lt;/code&gt;, by &lt;code&gt;new Array(n)&lt;/code&gt; without immediate fill, by writing past &lt;code&gt;length&lt;/code&gt;). On the value-type axis the demotion is permanent for the life of that array, even if the offending value is removed—writing an integer back over the float doesn't restore &lt;code&gt;PACKED_SMI&lt;/code&gt;. The holey axis is the one exception: &lt;code&gt;Array.prototype.fill&lt;/code&gt; can return a holey array to packed, including one made holey by &lt;code&gt;delete&lt;/code&gt;.&lt;sup id="fnref7"&gt;7&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Typed arrays—&lt;code&gt;Float64Array&lt;/code&gt;, &lt;code&gt;Int32Array&lt;/code&gt;, and friends—sit &lt;em&gt;outside&lt;/em&gt; this lattice on their own elements kinds (&lt;code&gt;FLOAT64_ELEMENTS&lt;/code&gt;, &lt;code&gt;INT32_ELEMENTS&lt;/code&gt;, etc.). They cannot be demoted, cannot be made holey, and cannot have their type changed—the typed-array API enforces all three. From V8's point of view, a typed-array element is already in its raw machine-level representation on the backing buffer; reading one is a memory load, not a wrapper unwrap.&lt;/p&gt;

&lt;p&gt;Typed arrays aren't an optimization; they're a guarantee. Every element is provably a fixed-width number, every access has a well-defined in-bounds outcome, every stored value is provably the right type.&lt;/p&gt;

&lt;p&gt;That guarantee, rather than raw speed, is why numerical stdlib code is built on them—and the distinction matters, because the obvious version of the claim isn't true. A plain &lt;code&gt;Array&lt;/code&gt; of integers isn't automatically slower than an &lt;code&gt;Int32Array&lt;/code&gt;. Typed arrays even do work a plain array doesn't: every write is coerced to the element type, so &lt;code&gt;2**31&lt;/code&gt; stored into an &lt;code&gt;Int32Array&lt;/code&gt; reads back as &lt;code&gt;-2147483648&lt;/code&gt;, and &lt;code&gt;300&lt;/code&gt; stored into a &lt;code&gt;Uint8ClampedArray&lt;/code&gt; reads back as &lt;code&gt;255&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What they buy isn't a lower floor. It's a narrower band. A plain array is fast right up until something demotes it, and then it's quietly slower for the rest of its life—and nothing in the code says so. A typed array can't be demoted, so the millionth call performs like the first. That's what &lt;em&gt;write it like C&lt;/em&gt; is asking for; the Zen says "prefer predictable performance," not peak performance.&lt;/p&gt;

&lt;p&gt;And one reason has nothing to do with the optimizer at all: a typed array is a view over an &lt;code&gt;ArrayBuffer&lt;/code&gt;, which is the layout a C routine expects on the other side of a native add-on, so the data crosses without being copied.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for stdlib-style code
&lt;/h2&gt;

&lt;p&gt;This is where the aphorisms cash out. Each convention below protects one specific decision the machinery above makes, and once you can name the decision, the convention stops reading as a prohibition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One dtype, one implementation.&lt;/strong&gt; stdlib ships separate &lt;code&gt;dasum&lt;/code&gt; and &lt;code&gt;sasum&lt;/code&gt;—double-precision and single-precision absolute-sum kernels—instead of one polymorphic kernel that dispatches on argument type. Two stories combine here, elements kinds and ICs: passing a &lt;code&gt;Float64Array&lt;/code&gt; and a &lt;code&gt;Float32Array&lt;/code&gt; to the same function makes its loop body see two different elements kinds, so the IC at the indexed-access site goes polymorphic, and the optimizer's bet gets weaker on every successive call. Two separate functions, each used only with its own dtype, stay monomorphic. stdlib makes this argument in its own source, too. &lt;code&gt;@stdlib/array/base/arraylike2object&lt;/code&gt; exists to normalize array-likes into one fixed-shape descriptor object, and its README says why in as many words: if objects are built with properties in different orders, "then those objects will have different 'hidden' classes," and a function fed enough of those shapes "will cause the function to be considered 'megamorphic'." The workaround is a helper whose only job is to hand every call site the same shape.&lt;/p&gt;

&lt;p&gt;One of stdlib's maintainers frames the whole discipline in terms of the language it's imitating: "Just like in C you have to create every single different typed interface as a new function, we're doing the exact same thing." The proliferation of near-identical kernels isn't duplication that a cleverer abstraction would collapse. It's the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hidden classes have to be stable up to and across the kernel.&lt;/strong&gt; When a kernel takes an options object—a strided-ndarray descriptor, a BLAS-style transposition flag—every call site has to build that object the same way, with the same property names in the same order. If a caller mutates the object after construction (adds a property, deletes one), it transitions the hidden class out of whatever the kernel's IC was specialized on. stdlib's convention follows from that: every property defined at construction time, in a fixed order, never mutated afterward—and, where it matters, that construction centralized in a factory rather than left to each call site to remember.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typed arrays do double duty.&lt;/strong&gt; They give you (1) shape stability—&lt;code&gt;Float64Array&lt;/code&gt; cannot become a &lt;code&gt;Float32Array&lt;/code&gt;, cannot acquire holes, cannot become megamorphic—and (2) unboxed representation—every element is a raw double on the backing buffer, no HeapNumber wrapper. A plain &lt;code&gt;Array&lt;/code&gt; of numbers can have the second: a &lt;code&gt;PACKED_DOUBLE&lt;/code&gt; array stores raw doubles in a &lt;code&gt;FixedDoubleArray&lt;/code&gt;, no wrappers. What it can't have is the first—and losing the first costs you the second. One string write demotes the array to &lt;code&gt;PACKED_ELEMENTS&lt;/code&gt;, and every element becomes a tagged pointer again. The guarantee is what makes the representation durable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loops stay lean.&lt;/strong&gt; TurboFan can optimize an enormous amount, but it cannot prove invariants the source doesn't make available to it. This is the mechanical content of &lt;em&gt;be explicit&lt;/em&gt;—the optimizer is one of the readers whose mental model won't match yours. Three patterns follow from it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Loop unswitching.&lt;/em&gt; Where a loop body branches on a value that doesn't change inside the loop (an &lt;code&gt;option === 'conjugate-transpose'&lt;/code&gt; flag, say), stdlib hoists the branch out and writes two loop bodies. TurboFan has loop-invariant code motion, but it can't always prove an opaque comparison is side-effect-free, so the fix lands at authoring time instead—as a stdlib maintainer puts it, "if you need to duplicate loops, so be it." (In benchmarks it does a second job: it keeps the optimizer from eliminating the unobserved branch and silently measuring less work than you meant to.)&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Scalar decomposition before entry.&lt;/em&gt; Where a complex scalar &lt;code&gt;alpha&lt;/code&gt; is used throughout a loop, it gets decomposed once before entry—&lt;code&gt;const re = real(alpha); const im = imag(alpha);&lt;/code&gt;—and the loop reads &lt;code&gt;re&lt;/code&gt; and &lt;code&gt;im&lt;/code&gt;. Even where TurboFan would have hoisted the calls, being explicit removes the uncertainty.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;No allocation inside the loop.&lt;/em&gt; Calling &lt;code&gt;.get(i)&lt;/code&gt; on a &lt;code&gt;Complex128Array&lt;/code&gt; inside a loop allocates a complex-number object every iteration, so kernels cast to a &lt;code&gt;Float64Array&lt;/code&gt; view of the same memory with &lt;code&gt;@stdlib/strided/base/reinterpret-complex128&lt;/code&gt; and read pairs of doubles instead. It's the same memory layout a C implementation would walk with a &lt;code&gt;double*&lt;/code&gt; over interleaved real and imaginary parts, which is the part of &lt;em&gt;write it like C&lt;/em&gt; that's literal rather than analogical. &lt;a href="https://blog.stdlib.io/introducing-the-accessor-protocol-for-array-like-objects/" rel="noopener noreferrer"&gt;Athan Reines' accessor-protocol post&lt;/a&gt; unpacks that two-layer arrangement: logical complex-values on top, raw floats underneath.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The through-line: move anything invariant across iterations out of the loop at authoring time, rather than leaving it for the compiler to maybe hoist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain arrays stay packed.&lt;/strong&gt; Where a plain &lt;code&gt;Array&lt;/code&gt; is unavoidable—the stdlib &lt;code&gt;generic&lt;/code&gt; dtype, argument lists—stdlib builds it with a literal &lt;code&gt;[ ]&lt;/code&gt; or with &lt;code&gt;push&lt;/code&gt; rather than &lt;code&gt;new Array(n)&lt;/code&gt; followed by index assignment, and removes indices with &lt;code&gt;splice&lt;/code&gt; rather than &lt;code&gt;delete&lt;/code&gt;. Once an array transitions to &lt;code&gt;HOLEY_*&lt;/code&gt;, every read pays for a possible prototype-chain walk in case &lt;code&gt;Array.prototype[i]&lt;/code&gt; was set. That cost is what &lt;em&gt;fix them early&lt;/em&gt; is pointing at: on the value-type axis the lattice is one-way, so there's no later cleanup pass that undoes it, and even on the recoverable holey axis the fix has to be deliberate—&lt;code&gt;fill&lt;/code&gt; is something you have to know to reach for, not something that happens on its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prototypes don't change after instances exist.&lt;/strong&gt; Adding a method to a constructor's prototype after instances exist invalidates every IC that watched that prototype. Adding anything to &lt;code&gt;Object.prototype&lt;/code&gt; invalidates every prototype-walking IC in the running program. This is &lt;em&gt;mistakes are infectious&lt;/em&gt; with a runtime mechanism behind it—one upstream mutation is paid for by every downstream caller, none of whom did anything wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify, don't theorize.&lt;/strong&gt; This was the part I found most reassuring, because it means none of the above has to be taken on faith. Run Node.js with &lt;code&gt;--allow-natives-syntax&lt;/code&gt; and use &lt;code&gt;%HaveSameMap(a, b)&lt;/code&gt; to confirm two objects share a hidden class, or &lt;code&gt;%DebugPrint(arr)&lt;/code&gt; to read off an array's elements kind. Run with &lt;code&gt;--trace-opt --trace-deopt&lt;/code&gt; to watch promotions and deopts scroll past, with the reason attached—&lt;code&gt;wrong map&lt;/code&gt; is the one you'll see most, and it's worth knowing that's the string V8 prints for a hidden-class mismatch. IC state transitions are a little more work: the old &lt;code&gt;--trace-ic&lt;/code&gt; flag no longer exists, and its replacement &lt;code&gt;--log-ic&lt;/code&gt; writes to a file rather than to your terminal—V8's own &lt;code&gt;tools/ic-processor&lt;/code&gt; then reads that file. For the generated machine code, &lt;code&gt;--print-opt-code&lt;/code&gt; dumps it. For the IR, &lt;code&gt;--trace-turbo&lt;/code&gt; plus &lt;a href="https://v8.github.io/tools/head/turbolizer/" rel="noopener noreferrer"&gt;Turbolizer&lt;/a&gt;.&lt;sup id="fnref8"&gt;8&lt;/sup&gt; The decisions that matter for a hot path are observable, which means "is this really monomorphic?" is a question you can answer rather than argue about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benchmark hygiene.&lt;/strong&gt; Microbenchmarks that look reasonable can measure nothing—Egorov's &lt;a href="http://mrale.ph/talks/goto2016/" rel="noopener noreferrer"&gt;&lt;em&gt;Performance Through the Spyglass&lt;/em&gt;&lt;/a&gt; is the canonical demonstration, and his one-line version of it is hard to improve on: optimizers eat microbenchmarks for dinner. The optimizer is allowed to constant-propagate, dead-code-eliminate, hoist invariants out of the timing loop, and unroll. stdlib's benchmark convention defends against this by including explicit lightweight assertions—e.g., a NaN self-inequality check (&lt;code&gt;if ( x !== x ) { b.fail(...) }&lt;/code&gt;) inside and after the timing loop. The trick exploits NaN's inequality with itself: the check is false for any ordinary value, so the fail path never runs. But the compiler still has to observe the read of &lt;code&gt;x&lt;/code&gt; (which defeats dead-code elimination) and it can't fold the check away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this seems to be heading
&lt;/h2&gt;

&lt;p&gt;The rules—monomorphic call sites, stable hidden classes, typed-array storage, lean loops—have held for a decade. The infrastructure underneath has been rebuilt three or four times and presumably will be again. I'm not going to pretend to know what V8 ships next, but three things look like they're already in motion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Float16&lt;/strong&gt; and &lt;code&gt;Float16Array&lt;/code&gt; arrived in browsers recently. The numerical case for half precision is mostly machine-learning-shaped (model weights, intermediate activations) but that case will increasingly intersect numerical-JS code living next to ML pipelines. stdlib has already moved: &lt;code&gt;float16&lt;/code&gt; is a working dtype with a real &lt;code&gt;Float16Array&lt;/code&gt; behind it, and it participates in the promotion and safe-cast tables like any other. &lt;code&gt;complex32&lt;/code&gt; is the genuinely forward-looking one—it's named in &lt;code&gt;@stdlib/ndarray/dtypes&lt;/code&gt; and in the dtype-kind taxonomy, but no &lt;code&gt;Complex32Array&lt;/code&gt; exists yet to back it and its addition is slated as future work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Maglev → Turboshaft path keeps compressing the warmup curve.&lt;/strong&gt; A hot loop reaches optimized machine code earlier on each successive V8 release, and where parts of a call graph used to sit in Ignition, never quite crossing the TurboFan threshold, they now land in Maglev instead. That makes occasional-path code matter slightly more than it used to: polymorphism in a moderately-called function is now something the optimizer has to account for, where before it wasn't looking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutable heap numbers and similar fine-grained representation tricks&lt;/strong&gt; are the genre of optimization V8 is shipping right now. These are mostly invisible from the source (good code keeps getting faster) but they reward the same disciplines that are already best practice: fewer abstraction layers between the source's intent and a single number in a single register.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of this is invisible if the kernels are well-behaved. That's by design. The optimizer's job is to take code that &lt;em&gt;describes&lt;/em&gt; a numerical loop dynamically and &lt;em&gt;execute&lt;/em&gt; it as if it were a statically-typed one. The shorter the gap between those two, the easier its job.&lt;/p&gt;

&lt;p&gt;Which is where I ended up on the aphorisms. &lt;em&gt;Write it like C&lt;/em&gt; isn't nostalgia for a language most of stdlib's JavaScript never touches, and &lt;em&gt;don't be clever&lt;/em&gt; isn't a general plea for humility. They both describe the shape a speculative optimizer is looking for, written in the imperative because that's the form a convention takes. Fifteen years of compiler engineering went into recognizing that shape at runtime. The list is short because the shape is simple—which is also, I think, what &lt;em&gt;simple is beautiful&lt;/em&gt; is doing at the end of the Zen of stdlib.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  V8 team primary sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://blog.chromium.org/2010/12/new-crankshaft-for-v8.html" rel="noopener noreferrer"&gt;A New Crankshaft for V8&lt;/a&gt; — Chromium Blog, December 2010&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/blog/ignition-interpreter" rel="noopener noreferrer"&gt;Firing up the Ignition interpreter&lt;/a&gt; — V8 blog, 2016&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/blog/launching-ignition-and-turbofan" rel="noopener noreferrer"&gt;Launching Ignition and TurboFan&lt;/a&gt; — V8 blog, May 2017&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/blog/v8-release-61" rel="noopener noreferrer"&gt;V8 release v6.1&lt;/a&gt; — V8 blog (Crankshaft removed)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/blog/v8-release-62" rel="noopener noreferrer"&gt;V8 release v6.2&lt;/a&gt; — V8 blog (baseline compiler removed)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/blog/turbofan-jit" rel="noopener noreferrer"&gt;Digging into the TurboFan JIT&lt;/a&gt; — V8 blog&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/docs/turbofan" rel="noopener noreferrer"&gt;TurboFan docs&lt;/a&gt; — V8 docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/docs/hidden-classes" rel="noopener noreferrer"&gt;Maps (Hidden Classes) in V8&lt;/a&gt; — V8 docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chromium.googlesource.com/v8/v8/+/HEAD/docs/heap/objects-and-maps.md" rel="noopener noreferrer"&gt;Objects and Maps in V8&lt;/a&gt; — V8 in-repo docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chromium.googlesource.com/v8/v8/+/HEAD/docs/runtime/hidden-classes-and-ics.md" rel="noopener noreferrer"&gt;Hidden Classes and Inline Caches in V8&lt;/a&gt; — V8 in-repo docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/blog/pointer-compression" rel="noopener noreferrer"&gt;Pointer Compression in V8&lt;/a&gt; — V8 blog (V8 8.0)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/blog/sparkplug" rel="noopener noreferrer"&gt;Sparkplug—a non-optimizing JavaScript compiler&lt;/a&gt; — V8 blog, 2021 (V8 9.1)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/blog/maglev" rel="noopener noreferrer"&gt;Maglev: V8's Fastest Optimizing JIT&lt;/a&gt; — V8 blog, 2023 (Chrome M117)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/blog/leaving-the-sea-of-nodes" rel="noopener noreferrer"&gt;Land ahoy: leaving the Sea of Nodes&lt;/a&gt; — V8 blog, March 2025 (Turboshaft)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/blog/mutable-heap-number" rel="noopener noreferrer"&gt;Turbocharging V8 with mutable heap numbers&lt;/a&gt; — V8 blog, February 2025&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/blog/fast-properties" rel="noopener noreferrer"&gt;Fast properties in V8&lt;/a&gt; — V8 blog&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/blog/elements-kinds" rel="noopener noreferrer"&gt;Elements kinds in V8&lt;/a&gt; — Mathias Bynens, V8 blog&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v8.dev/blog/10-years" rel="noopener noreferrer"&gt;Celebrating 10 years of V8&lt;/a&gt; — V8 blog&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Talks and write-ups by V8 team members and adjacent practitioners
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Mathias Bynens — &lt;a href="https://mathiasbynens.be/notes/shapes-ics" rel="noopener noreferrer"&gt;&lt;em&gt;JavaScript engine fundamentals: Shapes and Inline Caches&lt;/em&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Mathias Bynens — &lt;a href="https://www.youtube.com/watch?v=m9cTaYI95Zc" rel="noopener noreferrer"&gt;&lt;em&gt;V8 internals for JavaScript developers&lt;/em&gt;&lt;/a&gt; (JSConf EU)&lt;/li&gt;
&lt;li&gt;Benedikt Meurer — &lt;a href="https://www.youtube.com/watch?v=IFWulQnM5E0" rel="noopener noreferrer"&gt;&lt;em&gt;JavaScript engines: a tale of types, classes, and maps&lt;/em&gt;&lt;/a&gt; (JSCamp Barcelona, 2018)&lt;/li&gt;
&lt;li&gt;Franziska Hinkelmann — &lt;a href="https://www.youtube.com/watch?v=p-iiEDtpy6I" rel="noopener noreferrer"&gt;&lt;em&gt;JavaScript engines—how do they even?&lt;/em&gt;&lt;/a&gt; (JSConf EU)&lt;/li&gt;
&lt;li&gt;Franziska Hinkelmann — &lt;a href="https://www.youtube.com/watch?v=j6LfSlg8Fig" rel="noopener noreferrer"&gt;&lt;em&gt;Performance Profiling for V8&lt;/em&gt;&lt;/a&gt; (Script'17)&lt;/li&gt;
&lt;li&gt;Vyacheslav Egorov — &lt;a href="https://www.youtube.com/watch?v=r76ZjdzFExg" rel="noopener noreferrer"&gt;&lt;em&gt;JavaScript Performance Through the Spyglass&lt;/em&gt;&lt;/a&gt; (&lt;a href="http://mrale.ph/talks/goto2016/" rel="noopener noreferrer"&gt;slides&lt;/a&gt;, GOTO Amsterdam, 2016)&lt;/li&gt;
&lt;li&gt;Vyacheslav Egorov — &lt;a href="https://mrale.ph/blog/2012/06/03/explaining-js-vms-in-js-inline-caches.html" rel="noopener noreferrer"&gt;&lt;em&gt;Explaining JavaScript VMs in JavaScript—Inline Caches&lt;/em&gt;&lt;/a&gt; — 2012; the explanation V8's own &lt;a href="https://v8.dev/blog/fast-properties" rel="noopener noreferrer"&gt;&lt;em&gt;Fast properties in V8&lt;/em&gt;&lt;/a&gt; sends readers to for inline caches&lt;/li&gt;
&lt;li&gt;Vyacheslav Egorov — &lt;a href="https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html" rel="noopener noreferrer"&gt;&lt;em&gt;What's up with monomorphism?&lt;/em&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bartek Szopka — &lt;a href="https://www.youtube.com/watch?v=MqHDDtVYJRI" rel="noopener noreferrer"&gt;&lt;em&gt;Everything you never wanted to know about JavaScript numbers&lt;/em&gt;&lt;/a&gt; (JSConf EU 2013)&lt;/li&gt;
&lt;li&gt;Ishtmeet Singh — &lt;a href="https://www.thenodebook.com/node-arch/v8-engine-intro" rel="noopener noreferrer"&gt;&lt;em&gt;V8 JavaScript Engine in Node.js: Architecture, Tiers, Shapes, and Deoptimization&lt;/em&gt;&lt;/a&gt; — &lt;em&gt;The NodeBook&lt;/em&gt;, September 2025&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  stdlib and stdlib-adjacent
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/stdlib-js/stdlib" rel="noopener noreferrer"&gt;&lt;code&gt;stdlib-js/stdlib&lt;/code&gt;&lt;/a&gt; — the codebase&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/stdlib-js/stdlib/tree/develop/lib/node_modules/%40stdlib/array/dtypes" rel="noopener noreferrer"&gt;&lt;code&gt;@stdlib/array/dtypes&lt;/code&gt;&lt;/a&gt; — dtype registry; the "C type system at the API level"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/stdlib-js/stdlib/tree/1743c65c572a188de7ccae946f542efce8755a0f/lib/node_modules/%40stdlib/blas/base/dasum" rel="noopener noreferrer"&gt;&lt;code&gt;@stdlib/blas/base/dasum&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/stdlib-js/stdlib/tree/1743c65c572a188de7ccae946f542efce8755a0f/lib/node_modules/%40stdlib/blas/base/sasum" rel="noopener noreferrer"&gt;&lt;code&gt;@stdlib/blas/base/sasum&lt;/code&gt;&lt;/a&gt; — the canonical "one dtype, one implementation" pair&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/stdlib-js/stdlib/tree/1743c65c572a188de7ccae946f542efce8755a0f/lib/node_modules/%40stdlib/blas/base/zaxpy/lib/ndarray.js" rel="noopener noreferrer"&gt;&lt;code&gt;@stdlib/blas/base/zaxpy&lt;/code&gt; (&lt;code&gt;ndarray.js&lt;/code&gt;)&lt;/a&gt; — the reference implementation of the lean-inner-loop pattern with &lt;code&gt;reinterpret-complex128&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Athan Reines — &lt;a href="https://blog.stdlib.io/zen-of-stdlib/" rel="noopener noreferrer"&gt;&lt;em&gt;The Zen of stdlib&lt;/em&gt;&lt;/a&gt; — blog.stdlib.io; goes into considerably more detail than the &lt;a href="https://github.com/stdlib-js/stdlib/blob/develop/docs/contributing/zen_of_stdlib.md" rel="noopener noreferrer"&gt;contributing-docs version&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Athan Reines — &lt;a href="https://blog.stdlib.io/introducing-the-accessor-protocol-for-array-like-objects/" rel="noopener noreferrer"&gt;&lt;em&gt;Introducing the Accessor Protocol for Array-Like Objects&lt;/em&gt;&lt;/a&gt; — blog.stdlib.io&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;
    &lt;em&gt;Mara Averick is a developer advocate at &lt;a href="https://quansight.com/" rel="noopener noreferrer"&gt;Quansight&lt;/a&gt; and contributor experience lead for &lt;a href="https://github.com/stdlib-js/stdlib" rel="noopener noreferrer"&gt;stdlib&lt;/a&gt;.&lt;/em&gt;
&lt;/p&gt;




&lt;p&gt;&lt;a href="https://github.com/stdlib-js/stdlib" rel="noopener noreferrer"&gt;stdlib&lt;/a&gt; is an open source software project dedicated to providing a comprehensive suite of robust, high-performance libraries to accelerate your project's development and give you peace of mind knowing that you're depending on expertly crafted, high-quality software.&lt;/p&gt;

&lt;p&gt;If you've enjoyed this post, give us a star 🌟 on &lt;a href="https://github.com/stdlib-js/stdlib" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and consider &lt;a href="https://opencollective.com/stdlib" rel="noopener noreferrer"&gt;supporting&lt;/a&gt; the project. Your contributions and continued support help ensure the project's long-term success and are greatly appreciated!&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgments
&lt;/h2&gt;

&lt;p&gt;This work was supported in part by the National Science Foundation under &lt;a href="https://www.nsf.gov/awardsearch/showAward?AWD_ID=2449410&amp;amp;HistoricalAwards=false" rel="noopener noreferrer"&gt;Award No. 2449410&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclaimer: Any opinions, findings and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;V8's &lt;a href="https://v8.dev/blog/ignition-interpreter" rel="noopener noreferrer"&gt;Ignition announcement&lt;/a&gt; gives both figures: baseline-compiled machine code was occupying roughly a third of Chrome's JavaScript heap, and Ignition's bytecode is 25–50% the size of the equivalent baseline machine code.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;The &lt;a href="https://v8.dev/blog/launching-ignition-and-turbofan" rel="noopener noreferrer"&gt;launch post&lt;/a&gt; puts Crankshaft at "more than ten thousand lines of code per chip architecture," against a few thousand for TurboFan's architecture-specific layer.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;Both ratios are &lt;a href="https://v8.dev/blog/maglev" rel="noopener noreferrer"&gt;the V8 team's own&lt;/a&gt;: Maglev compiles roughly an order of magnitude faster than TurboFan and an order of magnitude slower than Sparkplug.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;The &lt;a href="https://v8.dev/blog/leaving-the-sea-of-nodes" rel="noopener noreferrer"&gt;Turboshaft write-up&lt;/a&gt; reports compile time "divided by 2" versus the sea-of-nodes pipeline.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;The ~40% heap-memory reduction is reported in V8's &lt;a href="https://v8.dev/blog/pointer-compression" rel="noopener noreferrer"&gt;pointer-compression write-up&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;The engine-by-engine mapping is Mathias Bynens and Benedikt Meurer's, from &lt;a href="https://mathiasbynens.be/notes/shapes-ics" rel="noopener noreferrer"&gt;&lt;em&gt;JavaScript engine fundamentals: Shapes and Inline Caches&lt;/em&gt;&lt;/a&gt; (14 June 2018). Their list adds the collision warnings the table compresses away: &lt;em&gt;Hidden Classes&lt;/em&gt; is confusing with respect to JavaScript classes, &lt;em&gt;Maps&lt;/em&gt; with respect to JavaScript &lt;code&gt;Map&lt;/code&gt;s, and &lt;em&gt;Types&lt;/em&gt; with respect to &lt;code&gt;typeof&lt;/code&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;V8's &lt;a href="https://v8.dev/blog/elements-kinds" rel="noopener noreferrer"&gt;elements-kinds post&lt;/a&gt; carries an inline amendment, "Update @ 2025-02-28: There is now an exception to this for &lt;code&gt;Array.prototype.fill&lt;/code&gt; specifically," and gives no further detail. Testing on Node v24.19.0 puts the exception slightly wider than the note implies: &lt;code&gt;fill&lt;/code&gt; restores packed status from &lt;code&gt;delete&lt;/code&gt;-induced holes as well.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;Singh's &lt;em&gt;NodeBook&lt;/em&gt; has flag tables—&lt;a href="https://www.thenodebook.com/node-arch/v8-engine-intro#informational-flags" rel="noopener noreferrer"&gt;informational&lt;/a&gt;, &lt;a href="https://www.thenodebook.com/node-arch/v8-engine-intro#behavioral-flags" rel="noopener noreferrer"&gt;behavioral&lt;/a&gt;, and &lt;a href="https://www.thenodebook.com/node-arch/v8-engine-intro#how-to-use-flags" rel="noopener noreferrer"&gt;how to pass them&lt;/a&gt;—which are a far better starting point than the &lt;a href="https://nodejs.org/docs/latest-v24.x/api/v8.html#v8setflagsfromstringflags" rel="noopener noreferrer"&gt;raw &lt;code&gt;--v8-options&lt;/code&gt; dump&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>javascript</category>
      <category>devrel</category>
    </item>
    <item>
      <title>Your AI Policy Doesn't Have to Apply to You</title>
      <dc:creator>Mara Averick</dc:creator>
      <pubDate>Wed, 12 Aug 2026 14:06:03 +0000</pubDate>
      <link>https://dev.to/stdlib/your-ai-policy-doesnt-have-to-apply-to-you-m6o</link>
      <guid>https://dev.to/stdlib/your-ai-policy-doesnt-have-to-apply-to-you-m6o</guid>
      <description>&lt;p&gt;A confession: stdlib doesn't have an AI-use policy.&lt;/p&gt;

&lt;p&gt;It isn't for lack of trying. There's an open RFC proposing guidance on AI usage—citations to prior art, disclosure requirements, a carve-out for "good first issue"&lt;sup id="fnref1"&gt;1&lt;/sup&gt;—and a draft PR with the policy text, both of which are from December 2025.&lt;sup id="fnref2"&gt;2&lt;/sup&gt; Both are still open. Both are still drafts.&lt;/p&gt;

&lt;p&gt;It's not because AI usage is hypothetical here, either. We've had stdlib core contributors participating in &lt;a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/" rel="noopener noreferrer"&gt;METR's randomized trial&lt;/a&gt; on AI's effect on experienced open-source developers since 2025. Philipp Burckhardt, one of stdlib's co-maintainers, wrote up &lt;a href="https://blog.stdlib.io/reflection-on-the-metr-study-2025/" rel="noopener noreferrer"&gt;his experience&lt;/a&gt; on the project blog, including which tools he uses and how his workflow changed.&lt;sup id="fnref3"&gt;3&lt;/sup&gt; More recently, Karan Anand described &lt;a href="https://blog.stdlib.io/the-codebase-is-the-prompt/" rel="noopener noreferrer"&gt;using the codebase itself as the prompt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So: we use these tools, we've said so in public, and (beyond some check boxes in our PR template) we still haven't codified what we expect from anyone else. We've gotten stuck, and one of the sticking points has been an obvious one—discomfort with publishing a rule we're not going to hold ourselves to.&lt;/p&gt;

&lt;p&gt;This is a real objection. It's also, I've come to think, the wrong one, and we're not the only project stalling out on it. The question isn't &lt;em&gt;whether&lt;/em&gt; AI is being used on this project. It's whether there's anything written down that a contributor can read.&lt;/p&gt;

&lt;p&gt;Plenty of projects got past this. Melissa Weber Mendonça keeps a running catalog of what's been published;&lt;sup id="fnref4"&gt;4&lt;/sup&gt; Kate Holterhoff's RedMonk survey was up to eighty-six policies as of April 2026, dimensionalized by stance and primary concern;&lt;sup id="fnref5"&gt;5&lt;/sup&gt; CHAOSS's AI-alignment working group runs a companion list with an incident-and-discussion layer.&lt;sup id="fnref6"&gt;6&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Given the scope of the discourse around AI usage in open source, this is a strikingly small corpus—and a young one.&lt;sup id="fnref7"&gt;7&lt;/sup&gt; Nobody is late to a settled consensus here. As James Fredley put it, after a tier-by-tier read from the primary documents: "there is no single industry-wide standard yet."&lt;sup id="fnref8"&gt;8&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;What the policies themselves agree on is narrower than it first looks. Stance splits three ways—permissive, ban, undecided—and the stated concerns split at least three ways again: code quality, licensing liability, and ethics. Where they &lt;em&gt;do&lt;/em&gt; line up is on something small and specific: &lt;strong&gt;contributors are responsible for understanding what they submit.&lt;/strong&gt; Sean McLellan's case for the &lt;code&gt;Assisted-by:&lt;/code&gt; Git trailer states the reasoning plainly—using &lt;code&gt;Co-authored-by:&lt;/code&gt; claims shared authorship, and with it &lt;em&gt;accountability&lt;/em&gt;. A tool can't hold up its end of that bargain.&lt;sup id="fnref9"&gt;9&lt;/sup&gt; The trailer itself is one lightweight implementation; the claim underneath it is what travels.&lt;/p&gt;

&lt;p&gt;Accountability is what the trailer is &lt;em&gt;for&lt;/em&gt;; &lt;em&gt;responsibility&lt;/em&gt; is what the policies actually ask of you. Part of that is legal clarity—the human is the sole &lt;em&gt;author&lt;/em&gt; of the commit, whatever helped them write it. But what responsibility looks like in practice depends on the author's relationship to the project.&lt;/p&gt;

&lt;p&gt;Two mechanisms tend to get bundled together here. &lt;strong&gt;Disclosure&lt;/strong&gt; is a record-keeping rule—the &lt;code&gt;Assisted-by:&lt;/code&gt; trailer, the &lt;a href="https://github.com/stdlib-js/stdlib/blob/fd17bfa9bc99c1abbd3d99e882cb18bddab42c08/.github/PULL_REQUEST_TEMPLATE.md?plain=1#L37-L55" rel="noopener noreferrer"&gt;AI-assistance section of our own PR template&lt;/a&gt;—and what it produces is provenance: &lt;em&gt;what helped write this&lt;/em&gt;. Everyone takes part in that one. &lt;strong&gt;Gating&lt;/strong&gt; is a permission rule: who may use these tools, on what, with how much latitude. What follows is an argument about the gate, not the disclosure.&lt;/p&gt;

&lt;p&gt;A maintainer who merges a bad change is still there in three months when it breaks. They wrote the module it broke, or reviewed it, and they will be the one bisecting at midnight to find out why the build went red. Responsibility, for them, isn't a claim. It's a structural fact about where they're standing.&lt;/p&gt;

&lt;p&gt;An infrequent contributor who submits a bad change usually isn't there. Not out of bad faith—that's simply what infrequent means. Whatever &lt;em&gt;I take responsibility for this&lt;/em&gt; means when they sign off on their PR, it can't mean quite the same thing as it does for a maintainer. The position it's said from is different.&lt;/p&gt;

&lt;p&gt;So a policy that says &lt;em&gt;you're responsible for what you submit&lt;/em&gt; is asking two structurally different things of two structurally different people, while looking like it asks one thing of everyone. That's roughly where our hypocrisy objection came from. It's also, it turns out, where the objection comes apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  The word for it
&lt;/h2&gt;

&lt;p&gt;There's a word for a rule that behaves this way, and I'm going to use it even though it's unfamiliar, because the precise word does work the familiar ones don't.&lt;/p&gt;

&lt;p&gt;A rule can be &lt;strong&gt;defeasible&lt;/strong&gt;: correct as a default, and &lt;em&gt;properly overridden&lt;/em&gt; when the situations it applies to genuinely differ. The everyday version is &lt;em&gt;the speed limit is 45, except for the ambulance.&lt;/em&gt; The rule holds. The exception is principled. The exception doesn't retroactively invalidate the rule.&lt;sup id="fnref10"&gt;10&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The open-source policy version goes something like this:&lt;/p&gt;

&lt;p&gt;There's a naïve consistency heuristic: &lt;em&gt;don't require of others what you wouldn't do yourself&lt;/em&gt;. It's correct for most governance choices. Don't require signed commits if you don't sign yours. Don't require test coverage your codebase doesn't have.&lt;/p&gt;

&lt;p&gt;But it's &lt;em&gt;defeasible&lt;/em&gt; where the situations genuinely differ, and AI-use policy is the clean case. A maintainer using AI owns the fallout, holds the project's mental model, and can recognize when the output is wrong. A first-time contributor using AI may be offloading understanding to the tool—producing work they can't fully vouch for—and has no accountability chain yet. Those are different situations. Different rules are warranted.&lt;/p&gt;

&lt;p&gt;The distinction comes down to what's underneath the rule. &lt;em&gt;Because I own the fallout in a way you don't&lt;/em&gt; is principled. &lt;em&gt;Because I said so&lt;/em&gt; isn't. The trust ledger is what makes the difference legible, and it's why "just be consistent" is worse advice than it sounds.&lt;/p&gt;

&lt;p&gt;The naïve-consistency rule is itself defeasible. That's the whole thing. It's a good default, it does honest work most of the time, and this is one of the situations where the exception is warranted rather than merely convenient.&lt;/p&gt;

&lt;p&gt;Ghostty's &lt;code&gt;AI_POLICY.md&lt;/code&gt; lays out the usual disclosure-and-human-in-the-loop rules for contributions, and then closes with this:&lt;sup id="fnref11"&gt;11&lt;/sup&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These rules apply only to outside contributions to Ghostty. &lt;br&gt;
Maintainers are exempt from these rules and may use AI tools at their discretion; they've proven themselves trustworthy to apply good judgment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two sentences. The first says &lt;em&gt;this policy is for you, the contributor, not for me.&lt;/em&gt; The second says &lt;em&gt;why.&lt;/em&gt; And the why isn't brash authority—it's a claim about accumulated context. &lt;em&gt;They've proven themselves.&lt;/em&gt; You can agree with it, or push back on it, or ask what the proving consisted of.&lt;/p&gt;

&lt;p&gt;So: you (potential contributor) can weigh and evaluate the claim. Naming the asymmetry on the page—rather than writing a symmetric-sounding rule and then quietly not applying it to yourself—is the move. It's two sentences. It's been shipped, in public. Just like &lt;a href="https://blog.stdlib.io/do-you-want-contributors/" rel="noopener noreferrer"&gt;a good &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;&lt;/a&gt;, the honest accounting is a kindness.&lt;/p&gt;

&lt;h2&gt;
  
  
  "You're just formalizing hierarchy"
&lt;/h2&gt;

&lt;p&gt;There's a critique here I too feel the pull of, and it might be part of what keeps projects like ours from making the move. &lt;em&gt;You're building a hierarchy where maintainers get one rule and contributors get another.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The hierarchy is already there, and has been the whole time—and it's not just maintainers vs. contributors, there are often tiers of trust (maintainers is just a useful shorthand). A maintainer with three years of context on a codebase reads a stranger's PR differently from how they read their own or one of a long-time contributor. The maintainer/reviewer knows what's load-bearing, what's under active refactor, which review objections are terminal versus fixable. None of that is malicious; most of it isn't even conscious. It's what accumulated context does. Call it a "trust ledger" (a term I'll return to in a subsequent post).&lt;/p&gt;

&lt;p&gt;What a written policy does is convert an existing asymmetry from &lt;strong&gt;invisible-and-unchallengeable&lt;/strong&gt; into &lt;strong&gt;visible-and-contestable&lt;/strong&gt;. That's an accountability gain, not a hierarchy invention. Writing it down is what makes it something you can be held to.&lt;/p&gt;

&lt;p&gt;If you've been sitting on a policy because the &lt;em&gt;but-I-don't-hold-myself-to-this&lt;/em&gt; objection kept stopping you: note it, acknowledge that it does ethical work worth respecting in most contexts, &lt;em&gt;and&lt;/em&gt; it doesn't apply here in the way it feels like it does. The situations genuinely differ. Different rules are principled when the difference has a reason you can be held to.&lt;/p&gt;

&lt;p&gt;I'm not saying every project should ship one; whether it's right for a given project depends on more than fits in a piece this size. What I &lt;em&gt;am&lt;/em&gt; saying is that this particular reason for stalling—the hypocrisy-avoidance reason, the one that stopped us—is dissolvable.&lt;/p&gt;

&lt;p&gt;Take it, if it's useful. Ours has been open since December; I'm hoping this is a step towards closing it out.&lt;/p&gt;




&lt;p&gt;
    &lt;em&gt;Mara Averick is a developer advocate at &lt;a href="https://quansight.com/" rel="noopener noreferrer"&gt;Quansight&lt;/a&gt; and contributor experience lead for &lt;a href="https://github.com/stdlib-js/stdlib" rel="noopener noreferrer"&gt;stdlib&lt;/a&gt;.&lt;/em&gt;
&lt;/p&gt;




&lt;p&gt;&lt;a href="https://github.com/stdlib-js/stdlib" rel="noopener noreferrer"&gt;stdlib&lt;/a&gt; is an open source software project dedicated to providing a comprehensive suite of robust, high-performance libraries to accelerate your project's development and give you peace of mind knowing that you're depending on expertly crafted, high-quality software.&lt;/p&gt;

&lt;p&gt;If you've enjoyed this post, give us a star 🌟 on &lt;a href="https://github.com/stdlib-js/stdlib" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and consider &lt;a href="https://opencollective.com/stdlib" rel="noopener noreferrer"&gt;supporting&lt;/a&gt; the project. Your contributions and continued support help ensure the project's long-term success and are greatly appreciated!&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgments
&lt;/h2&gt;

&lt;p&gt;This work was supported in part by the National Science Foundation under &lt;a href="https://www.nsf.gov/awardsearch/showAward?AWD_ID=2449410&amp;amp;HistoricalAwards=false" rel="noopener noreferrer"&gt;Award No. 2449410&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclaimer: Any opinions, findings and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;stdlib-js/stdlib issue &lt;a href="https://github.com/stdlib-js/stdlib/issues/9347" rel="noopener noreferrer"&gt;#9347&lt;/a&gt;, "[RFC]: Add guidance concerning AI usage"—proposing disclosure of AI assistance in pull requests and issues, excluding &lt;code&gt;good first issue&lt;/code&gt; work from AI-assisted resolution (per the draft PR's rule 5: "these issues are intended to help newcomers learn about the project and gain experience with the mechanics of contributing"), and ruling out model output posted as comments on GitHub or Zulip. Opened December 24, 2025.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;stdlib-js/stdlib pull request &lt;a href="https://github.com/stdlib-js/stdlib/pull/9459" rel="noopener noreferrer"&gt;#9459&lt;/a&gt;—the companion draft PR adding the policy text, opened December 31, 2025; still a draft as of this writing.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;Philipp Burckhardt, &lt;a href="https://blog.stdlib.io/reflection-on-the-metr-study-2025/" rel="noopener noreferrer"&gt;"Using AI in the development of stdlib,"&lt;/a&gt; Numerical Bits, July 17, 2025—a reflection on stdlib's participation in METR's &lt;a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/" rel="noopener noreferrer"&gt;"Impact of Early-2025 AI on Experienced Open-Source Developer Productivity"&lt;/a&gt; study, in which two stdlib core contributors worked randomized project issues under AI-allowed and AI-disallowed conditions—a snapshot from early 2025; both the models and the surrounding tooling have changed considerably since.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;Melissa Weber Mendonça, &lt;a href="https://github.com/melissawm/open-source-ai-contribution-policies" rel="noopener noreferrer"&gt;&lt;code&gt;open-source-ai-contribution-policies&lt;/code&gt;&lt;/a&gt;—a community-sourced catalog framed simply as "a list of policies by different open source projects about how to engage with AI-generated contributions." Roughly 100 project rows as of August 2026, plus separate sections for ongoing discussions and adjacent references.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;Kate Holterhoff, &lt;a href="https://redmonk.com/kholterhoff/2026/02/26/generative-ai-policy-landscape-in-open-source/" rel="noopener noreferrer"&gt;"The Generative AI Policy Landscape in Open Source,"&lt;/a&gt; RedMonk, February 26, 2026 (with rolling edits through April 12, 2026). The dimensionalized cross-section—stance, primary concern, disclosure requirement, adoption date—of the same underlying corpus Melissa's catalog and CHAOSS's list track from different registers. Companion &lt;a href="https://oss-ai-policies.netlify.app/" rel="noopener noreferrer"&gt;visualization site&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;CHAOSS AI Alignment Working Group, &lt;a href="https://github.com/chaoss/wg-ai-alignment/blob/main/moderation/README.md" rel="noopener noreferrer"&gt;"Awesome LLM Policy,"&lt;/a&gt; live PR-editable catalog with a nine-part taxonomy—including &lt;em&gt;discussion&lt;/em&gt; threads and &lt;em&gt;incident evidence&lt;/em&gt; as first-class categories alongside finished policy artifacts.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;The adjacent body of work is about as short as the list of trackers. Sviatoslav Sydorenko's &lt;a href="https://gist.github.com/webknjaz/1819dd466cc908ff09e2ed4d934455eb" rel="noopener noreferrer"&gt;EuroPython 2026 reference gist&lt;/a&gt; collects the maintainer-side tooling and files both Holterhoff's RedMonk piece and the CHAOSS list under "Surveys of the policy landscape"; the three trackers each reference the other two.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;James Fredley, &lt;a href="https://allthingsopen.org/articles/open-source-ai-contributions-assisted-by-git-trailer-standard" rel="noopener noreferrer"&gt;"Assisted-by: How open source projects are drawing the line on AI contributions,"&lt;/a&gt; All Things Open, May 11, 2026. A tier-by-tier read from the primary documents—QEMU, Gentoo, and NetBSD at the total-ban end; LLVM, Fedora, the Linux Kernel, Apache, OpenInfra, OpenTelemetry, and Rocky Linux across the permissive-with-disclosure middle—arguing that &lt;code&gt;Assisted-by:&lt;/code&gt; is settling in as the de facto convention over &lt;code&gt;Co-authored-by:&lt;/code&gt;, which "implies legal personhood." Fredley is chair of the Apache Grails PMC.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn9"&gt;
&lt;p&gt;Sean McLellan, &lt;a href="https://www.baristalabs.io/blog/ai-assisted-commits-need-provenance-trailer" rel="noopener noreferrer"&gt;"Assisted-by git trailer: provenance for AI-assisted commits,"&lt;/a&gt; Barista Labs, June 20, 2026. Key line: "Co-authorship says this entity shares authorship and accountability." Pulled into stdlib's own trailer discussion by Athan Reines' (&lt;a href="https://github.com/stdlib-js/stdlib/issues/9347#issuecomment-5052552276" rel="noopener noreferrer"&gt;comment&lt;/a&gt; on issue &lt;a href="https://github.com/stdlib-js/stdlib/issues/9347" rel="noopener noreferrer"&gt;#9347&lt;/a&gt;)—not necessarily the line's original point of entry into OSS AI-policy discourse, just where this piece's thread of it traces back to.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn10"&gt;
&lt;p&gt;The term has a longer life outside software. H. L. A. Hart, &lt;a href="http://www.jstor.org/stable/4544455" rel="noopener noreferrer"&gt;"The Ascription of Responsibility and Rights,"&lt;/a&gt; &lt;em&gt;Proceedings of the Aristotelian Society, New Series&lt;/em&gt; 49 (1948–1949): 171–194, is the canonical statement of the legal version—rules that hold unless a specific exception overrides them. John L. Pollock, &lt;a href="https://doi.org/10.1207/s15516709cog1104_4" rel="noopener noreferrer"&gt;"Defeasible Reasoning,"&lt;/a&gt; &lt;em&gt;Cognitive Science&lt;/em&gt; 11, no. 4 (1987): 481–518, formalizes the everyday version: how people reason with rough-and-ready rules that new information can &lt;em&gt;defeat&lt;/em&gt;, and what it takes for an override to count.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn11"&gt;
&lt;p&gt;Ghostty, &lt;a href="https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md" rel="noopener noreferrer"&gt;&lt;code&gt;AI_POLICY.md&lt;/code&gt;&lt;/a&gt; (fetched 2026-08-03). Iterated in public via PR &lt;a href="https://github.com/ghostty-org/ghostty/pull/8289" rel="noopener noreferrer"&gt;#8289&lt;/a&gt; (initial disclosure rules, August 2025) and PR &lt;a href="https://github.com/ghostty-org/ghostty/pull/10412" rel="noopener noreferrer"&gt;#10412&lt;/a&gt; (hardened into the standalone policy, January 2026).&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>devrel</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Do you WANT contributors?</title>
      <dc:creator>Mara Averick</dc:creator>
      <pubDate>Thu, 23 Jul 2026 16:12:00 +0000</pubDate>
      <link>https://dev.to/stdlib/do-you-want-contributors-37lo</link>
      <guid>https://dev.to/stdlib/do-you-want-contributors-37lo</guid>
      <description>&lt;p&gt;My open-source origin story goes something like this: &lt;em&gt;I fixed a typo, tweeted some stuff, tripped, fell, and found myself in the inner circle of a large-scale OSS ecosystem with an active, supportive community!&lt;/em&gt; It's trite but true—neither universal nor unique. But "inner circle of a large-scale OSS ecosystem" is where I landed, and it's colored a lot of what I've written since—including an implicit assumption that you (person who is reading this) are trying to build an ecosystem-scale project too.&lt;/p&gt;

&lt;p&gt;POSE (Pathways to Enable Open-Source Ecosystems) has it right there in the name. It expressly describes this shape as the first item in the list of activities it's meant to support:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Building and maintaining a distributed community of external contributors who will actively participate in the ongoing development of the open-source product.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;However, this isn't the only (or even the most common) experience for open-source maintainers or projects. Most projects run on one to three active maintainers.&lt;sup id="fnref1"&gt;1&lt;/sup&gt; Solo isn't a failure mode; it's the default.&lt;/p&gt;

&lt;p&gt;There's a wide spectrum of how folks define open source: from "it's just a license" to "radically open and participatory." For the rest of this post to land, I need you to accept the license-end as at least legitimate—not right, necessarily, just legitimate. There's no need to re-litigate the decades-long disagreement in order to say that you, as a maintainer, get to decide the bounds of participation in your project. &lt;/p&gt;

&lt;p&gt;An open-source project's first few artifacts are click-through: license (checkbox), &lt;code&gt;README&lt;/code&gt; (template), &lt;code&gt;.gitignore&lt;/code&gt; (whatever your framework spits out). Then you hit &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;—the first artifact that actually asks you what you want. Sure, there are best practices and helpful guides for you to follow, but it's where your project starts having opinions instead of defaults. So it's worth pausing and really unpacking what you want yours to look like. It's more than the mechanics of filing issues and submitting pull requests: &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; is the foundational framework for the relationship between your project and the unnamed masses who may or may not encounter it. &lt;/p&gt;

&lt;p&gt;In fairness, the bare-bones version of contributor guidelines may work just fine for you. Or you and your project may have a version that &lt;em&gt;was&lt;/em&gt; working in the past, but no longer feels sufficient.&lt;/p&gt;

&lt;p&gt;For years, I had a Venn diagram I used in talks to illustrate what I called the "FOSS happy place":&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%2F9l1ldkifeva125pw0s5b.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%2F9l1ldkifeva125pw0s5b.png" alt="Two overlapping circles labeled " width="799" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It wasn't a lie—it reflected my early experience as a contributor to open source (even if my contributions weren't code-shaped). It's still not "wrong," per se. But it &lt;em&gt;is&lt;/em&gt; guilty of radical oversimplification—not &lt;em&gt;quite&lt;/em&gt; discretizing a continuous variable, but &lt;em&gt;definitely&lt;/em&gt; improper dimensionality reduction. Critical features, like time, remuneration, and changing conditions/priorities are nowhere to be seen. &lt;/p&gt;

&lt;p&gt;I'm not the only one whose journey was typo fix → full-time job/way of life. Kent C. Dodds (among the best in the game of open-source developer experience, in my opinion) describes a similar origin story in &lt;a href="https://kentcdodds.com/blog/how-getting-into-open-source-has-been-awesome-for-me" rel="noopener noreferrer"&gt;How getting into Open Source has been awesome for me&lt;/a&gt; (2020). I mention him for two reasons, and neither is throwing him under the bus: one, it's not just me; two, it's not dev-rel delusion—because (unlike me) he's a lines-of-code, PR-reviews, engineer-shaped maintainer of a widely used open-source project (&lt;a href="https://testing-library.com/" rel="noopener noreferrer"&gt;Testing Library&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;A career in open source isn't always a heart-eyes-emoji experience. Evan Czaplicki gave a great (aptly named) talk at &lt;em&gt;Strange Loop&lt;/em&gt; 2018:  &lt;a href="https://youtu.be/o_4EX4dPppA?si=5PuZgCgQrGFxlOYn" rel="noopener noreferrer"&gt;The Hard Parts of Open Source&lt;/a&gt;. The challenges he describes seem somewhat inherent to the craft: you can't please everyone, conflicts will arise, things that once motivated you will be deeply draining at times. Anyone who's struggled through a group project in school will recognize the shape. None of that friction is new—LLMs didn't create maintainer burnout. In a 2019 piece by Nadia Eghbal, &lt;a href="https://increment.com/open-source/the-rise-of-few-maintainer-projects/" rel="noopener noreferrer"&gt;"The rise of few-maintainer projects"&lt;/a&gt;, she wrote:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The salient issue for maintainers today is less about growing contributor numbers and more about navigating the flow of developers who are clamoring for their time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sounds familiar, right? AI has ratcheted things up. Maintainers are seriously rethinking what their relationship to their projects and users looks like. And here's where I return to &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; because &lt;em&gt;this&lt;/em&gt; is a place where you get to be explicit about the choices you're making. Even if nobody reads every word (and most people don't read &lt;code&gt;CONTRIBUTING&lt;/code&gt; files at all), writing one is how you set your boundaries in the first place. A &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; that reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;No thanks! I'm gonna go it alone.
But feel free to fork and do whatever you like!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;may not be what you're used to seeing (in fairness, that sentiment is likely carried through the &lt;em&gt;absence&lt;/em&gt; of a CONTRIBUTING file), but it's legitimate. Or, maybe you think it isn't. Maybe it's anathema to open source, &lt;strong&gt;BUT&lt;/strong&gt; it &lt;em&gt;is&lt;/em&gt; a move that &lt;em&gt;someone&lt;/em&gt; could make.&lt;/p&gt;

&lt;p&gt;Am I recommending it? No. But I'm not you. I'm not everyone. And, importantly, it forces you to acknowledge that you get to set your boundaries and expectations. Again, this is an oversimplification. If you're maintaining a mature project with a set governance pattern and change-management process, waking up and burning your contributing process to the ground is likely to have serious fallout. But, that doesn't mean that you can't experiment (especially if the status quo feels like you're headed off a cliff.)&lt;/p&gt;

&lt;p&gt;Certain projects have, by design, made the bar for contribution high for legitimate reasons. Debian's already the perfect example of a working gatekeeping mechanism that's high-demand. You need a sponsor, you find a mentor, you meet in person with someone who can vouch for you—as Nadia Eghbal put it, &lt;em&gt;"Debian's process is built on the need for trust."&lt;/em&gt;&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;But Debian is one shape among several. Athan Reines pitched a taxonomy for this: not &lt;em&gt;"how open is your project"&lt;/em&gt; on a slider, but &lt;em&gt;"what shape of project are you actually running?"&lt;/em&gt; He described four archetypes: the walled castle, the clique, the university, and the hippie commune.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The walled castle (or black box).&lt;/strong&gt; You get the code, but the drawbridge stays up. SQLite is the cleanest live example. Their copyright page has a section literally titled "Open-Source, not Open-Contribution," which says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;SQLite is open-source, meaning that you can make as many copies of it as you want and do whatever you want with those copies, without limitation. But SQLite is not open-contribution...the project does not accept patches from random people on the internet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can't misread that. It's on their homepage. This archetype fits when the maintainer has a firm boundary they don't want to soften—a totally legitimate choice. It also fits when there's an author who's just done. The question: Do you want contributors? The answer: No.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The clique.&lt;/strong&gt; You need to know the right people to get the secret password. OpenBSD is the best not-pejoratively-clique example I could find. Their &lt;a href="https://www.openbsd.org/hackathons.html" rel="noopener noreferrer"&gt;hackathons page&lt;/a&gt; says, plainly: "Hackathon attendees come by invitation only. Some new people in the community who show promise are sometimes invited to see if they have what it takes." Linux kernel subsystems run on the same social logic at scale: the kernel docs describe a "chain of trust" where pull requests from unknown developers are received warily by subsystem maintainers until they've earned their way in. This archetype fits when the cost of misplaced trust is high and the mechanism for establishing trust is "does someone I already trust vouch for you."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The university.&lt;/strong&gt; If you do your homework and score high enough on your exams, we'll admit you. Debian's process, above, is the archetypal case: the sponsor and mentor are part of a longer sequence that includes an application manager, a Philosophy &amp;amp; Procedures exam, a Tasks &amp;amp; Skills exam, and then DAM approval (or not). Their own docs are unusually honest about the deal: &lt;em&gt;"the whole NM process is very strict and thorough. This is not meant to discourage people... but it does explain why the New Member process takes so much time."&lt;/em&gt; Kubernetes runs the most numerically explicit ladder I've seen: Member (sponsored by two reviewers from different companies), Reviewer (primary on 5+ PRs, reviewed 20+ substantial ones), Approver (30+ merged, nominated by a subproject owner). If you want the stdlib-adjacent case, &lt;a href="https://www.pyopensci.org/" rel="noopener noreferrer"&gt;pyOpenSci&lt;/a&gt; has documented onboarding flows for reviewers and editors: guest-editor probation, editor-in-chief nominations, a peer-review process modeled on academic journals. This archetype fits infrastructure that's load-bearing enough that mistakes are expensive to unwind, and it works if the syllabus is real. (It fails, badly, if the syllabus is a rebrand of "vibes-based homework we won't tell you the rules of.")&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hippie commune.&lt;/strong&gt; Do you have a pulse? Yes? You're in. freeCodeCamp basically checks for a pulse and hands you a &lt;code&gt;first timers only&lt;/code&gt; label. Their whole cultural stance is "let us help you land your first PR." I've been part of projects like this, and it's actually onerous in its own way. But, they're where a lot of folks get started. Without these, the rest of the spectrum loses its future contributors and maintainers. MDN Web Docs runs the same posture on the docs side: their contributing guide is basically "there's a place for you here, even if your grammar isn't perfect." (Wikipedia is the mother of this archetype but isn't code-shaped, so treat that as a cultural reference, not a technical one.) This archetype fits projects where the marginal contribution has low blast radius, and the real bottleneck is reach or discoverability, not integration cost. It also fits projects whose &lt;em&gt;goal&lt;/em&gt; is teaching people to contribute, where the code is almost a byproduct of the mentorship.&lt;/p&gt;

&lt;p&gt;In reality, there is, of course, a spectrum—and shades of gray as well. A project doesn't necessarily have to operate in a single mode, either. SQLite runs some clique-shaped review practices under its walled-castle surface. Debian's university process has real clique-shaped social realities inside it. Any given project can occupy more than one archetype at once—a codebase that's walled-castle at the core and hippie commune at the docs layer, say. And most projects drift between shapes as they age.&lt;/p&gt;

&lt;p&gt;I also want to be clear that you're ALLOWED TO MOVE where your project is at. Is hippie-commune mode failing you in the age of AI slop? Maybe you run an experiment with a more rigorous onboarding (or have folks who have already contributed to the project mentor newcomers through their first PR so that the onus doesn't always fall on the maintainer.)&lt;/p&gt;

&lt;p&gt;There is no more a platonic form for a perfect &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; than there is for a universally "perfect" job. The fit is between your project and its contributors. What works for one can be exactly wrong for another.&lt;/p&gt;

&lt;p&gt;But here's why the taxonomy belongs in a piece about what you actually want: it forces you to say the thing out loud. &lt;em&gt;"I want a hippie commune"&lt;/em&gt; is a different maintainer future than &lt;em&gt;"I want a university,"&lt;/em&gt; and both are different from &lt;em&gt;"I want to keep the code out here but the process in here."&lt;/em&gt; You can't dodge the question by writing a &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; that just describes issue templates. The people who read it (the ~7 who will) can tell which archetype you're aiming for even when you haven't named it. Might as well spell it out.&lt;/p&gt;

&lt;p&gt;There are pros and cons to the types of friction you introduce—selectively inviting who can interact, permanently banning a suspected peddler of AI slop, toggling off the "Issues" section of a repository. But, if the relationship (that between a maintainer and contributors) is evolving in such a way that it feels like it cannot hold, that's its own type of failure. Neither the maintainer nor "the community" are to &lt;em&gt;blame&lt;/em&gt; when things end in a silent resignation or a public crashout—but maybe they can be avoided by experimenting with things you hadn't previously considered as being "worth it." &lt;/p&gt;

&lt;p&gt;Like my old FOSS-happy-place Venn diagram, the chart below doesn't capture the n-dimensional reality of working in open source. But I'd like to think it's an improvement. On the x-axis you have maintainer/project (dis)comfort. And the y-axis is something akin to ROI (return on investment)—demand from contributors/community/ecosystem.&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%2F3h9r178elo1niefmyirc.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%2F3h9r178elo1niefmyirc.png" alt="A hand-drawn chart on graph paper. The vertical axis runs from " width="800" height="680"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The catch is that you don't really know where an experiment lands on this chart until you try it—not how much it'll cost you (the x-axis), and not what you'll get back (the y-axis). That doesn't mean you have to try everything. But it's worth paying attention to where things actually fall, rather than where you assumed they would.&lt;/p&gt;

&lt;p&gt;Here are some things that we're trying (or considering taking for a spin):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Code review as the alignment step.&lt;/strong&gt; Ed Yang makes the case that &lt;a href="https://blog.ezyang.com/2025/12/code-review-as-human-alignment-in-the-era-of-llms/" rel="noopener noreferrer"&gt;code review is where humans do the alignment work&lt;/a&gt; now. As AI takes on more of the implementation, the reviewer's job shifts away from line-by-line mechanics and toward the big-picture questions—does this fit, should it exist, is this the shape we want?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Using AI as a market-research companion, not a PR-generation machine.&lt;/strong&gt; Instead of &lt;em&gt;AI writes the pull request&lt;/em&gt;, it's &lt;em&gt;AI helps you see the landscape&lt;/em&gt;. Feed it the parallel projects—for us that's NumPy, SciPy, Math.js, and friends—let it aggregate the patterns everyone else has converged on, and then &lt;em&gt;you&lt;/em&gt; decide what fits. Same tool, pointed at &lt;em&gt;what should we build&lt;/em&gt; instead of &lt;em&gt;let's crank out code&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Livestreamed PR review.&lt;/strong&gt; Twitch, Discord, YouTube—pick your poison. Someone watches a maintainer work through a review in real time. The discomfort scales hard depending on who you are (some folks would genuinely rather chew glass than do their job on camera), but it puts other humans in the room at the moment you're deciding something. And it teaches in both directions: people watching learn what makes a good contribution &lt;em&gt;and&lt;/em&gt; what being a maintainer actually takes. It's knowledge capture without the curation—which means it might surface insights you never knew people wanted.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is guaranteed to work. Most of what gets written about open-source communities and contributor experience gets written after the fact—after something worked. That means there's a bottom drawer somewhere full of the things people tried that didn't (those posts mostly don't get written.) And even the experiments that did work worked in the &lt;em&gt;specific conditions&lt;/em&gt; of that project—its size, its maintainers, its users, its moment. Different projects, different fits. This isn't Wills and Trusts. You can iterate. Your &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; can change. Your governance can change. Your mind can change. Whatever you want to do is okay—just own it. And if you're trying something (the thing that's working, or the thing that isn't) come tell us about it. We're figuring it out too.&lt;/p&gt;

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




&lt;p&gt;
    &lt;em&gt;Mara Averick is a developer advocate at &lt;a href="https://quansight.com/" rel="noopener noreferrer"&gt;Quansight&lt;/a&gt; and contributor experience lead for &lt;a href="https://github.com/stdlib-js/stdlib" rel="noopener noreferrer"&gt;stdlib&lt;/a&gt;.&lt;/em&gt;
&lt;/p&gt;




&lt;p&gt;&lt;a href="https://github.com/stdlib-js/stdlib" rel="noopener noreferrer"&gt;stdlib&lt;/a&gt; is an open source software project dedicated to providing a comprehensive suite of robust, high-performance libraries to accelerate your project's development and give you peace of mind knowing that you're depending on expertly crafted, high-quality software.&lt;/p&gt;

&lt;p&gt;If you've enjoyed this post, give us a star 🌟 on &lt;a href="https://github.com/stdlib-js/stdlib" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and consider &lt;a href="https://opencollective.com/stdlib" rel="noopener noreferrer"&gt;supporting&lt;/a&gt; the project. Your contributions and continued support help ensure the project's long-term success and are greatly appreciated!&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgments
&lt;/h2&gt;

&lt;p&gt;This work was supported in part by the National Science Foundation under &lt;a href="https://www.nsf.gov/awardsearch/showAward?AWD_ID=2449410&amp;amp;HistoricalAwards=false" rel="noopener noreferrer"&gt;Award No. 2449410&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclaimer: Any opinions, findings and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Linux Foundation, OpenSSF, and Harvard LISH. &lt;em&gt;Census III of Free and Open Source Software.&lt;/em&gt; December 2024. &lt;a href="https://www.linuxfoundation.org/hubfs/LF%20Research/lfr_censusiii_120424a.pdf?hsLang=en" rel="noopener noreferrer"&gt;https://www.linuxfoundation.org/hubfs/LF%20Research/lfr_censusiii_120424a.pdf?hsLang=en&lt;/a&gt;; GitHub. &lt;em&gt;Octoverse 2025.&lt;/em&gt; &lt;a href="https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/" rel="noopener noreferrer"&gt;https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Nadia Eghbal. "The Rise of Few-Maintainer Projects." &lt;em&gt;Increment&lt;/em&gt;, 2019. &lt;a href="https://increment.com/open-source/the-rise-of-few-maintainer-projects/" rel="noopener noreferrer"&gt;https://increment.com/open-source/the-rise-of-few-maintainer-projects/&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>devrel</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
