oc 0.50.61 → 0.50.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -5
- package/.turbo/turbo-lint.log +2 -2
- package/.turbo/turbo-test-silent.log +12 -11
- package/.turbo/turbo-test.log +1806 -1750
- package/CHANGELOG.md +13 -0
- package/README.md +16 -0
- package/dist/cli/domain/local.js +7 -4
- package/dist/components/oc-client/_package/package.json +4 -4
- package/dist/components/oc-client/_package/server.js +1 -1
- package/dist/components/oc-client/package.json +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/registry/domain/events-handler.d.ts +2 -5
- package/dist/registry/domain/http-server/express-adapter.d.ts +2 -2
- package/dist/registry/domain/http-server/express-adapter.js +47 -2
- package/dist/registry/domain/http-server/types.d.ts +21 -7
- package/dist/registry/domain/metadata-migration.js +8 -1
- package/dist/registry/domain/options-sanitiser.d.ts +8 -2
- package/dist/registry/domain/options-sanitiser.js +2 -0
- package/dist/registry/domain/plugins-initialiser.js +41 -5
- package/dist/registry/domain/repository.js +100 -19
- package/dist/registry/domain/validators/registry-configuration.d.ts +5 -2
- package/dist/registry/domain/validators/registry-configuration.js +5 -0
- package/dist/registry/index.js +50 -29
- package/dist/registry/middleware/cors.d.ts +4 -0
- package/dist/registry/middleware/cors.js +62 -4
- package/dist/registry/router.js +4 -2
- package/dist/registry/routes/component-info.js +3 -2
- package/dist/registry/routes/component-preview.js +3 -2
- package/dist/registry/routes/component.d.ts +2 -1
- package/dist/registry/routes/component.js +1 -2
- package/dist/registry/routes/components.d.ts +2 -1
- package/dist/registry/routes/components.js +1 -2
- package/dist/registry/routes/helpers/get-component.d.ts +2 -1
- package/dist/registry/routes/helpers/get-component.js +7 -8
- package/dist/registry/routes/index.js +11 -7
- package/dist/resources/index.d.ts +5 -1
- package/dist/resources/index.js +5 -1
- package/dist/types.d.ts +31 -3
- package/dist/utils/bounded-cache.d.ts +10 -0
- package/dist/utils/bounded-cache.js +31 -0
- package/js-library-optimization-playbook.md +1379 -0
- package/package.json +4 -3
- package/tsconfig.types.json +10 -0
|
@@ -0,0 +1,1379 @@
|
|
|
1
|
+
# How to Make a JavaScript Library Fast
|
|
2
|
+
|
|
3
|
+
A playbook extracted by diffing a from-scratch reimplementation of a mature JS library
|
|
4
|
+
against the original. The reimplementation kept the same public API and passes the
|
|
5
|
+
original's vendored test suite, while reporting 5–30× throughput on hot operations and
|
|
6
|
+
1.2–2.7× on primitives.
|
|
7
|
+
|
|
8
|
+
Nothing here is domain-specific. The techniques are the transferable part.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 0. The meta-lesson: where the wins actually came from
|
|
13
|
+
|
|
14
|
+
The speedups are **not** uniformly distributed. Ranked by multiplier:
|
|
15
|
+
|
|
16
|
+
| Tier | Technique | Typical gain | Cost |
|
|
17
|
+
|---|---|---|---|
|
|
18
|
+
| 1 | **Skip the work entirely** (tiered fast lanes) | 5–30× | High correctness risk; needs parity tests |
|
|
19
|
+
| 2 | **Move work to setup time** (compile, precompute tables) | 2–10× | Memory; staleness if config mutates |
|
|
20
|
+
| 3 | **Cache at the right layer** (whole results, not intermediates) | 2–60× | Cache-invalidation bugs; unbounded growth |
|
|
21
|
+
| 4 | **Delete async** (sync lanes, no promises when nothing is pending) | 2–10× | Two code paths to keep in sync |
|
|
22
|
+
| 5 | **Delete allocations** (no intermediate arrays/objects/closures) | 1.5–3× | Readability |
|
|
23
|
+
| 6 | **Micro-optimize primitives** (charCodeAt over regex) | 1.2–2.7× | Readability; low ceiling |
|
|
24
|
+
|
|
25
|
+
Most engineers start at tier 6 and stop. **The 30× rows all came from tiers 1–4.** Tier 6
|
|
26
|
+
is where you go *after* the structure is right, and its main value is that it makes the
|
|
27
|
+
fast lanes' guard checks nearly free.
|
|
28
|
+
|
|
29
|
+
Second meta-lesson, equally important: **the correctness harness is not optional overhead —
|
|
30
|
+
it's what makes tiers 1–4 possible at all.** Every aggressive shortcut in this codebase is
|
|
31
|
+
backed by a test that pins the observable behaviour it's allowed to skip. Without that,
|
|
32
|
+
you cannot ship a fast lane; you can only ship a bug.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Tier 1 — Don't do the work
|
|
37
|
+
|
|
38
|
+
### 1.1 Build a dispatch ladder, cheapest check first
|
|
39
|
+
|
|
40
|
+
The single highest-leverage pattern. Instead of one general algorithm, stack progressively
|
|
41
|
+
more expensive strategies and return at the first hit:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
function resolve(input) {
|
|
45
|
+
// (1) identity memo: two === compares, zero hashing
|
|
46
|
+
if (input === hotKey && context === hotContext) return hotResult
|
|
47
|
+
|
|
48
|
+
// (2) per-instance one-slot memo
|
|
49
|
+
if (instance.lastKey === input) return instance.lastResult
|
|
50
|
+
|
|
51
|
+
// (3) precomputed exact-answer table (built at setup time)
|
|
52
|
+
const exact = instance.exactTable[input]
|
|
53
|
+
if (exact !== undefined) return remember(input, exact)
|
|
54
|
+
|
|
55
|
+
// (4) bounded result cache
|
|
56
|
+
const cached = instance.cache.get(input)
|
|
57
|
+
if (cached !== undefined) return remember(input, cached)
|
|
58
|
+
|
|
59
|
+
// (5) specialized walker for the common input class
|
|
60
|
+
const simple = trySimpleStrategy(input)
|
|
61
|
+
if (simple !== undefined) return remember(input, simple)
|
|
62
|
+
|
|
63
|
+
// (6) the general algorithm
|
|
64
|
+
return remember(input, generalStrategy(input))
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Each rung must be strictly cheaper than the one below it. Rung (1) costs two pointer
|
|
69
|
+
compares; rung (6) may cost a full tree walk. On a workload with any temporal locality you
|
|
70
|
+
almost never reach the bottom.
|
|
71
|
+
|
|
72
|
+
### 1.2 Fast lanes need a *narrowing* gate, not an approximation
|
|
73
|
+
|
|
74
|
+
The rule that keeps this honest: **a fast lane must either produce the exact same result as
|
|
75
|
+
the general path, or refuse to run.** It never approximates.
|
|
76
|
+
|
|
77
|
+
Express that as an explicit blacklist of everything the lane can't handle:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
function canUseFastLane(item) {
|
|
81
|
+
const cached = item._fastLane // 0 | 1 | undefined
|
|
82
|
+
if (cached === 1) return true
|
|
83
|
+
if (cached === 0) return false
|
|
84
|
+
|
|
85
|
+
const opts = item.options
|
|
86
|
+
const ok = !(
|
|
87
|
+
opts.beforeHook || opts.afterHook || opts.transform ||
|
|
88
|
+
opts.errorHandler || opts.customSerializer || opts.middleware
|
|
89
|
+
)
|
|
90
|
+
item._fastLane = ok ? 1 : 0 // memoize on the object, computed once
|
|
91
|
+
return ok
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Two things to copy:
|
|
96
|
+
- **Blacklist, not whitelist.** New features are automatically excluded from the fast lane
|
|
97
|
+
until someone deliberately handles them. A whitelist silently breaks on the next feature.
|
|
98
|
+
- **Memoize the gate itself** as a `0 | 1` integer field on the object. The gate runs on
|
|
99
|
+
every call; the predicate runs once per object. Using `0/1` instead of `true/false/undefined`
|
|
100
|
+
keeps the field a Smi and the compare monomorphic.
|
|
101
|
+
|
|
102
|
+
### 1.3 Tri-state returns: "hit", "miss", "not my job"
|
|
103
|
+
|
|
104
|
+
A specialized strategy needs to distinguish *"I definitively determined there's no result"*
|
|
105
|
+
from *"this input is outside my competence, ask someone else"*. Collapsing those forces the
|
|
106
|
+
caller to re-run the expensive path on every negative.
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
// undefined = ineligible, fall through to the general path
|
|
110
|
+
// null = definitively no result, stop
|
|
111
|
+
// value = hit
|
|
112
|
+
function trySimpleStrategy(input) {
|
|
113
|
+
if (hasComplexFeature(node)) return undefined // not my job
|
|
114
|
+
const child = node.children[key]
|
|
115
|
+
if (!child) return null // definitive miss
|
|
116
|
+
return buildResult(child)
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The same idea at a coarser grain, for lanes that may complete synchronously, go async
|
|
121
|
+
mid-flight, or bail out:
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
// true = completed synchronously
|
|
125
|
+
// Promise = went async mid-lane
|
|
126
|
+
// false = ineligible, caller should use the general path
|
|
127
|
+
function tryFastLane(input) { ... }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 1.4 Precompute the answer table for the enumerable cases
|
|
131
|
+
|
|
132
|
+
If the set of "simple" inputs is finite and derivable from config, compute all the answers
|
|
133
|
+
at setup and serve them from a dictionary:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
function buildExactTable(config) {
|
|
137
|
+
const table = Object.create(null)
|
|
138
|
+
for (const item of config.items) {
|
|
139
|
+
if (!isSimple(item.key)) continue // only inputs with no dynamic parts
|
|
140
|
+
if (needsRuntimeHook(item)) continue // guard: anything user-observable is excluded
|
|
141
|
+
table[item.key] = computeResult(config, item.key)
|
|
142
|
+
if (caseInsensitive) table[item.key.toLowerCase()] = table[item.key] // pre-fold aliases
|
|
143
|
+
}
|
|
144
|
+
return table
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
At call time this is one property load returning a **shared, frozen** result — no walk, no
|
|
149
|
+
allocation, no parameter object. This was the biggest single structural win in the codebase.
|
|
150
|
+
|
|
151
|
+
Note the guards. Anything that could invoke user code, mutate, or vary per call disqualifies
|
|
152
|
+
an entry from precomputation. Precompute only pure, config-derived answers.
|
|
153
|
+
|
|
154
|
+
### 1.5 Skip the whole operation when the state is already correct
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
function refresh(opts) {
|
|
158
|
+
if (!opts?.force && isAlreadySettled()) {
|
|
159
|
+
return RESOLVED // shared singleton, zero allocation, zero microtasks
|
|
160
|
+
}
|
|
161
|
+
return doTheWork(opts)
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The general path in the original always allocated a promise and burned at least one tick
|
|
166
|
+
even when everything was already up to date. Checking for "nothing to do" is often the
|
|
167
|
+
cheapest big win in a library, and it's usually missing because the code was written
|
|
168
|
+
async-first.
|
|
169
|
+
|
|
170
|
+
**Ethics note:** the reimplementation's benchmark harness *deliberately excludes* this row
|
|
171
|
+
from its published table, because "we can skip the call entirely" isn't a comparison of
|
|
172
|
+
equivalent work. Do the same. Optimize it, don't brag about it.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Tier 2 — Move work from call time to setup time
|
|
177
|
+
|
|
178
|
+
### 2.1 Compile patterns into op arrays
|
|
179
|
+
|
|
180
|
+
Instead of re-parsing a template string on every call, parse once into a flat array of ops
|
|
181
|
+
and cache it — **including the failure**, so complex templates aren't rescanned forever:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
const compiled = Object.create(null) // key -> ops[] | null (null = "not compilable")
|
|
185
|
+
|
|
186
|
+
function compile(template) {
|
|
187
|
+
const hit = compiled[template]
|
|
188
|
+
if (hit !== undefined) return hit // `undefined` = miss, `null` = negative cache hit
|
|
189
|
+
|
|
190
|
+
const ops = []
|
|
191
|
+
// ...scan once...
|
|
192
|
+
if (tooComplex) return (compiled[template] = null)
|
|
193
|
+
return (compiled[template] = ops)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function apply(template, values) {
|
|
197
|
+
const ops = compile(template)
|
|
198
|
+
if (!ops) return null // caller falls back to the general path
|
|
199
|
+
let out = ''
|
|
200
|
+
for (let i = 0; i < ops.length; i++) {
|
|
201
|
+
const op = ops[i]
|
|
202
|
+
out += op.t === 0 ? op.s : values[op.k]
|
|
203
|
+
}
|
|
204
|
+
return out
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Note `undefined` vs `null` doing double duty as miss-vs-negative-hit. Negative caching
|
|
209
|
+
matters: without it, every uncompilable input pays a full scan forever.
|
|
210
|
+
|
|
211
|
+
### 2.2 Specialize a closure for the common arity
|
|
212
|
+
|
|
213
|
+
One step further than op arrays — compile straight to a closure, with a hand-written
|
|
214
|
+
monomorphic case for the overwhelmingly common shape:
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
function compileToClosure(template, keys) {
|
|
218
|
+
if (keys.length === 1) { // the ~90% case
|
|
219
|
+
const key = keys[0]
|
|
220
|
+
const idx = template.indexOf('$' + key)
|
|
221
|
+
const pre = template.slice(0, idx)
|
|
222
|
+
const post = template.slice(idx + key.length + 1)
|
|
223
|
+
return (values) => pre + values[key] + post // one concat, zero scanning
|
|
224
|
+
}
|
|
225
|
+
const parts = buildParts(template)
|
|
226
|
+
return (values) => { /* generic loop */ }
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The one-param closure has no loop, no array indexing, no branch. V8 inlines it.
|
|
231
|
+
|
|
232
|
+
### 2.3 Precompute capability flags on the config object
|
|
233
|
+
|
|
234
|
+
Walk the whole configuration once at setup and hoist "does anything anywhere use feature X"
|
|
235
|
+
into booleans. Then gate entire subsystems at call time with one property load:
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
// setup
|
|
239
|
+
let hasValidation = false
|
|
240
|
+
let hasMiddleware = false
|
|
241
|
+
for (const id in registry) {
|
|
242
|
+
const o = registry[id].options
|
|
243
|
+
if (o?.middleware?.length) { hasMiddleware = true; hasValidation = true; break }
|
|
244
|
+
if (o?.validate) hasValidation = true
|
|
245
|
+
}
|
|
246
|
+
processed.hasValidation = hasValidation
|
|
247
|
+
processed.hasMiddleware = hasMiddleware
|
|
248
|
+
|
|
249
|
+
// call time
|
|
250
|
+
if (processed.hasValidation) { /* the entire validation pipeline */ }
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Caveat found in the wild:** one such flag was computed with a full recursive tree walk at
|
|
254
|
+
setup and then *never read on any hot path* — only by a unit test. Audit your precomputation
|
|
255
|
+
for dead work. Setup cost isn't free just because it's not in the hot loop.
|
|
256
|
+
|
|
257
|
+
### 2.4 Fold case/normalization into the key at build time
|
|
258
|
+
|
|
259
|
+
If lookups are case-insensitive, don't lowercase the input on every call and don't keep two
|
|
260
|
+
lookup tables. Normalize the *keys* once at setup and store pre-folded aliases. This is only
|
|
261
|
+
possible if you scope the feature at the container level rather than per-item — see §9.2,
|
|
262
|
+
where deleting a feature enabled a strictly better data structure.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Tier 3 — Cache at the right layer
|
|
267
|
+
|
|
268
|
+
### 3.1 The cache hierarchy, cheapest first
|
|
269
|
+
|
|
270
|
+
| Layer | Cost per hit | When to use |
|
|
271
|
+
|---|---|---|
|
|
272
|
+
| Module-level 1-slot identity memo | 2× `===` | The exact same call repeats back-to-back |
|
|
273
|
+
| Per-instance 1-slot memo | 1 field load + `===` | Same, but instances interleave |
|
|
274
|
+
| Null-prototype dictionary | 1 keyed load | Small bounded key space |
|
|
275
|
+
| `Map` | hash + call | Non-string keys, or you need real LRU |
|
|
276
|
+
| Linked-list LRU | hash + 3–6 pointer writes | Large key space, real eviction pressure |
|
|
277
|
+
|
|
278
|
+
A one-slot memo is *dramatically* cheaper than a `Map` and is often enough:
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
let lastA = '', lastB = '', lastResult = ''
|
|
282
|
+
function expensive(a, b) {
|
|
283
|
+
if (a === lastA && b === lastB) return lastResult
|
|
284
|
+
const result = compute(a, b)
|
|
285
|
+
lastA = a; lastB = b; lastResult = result
|
|
286
|
+
return result
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Initialize sentinels to a value **no real input can equal** (`'\0'`, not `''`) so the
|
|
291
|
+
comparison IC never sees a degenerate shape and the guard never accidentally hits.
|
|
292
|
+
|
|
293
|
+
### 3.2 Cache the whole downstream result, not the intermediate
|
|
294
|
+
|
|
295
|
+
The biggest cache win wasn't caching the core algorithm — it was caching the **entire
|
|
296
|
+
derived tuple** the caller actually wanted, returned **by reference**:
|
|
297
|
+
|
|
298
|
+
```js
|
|
299
|
+
function getDerived(key) {
|
|
300
|
+
const hit = cache[key]
|
|
301
|
+
if (hit) return hit // returns the SAME array/tuple, zero allocation
|
|
302
|
+
|
|
303
|
+
const base = coreAlgorithm(key)
|
|
304
|
+
const result = [buildChain(base), base.params, base.leaf]
|
|
305
|
+
cache[key] = result
|
|
306
|
+
return result
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
The original cached the core algorithm's output but then rebuilt the tuple, copied a params
|
|
311
|
+
object, and allocated an array on every call — so the cache saved the cheap part and paid
|
|
312
|
+
for the expensive part every time. **Profile where the allocations are, not where the
|
|
313
|
+
algorithm is.**
|
|
314
|
+
|
|
315
|
+
### 3.3 Template + clone for objects that are mostly identical
|
|
316
|
+
|
|
317
|
+
When results are expensive to construct but differ only in a few mutable fields, snapshot a
|
|
318
|
+
sanitized template and clone it:
|
|
319
|
+
|
|
320
|
+
```js
|
|
321
|
+
// on the way out: strip everything user-mutable / request-specific
|
|
322
|
+
function toTemplate(obj) {
|
|
323
|
+
return { ...obj, data: undefined, error: undefined, context: {}, controller: undefined }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// on the way in: preallocated array, spread the template, patch the volatile fields
|
|
327
|
+
function cloneTemplate(template) {
|
|
328
|
+
const now = Date.now()
|
|
329
|
+
const out = new Array(template.length)
|
|
330
|
+
for (let i = 0; i < template.length; i++) {
|
|
331
|
+
out[i] = { ...toTemplate(template[i]), updatedAt: now, isPending: false }
|
|
332
|
+
}
|
|
333
|
+
return out
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Gate this hard: the snapshot is only taken (and only used) when no feature that could make
|
|
338
|
+
two results differ is active. Spread-cloning also preserves key order, so the clones share a
|
|
339
|
+
hidden class with the source.
|
|
340
|
+
|
|
341
|
+
### 3.4 Returning the *same reference* is itself an optimization
|
|
342
|
+
|
|
343
|
+
Every transform function should return its input unchanged when there's nothing to do:
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
function normalize(str) {
|
|
347
|
+
const i = str.indexOf(BAD)
|
|
348
|
+
if (i === -1) return str // same reference — downstream === checks now hit
|
|
349
|
+
return doTransform(str, i)
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
This compounds. Downstream one-slot memos, `Object.is` bailouts, and change-detection all
|
|
354
|
+
key on identity. A function that returns a fresh equal string invalidates every cache above
|
|
355
|
+
it.
|
|
356
|
+
|
|
357
|
+
### 3.5 Cache hazards — every one of these was found in the shipped code
|
|
358
|
+
|
|
359
|
+
- **Identity-keyed memos on mutable inputs.** `if (obj === lastObj) return lastResult` is
|
|
360
|
+
wrong the moment someone mutates `obj` in place. It only works under a documented
|
|
361
|
+
immutability invariant. Write the invariant down.
|
|
362
|
+
- **FIFO masquerading as LRU.** A bounded dictionary that evicts the first-inserted key is
|
|
363
|
+
FIFO. A hot key inserted early gets evicted while cold keys survive. Fine at size 32,
|
|
364
|
+
pathological at scale.
|
|
365
|
+
- **Object insertion order as a recency list.** Exploiting JS string-key insertion order to
|
|
366
|
+
avoid a linked list works — until a key is integer-like (`"0"`, `"12"`), which JS orders
|
|
367
|
+
*before* all string keys, silently corrupting recency. Also, `delete` + re-insert on every
|
|
368
|
+
`get` pushes the object into dictionary mode.
|
|
369
|
+
- **`Object.keys(store)[0]` to find the oldest key.** Allocates the entire key array to read
|
|
370
|
+
element 0. Only on the eviction path, but it's a real allocation at exactly the wrong time.
|
|
371
|
+
- **Unbounded caches.** Two of the highest-value caches had no eviction at all. Keyed by
|
|
372
|
+
user-controlled input, that's a memory leak and a DoS vector.
|
|
373
|
+
- **Module-global caches shared across instances and across tests.** Fine for pure functions
|
|
374
|
+
of their key. Not fine for anything else.
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Tier 4 — Delete the async
|
|
379
|
+
|
|
380
|
+
An `async` function that never actually suspends still costs: a promise allocation, a
|
|
381
|
+
coroutine frame, and a microtask tick per `await`. Across a pipeline that's 15–25 promises
|
|
382
|
+
and 25–40 ticks for work that could be zero.
|
|
383
|
+
|
|
384
|
+
### 4.1 Type your hot functions as "sync or async"
|
|
385
|
+
|
|
386
|
+
```js
|
|
387
|
+
/** @returns {void | Promise<void>} */
|
|
388
|
+
function run(input) {
|
|
389
|
+
const result = step1(input)
|
|
390
|
+
if (result instanceof Promise) return result.then(step2)
|
|
391
|
+
return step2(result) // returns undefined — no promise at all
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Callers join with an explicit check rather than `await`. Yes, it's two paths. That's the
|
|
396
|
+
price.
|
|
397
|
+
|
|
398
|
+
### 4.2 A shared resolved singleton
|
|
399
|
+
|
|
400
|
+
```js
|
|
401
|
+
export const RESOLVED = Promise.resolve()
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Return it from every "nothing to do" path. Then callers can identity-check it and skip even
|
|
405
|
+
the thenable test:
|
|
406
|
+
|
|
407
|
+
```js
|
|
408
|
+
const out = run(input)
|
|
409
|
+
if (out !== RESOLVED && isThenable(out)) return out.then(next)
|
|
410
|
+
return next()
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### 4.3 `instanceof Promise` vs `isThenable` — pick deliberately
|
|
414
|
+
|
|
415
|
+
```js
|
|
416
|
+
// hot internal joins: monomorphic, no property load, no call
|
|
417
|
+
if (value instanceof Promise) { ... }
|
|
418
|
+
|
|
419
|
+
// API boundaries (user callbacks, cross-realm values): correct but costlier
|
|
420
|
+
const isThenable = (v) => v != null && typeof v.then === 'function'
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Use `instanceof` where you control both sides; use duck-typing where you don't.
|
|
424
|
+
|
|
425
|
+
### 4.4 Continuation-passing instead of `async`/`await`
|
|
426
|
+
|
|
427
|
+
Rewriting a 4-`await` pipeline as chained continuations with a sync join at each step means
|
|
428
|
+
a fully-synchronous request allocates **one** promise (the API's return type) instead of ten:
|
|
429
|
+
|
|
430
|
+
```js
|
|
431
|
+
function handle(request) {
|
|
432
|
+
const afterC = (c) => finish(c)
|
|
433
|
+
const afterB = (b) => {
|
|
434
|
+
const c = stepC(b)
|
|
435
|
+
return isThenable(c) ? c.then(afterC) : afterC(c)
|
|
436
|
+
}
|
|
437
|
+
const afterA = (a) => {
|
|
438
|
+
const b = stepB(a)
|
|
439
|
+
return isThenable(b) ? b.then(afterB) : afterB(b)
|
|
440
|
+
}
|
|
441
|
+
const a = stepA(request)
|
|
442
|
+
return isThenable(a) ? a.then(afterA) : afterA(a)
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Critically: `await maybeUndefined()` suspends for a tick **even when the value is
|
|
447
|
+
`undefined`**. Guard it: `if (v != null) return wrap(v).then(next); return next()`.
|
|
448
|
+
|
|
449
|
+
### 4.5 Resume-at-`i+1` so one async item doesn't infect the rest
|
|
450
|
+
|
|
451
|
+
When iterating work items where most are synchronous, don't `await` in the loop. Recurse
|
|
452
|
+
into the same function at the next index only when you actually hit a promise:
|
|
453
|
+
|
|
454
|
+
```js
|
|
455
|
+
function processFrom(items, i) {
|
|
456
|
+
for (; i < items.length; i++) {
|
|
457
|
+
const result = items[i].run()
|
|
458
|
+
if (result instanceof Promise) {
|
|
459
|
+
return result.then((v) => {
|
|
460
|
+
commit(items[i], v)
|
|
461
|
+
return processFrom(items, i + 1) // resume the sync loop
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
commit(items[i], result) // sync item: no promise, no tick
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### 4.6 `{ ok, value }` result objects instead of try/catch across await
|
|
470
|
+
|
|
471
|
+
```js
|
|
472
|
+
function callUser(fn, arg) {
|
|
473
|
+
try { return { ok: true, value: fn(arg) } }
|
|
474
|
+
catch (value) { return { ok: false, value } }
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Keeps the try/catch in a tiny leaf function that V8 can handle well, and lets the caller
|
|
479
|
+
stay non-async.
|
|
480
|
+
|
|
481
|
+
### 4.7 Prototype methods, not async arrow properties
|
|
482
|
+
|
|
483
|
+
```js
|
|
484
|
+
class Thing {
|
|
485
|
+
navigate = async (opts) => { ... } // ✗ closure allocated PER INSTANCE
|
|
486
|
+
}
|
|
487
|
+
class Thing {
|
|
488
|
+
navigate(opts) { ... } // ✓ one function object on the prototype
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
For a library that constructs many short-lived instances (one per request, say), the arrow
|
|
493
|
+
form allocates a fresh closure for every method on every instance. It also makes it harder
|
|
494
|
+
for V8 to reuse optimized code across instances. The reimplementation ships a diagnostic
|
|
495
|
+
script that asserts `freshInstance.method === Prototype.method` — a structural precondition
|
|
496
|
+
for its cold-start numbers.
|
|
497
|
+
|
|
498
|
+
### 4.8 Don't make things async that don't need to be
|
|
499
|
+
|
|
500
|
+
The original's blocker system made every mutation `async` so it *could* await blockers, even
|
|
501
|
+
though blockers were almost never registered. The fix:
|
|
502
|
+
|
|
503
|
+
```js
|
|
504
|
+
push(value, opts) {
|
|
505
|
+
const blockers = this.blockers
|
|
506
|
+
if (blockers?.length && !opts?.ignore) {
|
|
507
|
+
return this.runBlockersThen(value) // async only when blockers exist
|
|
508
|
+
}
|
|
509
|
+
// straight-line synchronous commit
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## Tier 5 — Delete the allocations
|
|
516
|
+
|
|
517
|
+
### 5.1 Never create garbage you then have to clean up
|
|
518
|
+
|
|
519
|
+
```js
|
|
520
|
+
// ✗ closure + filtered array + joined string that intentionally creates "//" + regex pass
|
|
521
|
+
paths.filter(v => v !== undefined).join('/').replace(/\/{2,}/g, '/')
|
|
522
|
+
|
|
523
|
+
// ✓ decide the separator at each boundary, produce the right string directly
|
|
524
|
+
let out = ''
|
|
525
|
+
for (let i = 0; i < paths.length; i++) {
|
|
526
|
+
const v = paths[i]
|
|
527
|
+
if (v === undefined) continue
|
|
528
|
+
const needsSep = out.length && out.charCodeAt(out.length - 1) !== SLASH && v.charCodeAt(0) !== SLASH
|
|
529
|
+
if (needsSep) out += '/'
|
|
530
|
+
out += v
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### 5.2 The `split`/`map`/`join` chain
|
|
535
|
+
|
|
536
|
+
```js
|
|
537
|
+
// ✗ 2 arrays + 1 closure
|
|
538
|
+
value.split('/').map(seg => encode(seg)).join('/')
|
|
539
|
+
|
|
540
|
+
// ✓ 1 array, mutated in place, no closure, no megamorphic callback dispatch
|
|
541
|
+
const parts = value.split('/')
|
|
542
|
+
for (let i = 0; i < parts.length; i++) parts[i] = encode(parts[i])
|
|
543
|
+
return parts.join('/')
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### 5.3 Preallocate with known length; never `push` in a sized loop
|
|
547
|
+
|
|
548
|
+
```js
|
|
549
|
+
const out = new Array(items.length)
|
|
550
|
+
for (let i = 0; i < items.length; i++) out[i] = transform(items[i])
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
`.map()` allocates a closure and goes through a generic callback dispatch. `push` in a loop
|
|
554
|
+
means repeated capacity growth.
|
|
555
|
+
|
|
556
|
+
### 5.4 Mutate in place when the branch is provably impossible
|
|
557
|
+
|
|
558
|
+
```js
|
|
559
|
+
const canBranch = node.dynamicChild || node.optionalChildren?.length || node.wildcard
|
|
560
|
+
const chain = canBranch ? frame.chain.slice() : frame.chain // share when nothing can fork
|
|
561
|
+
chain.push(node.value)
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Copy-on-write, but only actually copy when a write could be observed by another path.
|
|
565
|
+
|
|
566
|
+
### 5.5 Frozen shared singletons for empty values
|
|
567
|
+
|
|
568
|
+
```js
|
|
569
|
+
const EMPTY_OBJ = Object.freeze(Object.create(null))
|
|
570
|
+
const EMPTY_PARAMS = Object.freeze(Object.create(null))
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
Return these instead of a fresh `{}`. Every static result in the system then shares one
|
|
574
|
+
object — no per-call allocation. Freeze so an accidental mutation fails loudly instead of
|
|
575
|
+
corrupting every other caller.
|
|
576
|
+
|
|
577
|
+
**But:** one parity test in this codebase specifically asserts that two results do **not**
|
|
578
|
+
share an empty-object singleton, because user code writes to that field. Know which of your
|
|
579
|
+
"empty" values are read-only.
|
|
580
|
+
|
|
581
|
+
### 5.6 Noop stand-ins for expensive host objects
|
|
582
|
+
|
|
583
|
+
```js
|
|
584
|
+
const noopController = {
|
|
585
|
+
signal: {
|
|
586
|
+
aborted: false, reason: undefined,
|
|
587
|
+
throwIfAborted() {}, addEventListener() {}, removeEventListener() {},
|
|
588
|
+
dispatchEvent() { return false },
|
|
589
|
+
},
|
|
590
|
+
abort() {},
|
|
591
|
+
}
|
|
592
|
+
// ...
|
|
593
|
+
controller: needsCancellation ? new AbortController() : noopController
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
`new AbortController()` per work item, when 90% of items never abort, is pure waste.
|
|
597
|
+
|
|
598
|
+
### 5.7 Lazy allocation with `??=`
|
|
599
|
+
|
|
600
|
+
```js
|
|
601
|
+
let listeners // undefined until someone subscribes
|
|
602
|
+
subscribe(fn) { (listeners ??= new Set()).add(fn) }
|
|
603
|
+
notify() { listeners?.forEach(fn => fn()) }
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
The original eagerly allocated four listener arrays and a buffer object per request, then
|
|
607
|
+
called `.slice()` on each empty array on every emit. Lazy + `if (!listeners?.length) return`
|
|
608
|
+
removes all of it for the common case.
|
|
609
|
+
|
|
610
|
+
### 5.8 Don't allocate in the function signature
|
|
611
|
+
|
|
612
|
+
```js
|
|
613
|
+
// ✗ `...rest` builds a fresh object on EVERY call, just to read one field
|
|
614
|
+
function f({ a, b, ...rest }) { use(rest.c) }
|
|
615
|
+
|
|
616
|
+
// ✓ positional args, monomorphic call site
|
|
617
|
+
function f(a, b, c) { ... }
|
|
618
|
+
function fPublic({ a, b, c }) { return f(a, b, c) } // thin destructuring wrapper
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### 5.9 Null-prototype dictionaries instead of `Map` for string keys
|
|
622
|
+
|
|
623
|
+
```js
|
|
624
|
+
const store = Object.create(null)
|
|
625
|
+
store[key] = value // keyed-load IC, no call, no prototype hop
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Faster than `Map.get`/`Map.set` for string keys, and no `Map` entry overhead. Trade-offs:
|
|
629
|
+
no `.size` without bookkeeping, no iteration order guarantee for integer-like keys, and
|
|
630
|
+
`delete` pushes the object into dictionary mode. The codebase wraps this in a small
|
|
631
|
+
`StringMap` class to keep the `Map`-ish call sites unchanged.
|
|
632
|
+
|
|
633
|
+
### 5.10 Skip the whole call when the input is trivially handled
|
|
634
|
+
|
|
635
|
+
```js
|
|
636
|
+
// ✗ called unconditionally for every item
|
|
637
|
+
const { result, used } = interpolate(item.template, params)
|
|
638
|
+
|
|
639
|
+
// ✓ 90% of items have no placeholders at all
|
|
640
|
+
let result = item.template
|
|
641
|
+
if (item.template.indexOf('$') !== -1) {
|
|
642
|
+
({ result, used } = interpolate(item.template, params))
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
---
|
|
647
|
+
|
|
648
|
+
## Tier 6 — Primitive-level string and scan work
|
|
649
|
+
|
|
650
|
+
These are 1.2–2.7× each and they compound, but more importantly they make the tier-1 guard
|
|
651
|
+
checks cheap enough to always run.
|
|
652
|
+
|
|
653
|
+
### 6.1 `charCodeAt` instead of regex / `startsWith` / indexing
|
|
654
|
+
|
|
655
|
+
```js
|
|
656
|
+
s.startsWith('/') → s.charCodeAt(0) === 47
|
|
657
|
+
s[0] === '?' → s.charCodeAt(0) === 63
|
|
658
|
+
s.endsWith('/') → s.charCodeAt(s.length - 1) === 47
|
|
659
|
+
/^\/{1,}/.test(s) → charCode loop
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
`s[0]` allocates a one-character string. `startsWith`/`endsWith` are method dispatches.
|
|
663
|
+
Regex entry has fixed setup cost per call.
|
|
664
|
+
|
|
665
|
+
Nearly every regex in the original had a hand-written scanner in the reimplementation:
|
|
666
|
+
slash collapsing, trimming, character-class validation, HTML escaping, percent-decoding,
|
|
667
|
+
whitespace detection, JSON sniffing.
|
|
668
|
+
|
|
669
|
+
### 6.2 The scan-first / slice-run-append idiom
|
|
670
|
+
|
|
671
|
+
The universal shape for "transform a string, cheaply, usually a no-op":
|
|
672
|
+
|
|
673
|
+
```js
|
|
674
|
+
function transform(src) {
|
|
675
|
+
let out = ''
|
|
676
|
+
let last = 0
|
|
677
|
+
for (let i = 0; i < src.length; i++) {
|
|
678
|
+
if (src.charCodeAt(i) !== TARGET) continue
|
|
679
|
+
out += src.slice(last, i) + REPLACEMENT
|
|
680
|
+
last = i + 1
|
|
681
|
+
}
|
|
682
|
+
return last === 0 ? src : out + src.slice(last) // ← same reference when unchanged
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
Copies runs in bulk (rope concat), never char-by-char, and returns the original reference
|
|
687
|
+
when there's nothing to do.
|
|
688
|
+
|
|
689
|
+
### 6.3 Tiered encoding: no-op / single-pass / full
|
|
690
|
+
|
|
691
|
+
```js
|
|
692
|
+
function encode(str) {
|
|
693
|
+
let needsSoftEscape = false
|
|
694
|
+
for (let i = 0; i < str.length; i++) {
|
|
695
|
+
const c = str.charCodeAt(i)
|
|
696
|
+
if (isUnreserved(c)) continue
|
|
697
|
+
if (c === SPACE) { needsSoftEscape = true; continue }
|
|
698
|
+
return fullEncode(str) // tier 3: rare
|
|
699
|
+
}
|
|
700
|
+
if (!needsSoftEscape) return str // tier 1: same reference, zero work
|
|
701
|
+
return replaceCode(str, SPACE, '+') // tier 2: one pass
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
For the dominant input class (already-safe strings) this does N integer compares and returns
|
|
706
|
+
the input. `encodeURIComponent` is a C++ call that always builds a new string.
|
|
707
|
+
|
|
708
|
+
Apply the same idea to decode:
|
|
709
|
+
|
|
710
|
+
```js
|
|
711
|
+
function decode(str) {
|
|
712
|
+
const plus = str.indexOf('+'), pct = str.indexOf('%')
|
|
713
|
+
if (plus === -1 && pct === -1) return str // no work at all
|
|
714
|
+
if (pct === -1) return replaceCode(str, PLUS, ' ') // no decodeURIComponent, no try/catch
|
|
715
|
+
try { return decodeURIComponent(prepped) } catch { return prepped }
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### 6.4 First-character dispatch before full string comparison
|
|
720
|
+
|
|
721
|
+
```js
|
|
722
|
+
// ✗ two full string compares + three ToNumber coercions for a plain string
|
|
723
|
+
if (s === 'false') return false
|
|
724
|
+
if (s === 'true') return true
|
|
725
|
+
return +s * 0 === 0 && +s + '' === s ? +s : s
|
|
726
|
+
|
|
727
|
+
// ✓ one charCodeAt for the dominant case
|
|
728
|
+
const c = s.charCodeAt(0)
|
|
729
|
+
if (c === 116 && s === 'true') return true
|
|
730
|
+
if (c === 102 && s === 'false') return false
|
|
731
|
+
if (c === 45 || (c >= 48 && c <= 57)) { const n = +s; return n * 0 === 0 && n + '' === s ? n : s }
|
|
732
|
+
return s
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
### 6.5 `| 32` for case-insensitive ASCII compare
|
|
736
|
+
|
|
737
|
+
```js
|
|
738
|
+
const a = s.charCodeAt(i + 1) | 32 // folds A-Z to a-z
|
|
739
|
+
const b = s.charCodeAt(i + 2) | 32
|
|
740
|
+
if (a === 50 && b === 53) { /* matched "25" case-insensitively */ }
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
Avoids `toLowerCase()` (which allocates) and case-insensitive regex.
|
|
744
|
+
|
|
745
|
+
### 6.6 Conditional normalization
|
|
746
|
+
|
|
747
|
+
```js
|
|
748
|
+
let needsFold = false
|
|
749
|
+
for (let i = 0; i < key.length; i++) {
|
|
750
|
+
const c = key.charCodeAt(i)
|
|
751
|
+
if (c >= 65 && c <= 90) { needsFold = true; break }
|
|
752
|
+
}
|
|
753
|
+
if (needsFold) key = key.toLowerCase() // otherwise keep the original reference
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
### 6.7 Avoid host objects on hot paths
|
|
757
|
+
|
|
758
|
+
`URLSearchParams` for query parsing costs: a C++-side parsed list, an iterator object per
|
|
759
|
+
loop, a two-element array per pair, plus destructuring. A hand-rolled `indexOf`/`slice`
|
|
760
|
+
scanner beat it 1.2–1.6× on both encode and decode.
|
|
761
|
+
|
|
762
|
+
Same category: prefer `URL.parse()` (returns `null`) over `new URL()` + `try/catch`. The
|
|
763
|
+
exception path for relative inputs was on the hot path of every link construction.
|
|
764
|
+
|
|
765
|
+
### 6.8 Avoid exceptions as control flow at high frequency
|
|
766
|
+
|
|
767
|
+
```js
|
|
768
|
+
// ✗ throws on every relative input
|
|
769
|
+
try { new URL(href); isAbsolute = true } catch {}
|
|
770
|
+
|
|
771
|
+
// ✓
|
|
772
|
+
isAbsolute = URL.canParse(href)
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
And guard even that with a cheap pre-check (`href.charCodeAt(0) === 47` → definitely
|
|
776
|
+
relative, skip the call).
|
|
777
|
+
|
|
778
|
+
### 6.9 Put `try/catch` in leaf functions
|
|
779
|
+
|
|
780
|
+
A `try` block in a function containing a hot loop is worse than a `try` in a tiny leaf the
|
|
781
|
+
loop calls. The original nested a `try/catch` *inside a `replaceAll` callback* — a closure
|
|
782
|
+
plus a handler frame per match.
|
|
783
|
+
|
|
784
|
+
### 6.10 Cheap monotonic IDs
|
|
785
|
+
|
|
786
|
+
```js
|
|
787
|
+
// ✗ RNG + float→string + substring, per call
|
|
788
|
+
(Math.random() + 1).toString(36).substring(7)
|
|
789
|
+
|
|
790
|
+
// ✓
|
|
791
|
+
let seq = 0
|
|
792
|
+
const nextKey = () => (++seq).toString(36)
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
Only valid if the ID doesn't need to be unguessable or globally unique. Check first.
|
|
796
|
+
|
|
797
|
+
---
|
|
798
|
+
|
|
799
|
+
## Tier 7 — Engine-level concerns (V8)
|
|
800
|
+
|
|
801
|
+
### 7.1 Hidden-class discipline
|
|
802
|
+
|
|
803
|
+
**Initialize every field in the constructor, in a fixed order.** Adding fields afterwards
|
|
804
|
+
creates divergent transition chains and turns every read site polymorphic.
|
|
805
|
+
|
|
806
|
+
```js
|
|
807
|
+
// ✓ one shape, always
|
|
808
|
+
function createNode() {
|
|
809
|
+
return { a: null, b: null, c: null, prefix: '', suffix: '', priority: 0 }
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// ✗ three shapes: base(4), base+prefix+suffix(6), base+parse+priority(6, different chain)
|
|
813
|
+
function createNode() { return { a: null, b: null, c: null, d: null } }
|
|
814
|
+
// ...later: node.prefix = x; node.suffix = y
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
The original was explicit about this, with a comment on its factory: *"Keys must be declared
|
|
818
|
+
in the same order as in the type, to ensure they are represented as the same object class in
|
|
819
|
+
the engine."* The reimplementation regressed here and ended up with 3+ node shapes.
|
|
820
|
+
|
|
821
|
+
**Keep key order identical across all return branches** of a function that produces one
|
|
822
|
+
logical type. Two `return { a, b, c }` / `return { b, a, c }` sites produce two hidden
|
|
823
|
+
classes for the same nominal type.
|
|
824
|
+
|
|
825
|
+
### 7.2 Tagged single shape beats a discriminated union of shapes
|
|
826
|
+
|
|
827
|
+
```js
|
|
828
|
+
// ✗ two maps at the load site
|
|
829
|
+
type Op = { t: 0, s: string } | { t: 1, k: string }
|
|
830
|
+
|
|
831
|
+
// ✓ one map; the unused field is a wasted word, which is cheaper than a polymorphic IC
|
|
832
|
+
type Op = { t: number, s: string, k: string }
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
### 7.3 Don't put accessors on hot objects
|
|
836
|
+
|
|
837
|
+
`Object.defineProperty(obj, 'x', { get })` converts a data property into an accessor and
|
|
838
|
+
poisons the load site. Lazy computation is worth it for genuinely cold, expensive things —
|
|
839
|
+
but install the getter on a *wrapper* object, never on the one in the hot loop.
|
|
840
|
+
|
|
841
|
+
### 7.4 Sentinels that don't degenerate
|
|
842
|
+
|
|
843
|
+
```js
|
|
844
|
+
const MISS = '\0' // not '', not undefined
|
|
845
|
+
let hotKey = MISS
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
Comment from the source: *"starts as a value no real input can equal, so the compare never
|
|
849
|
+
runs against the empty sentinel (that deopts compare ICs)."*
|
|
850
|
+
|
|
851
|
+
### 7.5 Avoid `delete` on hot objects
|
|
852
|
+
|
|
853
|
+
`delete` transitions an object to dictionary mode permanently. Acceptable in a cache's
|
|
854
|
+
eviction path; not acceptable on a `get`.
|
|
855
|
+
|
|
856
|
+
### 7.6 Verify, don't assume
|
|
857
|
+
|
|
858
|
+
The codebase ships diagnostic scripts using `--allow-natives-syntax` to print
|
|
859
|
+
`%GetOptimizationStatus` for ~20 named hot functions, run the exact benchmark loops, then
|
|
860
|
+
force-optimize (`%PrepareFunctionForOptimization` → run → `%OptimizeFunctionOnNextCall` →
|
|
861
|
+
re-status) to distinguish "not hot enough" from "not optimizable". Bit flags on Node 24:
|
|
862
|
+
|
|
863
|
+
```
|
|
864
|
+
1 fn | 2 never-opt | 8 maybe-deopted | 16 optimized | 32 maglev | 64 turbofan
|
|
865
|
+
128 interpreted | 32768 baseline
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
If a hot function reports `never-opt`, no amount of micro-optimization will help; find out
|
|
869
|
+
why (usually: a deopt loop, `eval`, `with`, or a shape explosion).
|
|
870
|
+
|
|
871
|
+
---
|
|
872
|
+
|
|
873
|
+
## Tier 8 — Shrink the graph, not just the code
|
|
874
|
+
|
|
875
|
+
Bundle size is a *graph reachability* problem, not a minification problem.
|
|
876
|
+
|
|
877
|
+
### 8.1 Keep leaf modules leaf
|
|
878
|
+
|
|
879
|
+
The original's path utilities imported a helper from a 1300-line module that itself imported
|
|
880
|
+
three more. Every consumer of the small utility dragged in the whole matcher. Extracting the
|
|
881
|
+
helper into a 112-line leaf module fixed it.
|
|
882
|
+
|
|
883
|
+
**Rule:** if module X is imported by many things, X may only import other leaves.
|
|
884
|
+
|
|
885
|
+
### 8.2 Split cold paths into modules and `import()` them
|
|
886
|
+
|
|
887
|
+
```js
|
|
888
|
+
let cachedImpl
|
|
889
|
+
function coldPath(arg) {
|
|
890
|
+
if (cachedImpl) return cachedImpl(arg) // memoize: only the FIRST call pays
|
|
891
|
+
return import('./cold-impl').then(({ impl }) => {
|
|
892
|
+
cachedImpl = impl
|
|
893
|
+
return impl(arg)
|
|
894
|
+
})
|
|
895
|
+
}
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
Server-only code, HMR code, and rarely-used subsystems each became separate modules reached
|
|
899
|
+
only by dynamic import. Guard dev-only code so it strips:
|
|
900
|
+
|
|
901
|
+
```js
|
|
902
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
903
|
+
Impl.prototype._devOnlyThing = async function () {
|
|
904
|
+
const { helper } = await import('./dev-helper')
|
|
905
|
+
...
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
### 8.3 Conditional exports vs runtime `import()` — a real trade-off
|
|
911
|
+
|
|
912
|
+
Two strategies for keeping server code out of client bundles:
|
|
913
|
+
|
|
914
|
+
**Conditional exports** (the original): map `"./env"` to `env/client.js` under the `browser`
|
|
915
|
+
condition, where `export const isServer = false`. Bundlers fold `if (isServer)` to a
|
|
916
|
+
constant and delete the block. Powerful — it makes *every* server branch in the codebase
|
|
917
|
+
disappear. Cost: an enormous exports map (that library had ~33 conditional entries with
|
|
918
|
+
`browser`/`node`/`worker`/`workerd`/`deno`/`bun`/`development` variants), and it only works
|
|
919
|
+
if the bundler honours your conditions.
|
|
920
|
+
|
|
921
|
+
**Runtime boolean + dynamic import** (the reimplementation): `export const isServer =
|
|
922
|
+
typeof document === 'undefined' ? true : undefined`. Simple, one module, works everywhere —
|
|
923
|
+
but `if (isServer ?? ...)` is a runtime check and **no server block is ever dropped**. This
|
|
924
|
+
is a direct contributor to the reimplementation *losing* on bundle size (1.21× larger gzip)
|
|
925
|
+
despite winning everywhere else.
|
|
926
|
+
|
|
927
|
+
If bundle size matters more than build simplicity, take the conditional-exports route.
|
|
928
|
+
|
|
929
|
+
### 8.4 A trap worth knowing
|
|
930
|
+
|
|
931
|
+
The reimplementation's own source comment:
|
|
932
|
+
|
|
933
|
+
> *"Boolean-only on purpose: re-exporting server loaders from here pulled the SSR graph into
|
|
934
|
+
> every `utils` import and blocked dead-code elimination."*
|
|
935
|
+
|
|
936
|
+
A widely-imported module that re-exports a heavy implementation defeats tree-shaking for
|
|
937
|
+
everyone. Environment flags should be **values only**, never re-export hubs.
|
|
938
|
+
|
|
939
|
+
### 8.5 Types-only subpath exports
|
|
940
|
+
|
|
941
|
+
Map a public subpath to a `.d.ts`-equivalent module containing only `import type`:
|
|
942
|
+
|
|
943
|
+
```json
|
|
944
|
+
"./serializer/transformer": "./src/serializer/transformer-types.ts"
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
Consumers get the types; the heavy runtime dependency behind it never enters the graph.
|
|
948
|
+
|
|
949
|
+
### 8.6 Delete dependencies
|
|
950
|
+
|
|
951
|
+
Each runtime dep is a subgraph you don't control. The reimplementation dropped:
|
|
952
|
+
- an external reactive-store library (~50 lines of hand-rolled code replaced it)
|
|
953
|
+
- a user-agent classification library (by deleting the feature that needed it — §9.3)
|
|
954
|
+
- and inlined a tiny assertion helper
|
|
955
|
+
|
|
956
|
+
The corresponding package went from 4 runtime deps to 2.
|
|
957
|
+
|
|
958
|
+
### 8.7 Assert DCE structurally, not numerically
|
|
959
|
+
|
|
960
|
+
Bundle-size numbers drift. Assert *what must not be reachable*:
|
|
961
|
+
|
|
962
|
+
```js
|
|
963
|
+
const serverMarkers = ['loadServerRoute', 'createRequestHandler', 'crossSerializeStream']
|
|
964
|
+
|
|
965
|
+
it('drops the server graph when only a leaf util is imported', async () => {
|
|
966
|
+
const { entry } = await bundle(`import { util } from 'lib'; console.log(util({}))`)
|
|
967
|
+
expect(entry).toContain('util')
|
|
968
|
+
expect(entry).not.toContain('CoreClass')
|
|
969
|
+
expect(serverMarkers.filter(m => entry.includes(m))).toEqual([])
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
it('keeps the cold path in an async chunk, not the entry', async () => {
|
|
973
|
+
const { entry, chunks } = await bundle(`import { createThing } from 'lib'; ...`)
|
|
974
|
+
expect(serverMarkers.filter(m => entry.includes(m))).toEqual([])
|
|
975
|
+
const asyncCode = Object.entries(chunks).filter(([n]) => n !== 'entry.js').map(([, c]) => c).join('\n')
|
|
976
|
+
expect(asyncCode).toContain('loadServerRoute') // it must exist — just not here
|
|
977
|
+
})
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
This is a *test*, run in CI, that fails the moment someone adds a static import that
|
|
981
|
+
reconnects the graph. It's the single most valuable bundle-size safeguard, because it
|
|
982
|
+
catches the regression at the cause rather than at the symptom.
|
|
983
|
+
|
|
984
|
+
### 8.8 Measure the initial graph, not the entry chunk
|
|
985
|
+
|
|
986
|
+
Walk `chunk.imports` transitively from every entry chunk and sum min+gzip. Deliberately do
|
|
987
|
+
**not** follow `dynamicImports` — that's the point of splitting them out. Disclose that in
|
|
988
|
+
your published numbers.
|
|
989
|
+
|
|
990
|
+
---
|
|
991
|
+
|
|
992
|
+
## Tier 9 — Narrow the support matrix (this is an optimization)
|
|
993
|
+
|
|
994
|
+
### 9.1 Modern runtime targets delete compatibility code
|
|
995
|
+
|
|
996
|
+
Pinning to a recent Node and a single major of the peer framework unlocked, and the
|
|
997
|
+
reimplementation actually uses:
|
|
998
|
+
|
|
999
|
+
| API | Replaces |
|
|
1000
|
+
|---|---|
|
|
1001
|
+
| `URL.canParse()` / `URL.parse()` | `new URL()` in `try/catch` |
|
|
1002
|
+
| `Object.hasOwn()` | `Object.prototype.hasOwnProperty.call()` |
|
|
1003
|
+
| `Error.isError()` | `instanceof Error` (cross-realm-correct) |
|
|
1004
|
+
| `String.prototype.toWellFormed()` | manual lone-surrogate handling / `try/catch` around `encodeURIComponent` |
|
|
1005
|
+
|
|
1006
|
+
Usage count: 19 call sites across those four APIs in the reimplementation, **zero** in the
|
|
1007
|
+
original — which still supports older runtimes. The original also maintains a
|
|
1008
|
+
multi-TypeScript-version type-test matrix (5 TS versions), which the reimplementation drops
|
|
1009
|
+
entirely.
|
|
1010
|
+
|
|
1011
|
+
Config-level consequences: `target: ES2024` / `lib: ES2024` vs `target: ES2020` /
|
|
1012
|
+
`lib: ES2022`. Higher targets mean the compiler stops downlevelling async/await, classes,
|
|
1013
|
+
optional chaining, and spread into slower ES5 shims.
|
|
1014
|
+
|
|
1015
|
+
### 9.2 Deleting a feature can unlock a better data structure
|
|
1016
|
+
|
|
1017
|
+
The original supported per-item case sensitivity, which forced it to maintain **two** lookup
|
|
1018
|
+
maps and probe both on every step. The reimplementation scoped case sensitivity to the
|
|
1019
|
+
container instead, which allowed **one** dictionary with pre-folded keys — halving the
|
|
1020
|
+
lookups and removing a `Map`.
|
|
1021
|
+
|
|
1022
|
+
That's not a micro-optimization. That's a feature-scope decision that changed the algorithm.
|
|
1023
|
+
Look for these: a rarely-used per-item option is often what's blocking your best data
|
|
1024
|
+
structure.
|
|
1025
|
+
|
|
1026
|
+
### 9.3 Deliberately changing behaviour, and documenting it
|
|
1027
|
+
|
|
1028
|
+
The original inspected a request header with a third-party classification library and, for
|
|
1029
|
+
one class of client, buffered the entire output instead of streaming it. The reimplementation
|
|
1030
|
+
deletes the branch: everything streams, always. That removed a dependency, a header parse,
|
|
1031
|
+
and a conditional from every request.
|
|
1032
|
+
|
|
1033
|
+
This is a **behaviour change**, not an optimization. It's defensible only because it's stated
|
|
1034
|
+
plainly in the README with the manual workaround. If you take this route, be that explicit.
|
|
1035
|
+
|
|
1036
|
+
### 9.4 Lint config as a performance policy
|
|
1037
|
+
|
|
1038
|
+
The reimplementation's linter runs 242 rules with a `"perf": "warn"` category, and — more
|
|
1039
|
+
interestingly — **disables** rules that fight performance:
|
|
1040
|
+
|
|
1041
|
+
```jsonc
|
|
1042
|
+
"unicorn/prefer-string-replace-all": "off", // manual scan beats replaceAll
|
|
1043
|
+
"unicorn/no-new-array": "off", // new Array(n) preallocation is wanted
|
|
1044
|
+
"unicorn/no-array-for-each": "off",
|
|
1045
|
+
"typescript/prefer-for-of": "off" // indexed for loops are faster
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
Your style guide encodes performance decisions whether you intend it to or not. Make the
|
|
1049
|
+
exemptions explicit and commented.
|
|
1050
|
+
|
|
1051
|
+
---
|
|
1052
|
+
|
|
1053
|
+
## Tier 10 — The measurement discipline
|
|
1054
|
+
|
|
1055
|
+
This is the most transferable section. Without it, tiers 1–4 are not engineering.
|
|
1056
|
+
|
|
1057
|
+
### 10.1 Gate the benchmark on equal work
|
|
1058
|
+
|
|
1059
|
+
**Before printing a single number**, prove both implementations do the same amount of work.
|
|
1060
|
+
Instrument the user-facing callback and count invocations across a fixed script:
|
|
1061
|
+
|
|
1062
|
+
```js
|
|
1063
|
+
async function countWork(createSubject) {
|
|
1064
|
+
const counter = { calls: 0 }
|
|
1065
|
+
const subject = createSubject(counter)
|
|
1066
|
+
|
|
1067
|
+
for (let i = 0; i < 100; i++) await subject.run(destinations[i % 5])
|
|
1068
|
+
const warmA = counter.calls
|
|
1069
|
+
|
|
1070
|
+
for (let i = 0; i < 100; i++) await subject.run({ id: String(i % 50) })
|
|
1071
|
+
const warmB = counter.calls - warmA // warm subject, varied input
|
|
1072
|
+
|
|
1073
|
+
const fresh = { calls: 0 }
|
|
1074
|
+
const freshSubject = createSubject(fresh)
|
|
1075
|
+
for (let i = 0; i < 100; i++) await freshSubject.run({ id: String(i % 50) })
|
|
1076
|
+
|
|
1077
|
+
return { warmA, warmB, cold: fresh.calls }
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const mine = await countWork(makeMine)
|
|
1081
|
+
const theirs = await countWork(makeTheirs)
|
|
1082
|
+
if (mine.warmA !== theirs.warmA || mine.warmB !== theirs.warmB || mine.cold !== theirs.cold) {
|
|
1083
|
+
throw new Error(`Work parity failed: ${JSON.stringify({ mine, theirs })}`)
|
|
1084
|
+
}
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
Three counters — warm/repeated, warm/varied, and cold — because each catches a different
|
|
1088
|
+
class of accidental cheating. Run it **in the parent process before spawning any timing
|
|
1089
|
+
run**, and print the verified counts in the report header so the table carries its own
|
|
1090
|
+
provenance.
|
|
1091
|
+
|
|
1092
|
+
Ship it as a standalone command too (`audit:work`), exiting nonzero on divergence.
|
|
1093
|
+
|
|
1094
|
+
### 10.2 Pin the same numbers as an offline unit test
|
|
1095
|
+
|
|
1096
|
+
```js
|
|
1097
|
+
expect({ warmA, warmB, cold }).toEqual({ warmA: 40, warmB: 100, cold: 100 })
|
|
1098
|
+
// and the contrast case that proves the caching claim isn't vacuous:
|
|
1099
|
+
expect(callsWithCachingEnabled).toBe(2)
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
The `40/100/100` vs `2` contrast proves the library is neither over-caching (inflating
|
|
1103
|
+
benchmarks) nor ignoring the caching option (making the feature a lie). This runs in CI with
|
|
1104
|
+
no network and no competitor installed.
|
|
1105
|
+
|
|
1106
|
+
### 10.3 Rotate inputs to defeat *your own* caches
|
|
1107
|
+
|
|
1108
|
+
If you ship intern caches, a benchmark that reuses one input measures your `Map.get`. Build
|
|
1109
|
+
fixture pools and advance a cursor on **both** sides:
|
|
1110
|
+
|
|
1111
|
+
```js
|
|
1112
|
+
const samples = [/* 6 distinct */]
|
|
1113
|
+
let cursor = 0
|
|
1114
|
+
measure(() => mine(samples[cursor++ % samples.length]))
|
|
1115
|
+
measure(() => theirs(samples[cursor++ % samples.length]))
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
For structured inputs, generate a cross-product (e.g. 64 distinct keys) and index with a
|
|
1119
|
+
power-of-two mask: `needles[cursor++ & 63]`.
|
|
1120
|
+
|
|
1121
|
+
**Audit this honestly.** One row in the published table (a 61× win) rotated 100 inputs
|
|
1122
|
+
against a 256-entry cache — so after the first iteration it was measuring a dictionary hit,
|
|
1123
|
+
not the algorithm. The rotation must exceed the cache size.
|
|
1124
|
+
|
|
1125
|
+
### 10.4 Warm up inside the same closure you'll time
|
|
1126
|
+
|
|
1127
|
+
```js
|
|
1128
|
+
function measure(fn, ms = 1500) {
|
|
1129
|
+
const warmupEnd = performance.now() + 200
|
|
1130
|
+
while (performance.now() < warmupEnd) fn() // untimed: let V8 tier up + ICs stabilize
|
|
1131
|
+
|
|
1132
|
+
let ops = 0
|
|
1133
|
+
const start = performance.now()
|
|
1134
|
+
const end = start + ms
|
|
1135
|
+
while (performance.now() < end) { fn(); ops++ }
|
|
1136
|
+
return ops / ((performance.now() - start) / 1000) // re-read elapsed, don't assume `ms`
|
|
1137
|
+
}
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
Duration-based throughput, not fixed-N latency: slow implementations aren't punished by
|
|
1141
|
+
wall-clock blowup and fast ones get enough samples.
|
|
1142
|
+
|
|
1143
|
+
### 10.5 Isolate sections in separate processes
|
|
1144
|
+
|
|
1145
|
+
Re-exec the harness per section, keyed by an env var, and pass results back as one JSON
|
|
1146
|
+
line on stdout:
|
|
1147
|
+
|
|
1148
|
+
```js
|
|
1149
|
+
function runSection(name) {
|
|
1150
|
+
const r = spawnSync(process.execPath, [...process.execArgv, ...process.argv.slice(1)],
|
|
1151
|
+
{ env: { ...process.env, BENCH_SECTION: name }, encoding: 'utf8' })
|
|
1152
|
+
const line = r.stdout.split('\n').find(l => l.startsWith('BENCH_JSON:'))
|
|
1153
|
+
if (!line) { process.stderr.write(r.stdout); throw new Error(`section ${name} produced no JSON`) }
|
|
1154
|
+
return JSON.parse(line.slice('BENCH_JSON:'.length))
|
|
1155
|
+
}
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
Preserving `process.execArgv` keeps your `--expose-gc` etc. Heap state, megamorphic IC
|
|
1159
|
+
pollution, and code-cache contents from section A can't contaminate section B. Call
|
|
1160
|
+
`globalThis.gc?.()` at the top of each section.
|
|
1161
|
+
|
|
1162
|
+
### 10.6 Compare against the *published artifact*, not local source
|
|
1163
|
+
|
|
1164
|
+
Import the competitor from real `node_modules` (pinned versions, printed in the report
|
|
1165
|
+
header), and deep-import its `dist/` file directly when a symbol isn't in its export map:
|
|
1166
|
+
|
|
1167
|
+
```js
|
|
1168
|
+
import { x as mine } from '../packages/core/src/x.ts'
|
|
1169
|
+
import { x as theirs } from '../node_modules/competitor/dist/esm/x.js'
|
|
1170
|
+
```
|
|
1171
|
+
|
|
1172
|
+
Run this under plain `node`, **not** your test runner, so your test-time aliases (which
|
|
1173
|
+
redirect the competitor's package name to your own source) aren't in effect.
|
|
1174
|
+
|
|
1175
|
+
Be aware of the asymmetry this creates: if you ship raw TS and they ship built ESM, you're
|
|
1176
|
+
comparing different compilation pipelines. Disclose it.
|
|
1177
|
+
|
|
1178
|
+
### 10.7 Prove all candidates compute the same function *before* timing them
|
|
1179
|
+
|
|
1180
|
+
Any shootout between N implementations should assert equivalence at module load:
|
|
1181
|
+
|
|
1182
|
+
```js
|
|
1183
|
+
for (const input of fixtures) {
|
|
1184
|
+
expect(decode(encode(input))).toBe(input)
|
|
1185
|
+
}
|
|
1186
|
+
// and for a multi-way shootout:
|
|
1187
|
+
if (implA(x) !== implB(x)) throw new Error('Implementation mismatch!')
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
A bench file that throws is infinitely better than a bench file that reports a fast wrong
|
|
1191
|
+
answer.
|
|
1192
|
+
|
|
1193
|
+
### 10.8 Defeat DCE of the benchmarked expression
|
|
1194
|
+
|
|
1195
|
+
```js
|
|
1196
|
+
let sink = 0
|
|
1197
|
+
function batch(input) {
|
|
1198
|
+
let size = 0
|
|
1199
|
+
for (let i = 0; i < N; i++) size += operation(input).length
|
|
1200
|
+
sink = size
|
|
1201
|
+
}
|
|
1202
|
+
// at module scope:
|
|
1203
|
+
void sink
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
Consume the result into a module-level binding that is then referenced. Neither the bundler
|
|
1207
|
+
nor V8 can prove the computation dead.
|
|
1208
|
+
|
|
1209
|
+
### 10.9 Publish your losing metric
|
|
1210
|
+
|
|
1211
|
+
The reimplementation's README reports its bundle size as **1.21× larger** than the
|
|
1212
|
+
competitor, with an explanation of why and a pointer to re-run the measurement. It also
|
|
1213
|
+
segregates rows that aren't strictly equal work into their own table, and **omits entirely**
|
|
1214
|
+
a row where its implementation is allowed to skip the call.
|
|
1215
|
+
|
|
1216
|
+
A benchmark table that only contains wins is marketing. One that contains losses,
|
|
1217
|
+
methodology, and exclusions is evidence.
|
|
1218
|
+
|
|
1219
|
+
### 10.10 Be honest about statistical rigour
|
|
1220
|
+
|
|
1221
|
+
The harness reports a single ops/s scalar per side — no stddev, no RME, no median-of-N, no
|
|
1222
|
+
A/B interleaving, and it always runs "mine first". That's a real weakness. If you're
|
|
1223
|
+
publishing ratios that people will make decisions on, add interleaving and dispersion.
|
|
1224
|
+
At minimum, fence the environment in the report ("4-core X, Linux, Node 24, in-memory") and
|
|
1225
|
+
explicitly forbid cross-environment comparison of your own numbers.
|
|
1226
|
+
|
|
1227
|
+
---
|
|
1228
|
+
|
|
1229
|
+
## Tier 11 — Testing a fast-path architecture
|
|
1230
|
+
|
|
1231
|
+
Once you have two paths, **the parity test is the most valuable test in the repo.**
|
|
1232
|
+
|
|
1233
|
+
### 11.1 Why the eligibility gate is not enough
|
|
1234
|
+
|
|
1235
|
+
The gate is a *negative* proof: "no exotic feature is present." It says nothing about
|
|
1236
|
+
whether the fast lane produces the same results, the same call sequence, or the same error
|
|
1237
|
+
semantics for the plain inputs it *does* accept.
|
|
1238
|
+
|
|
1239
|
+
### 11.2 What a fast-path parity suite must assert
|
|
1240
|
+
|
|
1241
|
+
From the real suite, generalized. Each of these is a bug class the fast lane invites:
|
|
1242
|
+
|
|
1243
|
+
1. **Derived keys recompute.** Navigate with input A, then input B; assert the second result
|
|
1244
|
+
reflects B and that the cache key includes the varying dimension.
|
|
1245
|
+
→ *catches: fast-lane cache key omits a dimension.*
|
|
1246
|
+
2. **Per-item mutable state is not shared.** `expect(a.ctx).not.toBe(b.ctx)` and
|
|
1247
|
+
`expect(Object.isFrozen(a.ctx)).toBe(false)`.
|
|
1248
|
+
→ *catches: the most tempting fast-path allocation saving.*
|
|
1249
|
+
3. **Synchronous user-code throw.** Assert: called exactly once, error handler fired exactly
|
|
1250
|
+
once with the *identical* error instance, the public promise **resolves** (doesn't
|
|
1251
|
+
reject), and the item is committed in an error state.
|
|
1252
|
+
→ *catches: sync execution double-invoking, or leaking a throw through the public API.*
|
|
1253
|
+
4. **Async rejection.** Same contract as (3) for the promise path.
|
|
1254
|
+
5. **Identity fields survive object reuse.** Pin every field user code or devtools reads:
|
|
1255
|
+
index, id, parent linkage, static metadata, flags.
|
|
1256
|
+
→ *catches: pooled/cloned objects losing fields.*
|
|
1257
|
+
6. **Defaults survive cache reuse.** A→B→A, and the *cached* A must still carry its
|
|
1258
|
+
defaulted/validated values.
|
|
1259
|
+
→ *catches: the template snapshot stripping too much.*
|
|
1260
|
+
7. **Empty-value singletons are not shared where they're written.**
|
|
1261
|
+
`expect(a.field).not.toBe(b.field)` for fields user code mutates.
|
|
1262
|
+
→ *catches: over-eager `EMPTY_OBJ` reuse (§5.5).*
|
|
1263
|
+
8. **Mid-sequence abort unwinds correctly.** When item 1 of 3 fails, assert items 2–3 were
|
|
1264
|
+
never invoked **and** that item 1's in-flight flags were cleared.
|
|
1265
|
+
→ *catches: the `resume-at-i+1` pattern (§4.5) leaving `isPending: true` forever. A
|
|
1266
|
+
throughput benchmark can never surface this — the UI just hangs.*
|
|
1267
|
+
|
|
1268
|
+
### 11.3 Run the upstream suite against your implementation, unmodified
|
|
1269
|
+
|
|
1270
|
+
If you're reimplementing a known API, don't write your own compatibility tests — **vendor
|
|
1271
|
+
theirs and alias the module specifiers**:
|
|
1272
|
+
|
|
1273
|
+
```js
|
|
1274
|
+
resolve: {
|
|
1275
|
+
alias: {
|
|
1276
|
+
'competitor-core': resolve(root, 'packages/core/src/index.ts'),
|
|
1277
|
+
'competitor-ui': resolve(root, 'packages/ui/src/index.ts'),
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
Add a resolver for subpath naming differences (their `camelCase` subpaths → your
|
|
1283
|
+
`kebab-case` files), so vendored tests import *verbatim*:
|
|
1284
|
+
|
|
1285
|
+
```js
|
|
1286
|
+
function resolveSubpath(baseDir, rest) {
|
|
1287
|
+
const kebab = rest.split('/').map(s => s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()).join('/')
|
|
1288
|
+
for (const c of [`${baseDir}/${kebab}.ts`, `${baseDir}/${kebab}/index.ts`, `${baseDir}/${rest}.ts`]) {
|
|
1289
|
+
if (existsSync(c)) return c
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
The reimplementation runs **163 vendored runtime specs + 26 vendored type-test files**,
|
|
1295
|
+
including ~25 regression tests named after upstream issue numbers. It keeps first-party and
|
|
1296
|
+
vendored suites in **separate configs with mutually exclusive `include`/`exclude`** so the
|
|
1297
|
+
two classes of evidence never blur.
|
|
1298
|
+
|
|
1299
|
+
It also aliases the competitor's own *third-party dependency* to its hand-rolled replacement,
|
|
1300
|
+
so vendored tests exercise the replacement rather than the original library.
|
|
1301
|
+
|
|
1302
|
+
### 11.4 Also vendor their benchmarks
|
|
1303
|
+
|
|
1304
|
+
Running the competitor's own micro-benchmarks against your implementation prevents the
|
|
1305
|
+
failure mode of only inventing self-favourable benchmarks.
|
|
1306
|
+
|
|
1307
|
+
### 11.5 Batch heavy suites into isolated processes
|
|
1308
|
+
|
|
1309
|
+
```js
|
|
1310
|
+
spawnSync('vitest', ['run', '--pool=forks', '--maxWorkers=1', ...batch],
|
|
1311
|
+
{ stdio: 'inherit', env: { ...process.env, NODE_OPTIONS: '--max-old-space-size=4096' } })
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
Full isolation, serial, deterministic; any nonzero status aborts.
|
|
1315
|
+
|
|
1316
|
+
### 11.6 CI gates
|
|
1317
|
+
|
|
1318
|
+
Format → lint → unused-code detection (with config hints as errors) → unit tests (including
|
|
1319
|
+
the DCE and parity suites) → type tests → each vendored suite as its own step. Four parallel
|
|
1320
|
+
jobs, everything on the pinned runtime.
|
|
1321
|
+
|
|
1322
|
+
---
|
|
1323
|
+
|
|
1324
|
+
## Tier 12 — Risk register: what you actually buy the speed with
|
|
1325
|
+
|
|
1326
|
+
Every technique above has a failure mode. These were all found in the shipped code — they
|
|
1327
|
+
are the price, not hypotheticals.
|
|
1328
|
+
|
|
1329
|
+
**Correctness / semantics**
|
|
1330
|
+
- Hand-rolled encoders diverge from spec-compliant host implementations on edge cases
|
|
1331
|
+
(reserved-character sets, malformed input, exponent notation). Round-trips still work;
|
|
1332
|
+
produced strings, cache keys, and cross-boundary comparisons differ.
|
|
1333
|
+
- Fast paths that skip normalization produce different output for malformed-but-accepted
|
|
1334
|
+
inputs.
|
|
1335
|
+
- Identity-keyed memos on mutable objects return stale results under in-place mutation.
|
|
1336
|
+
|
|
1337
|
+
**Architecture**
|
|
1338
|
+
- Replacing a fine-grained dependency-tracking system with a single broadcast channel makes
|
|
1339
|
+
each notification cheaper but wakes **every** subscriber, each running its selector. This
|
|
1340
|
+
can be a net loss at scale, and it was here: a single-field toggle went from one targeted
|
|
1341
|
+
write to a full snapshot copy + O(n) re-map + global broadcast.
|
|
1342
|
+
- Removing a computed/memoized derivation layer means derived values recompute and reallocate
|
|
1343
|
+
on **every** read. The codebase has comments working around exactly this
|
|
1344
|
+
(*"prefer the stable snapshot; `.get()` maps a new array on every call"*).
|
|
1345
|
+
- Precomputation that nothing reads is pure setup cost.
|
|
1346
|
+
- Fast lanes that allocate more than the general path on their *slow* branch are a bet on the
|
|
1347
|
+
fast branch always winning. Verify the bet.
|
|
1348
|
+
|
|
1349
|
+
**The one that matters most**
|
|
1350
|
+
|
|
1351
|
+
> The headline 16×/14×/30× rows measure a fast lane gated on `subscribers.size === 0`. In the
|
|
1352
|
+
> real deployment configuration, the library always installs a subscriber — so **the lane
|
|
1353
|
+
> never engages in production.** The benchmark ran in an environment where it did.
|
|
1354
|
+
|
|
1355
|
+
Before publishing a number, assert that the fast lane you're measuring is actually taken
|
|
1356
|
+
under the configuration your users run. Add a test that fails if the gate closes in the
|
|
1357
|
+
default setup. This is the single most important lesson in this document.
|
|
1358
|
+
|
|
1359
|
+
---
|
|
1360
|
+
|
|
1361
|
+
## Appendix — Order of operations
|
|
1362
|
+
|
|
1363
|
+
1. **Build the harness first.** Equal-work gate, rotating inputs, process isolation,
|
|
1364
|
+
duration-based timing, comparison against the published artifact.
|
|
1365
|
+
2. **Profile allocations, not just time.** The biggest wins came from removing allocations
|
|
1366
|
+
the profiler attributed to functions that "looked cheap."
|
|
1367
|
+
3. **Find the dominant input class.** What do 90% of calls actually look like? That's your
|
|
1368
|
+
fast lane's target.
|
|
1369
|
+
4. **Add tier-1 fast lanes with blacklist gates and full fallback.** Write the parity suite
|
|
1370
|
+
in the same commit.
|
|
1371
|
+
5. **Move work to setup time.** Compile patterns, precompute tables, hoist capability flags.
|
|
1372
|
+
6. **Add caches from the cheapest layer up.** One-slot memo → dictionary → LRU. Cache the
|
|
1373
|
+
whole downstream result. Bound every cache.
|
|
1374
|
+
7. **Delete the async** on paths that usually don't suspend.
|
|
1375
|
+
8. **Delete the allocations.**
|
|
1376
|
+
9. **Micro-optimize the primitives** — now they're also the guard checks for step 4.
|
|
1377
|
+
10. **Verify optimization tiers** with `%GetOptimizationStatus`. Fix shape explosions.
|
|
1378
|
+
11. **Shrink the graph.** Split cold modules, dynamic-import them, assert with DCE tests.
|
|
1379
|
+
12. **Re-run the equal-work gate.** Then publish, including your losses and exclusions.
|