V8/Node.js performance patterns for hot paths, parsers, and core libraries in JavaScript/TypeScript. Use when writing or reviewing performance-sensitive JS/TS code.
Installation
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
V8/Node.js performance patterns for hot paths, parsers, and core libraries in JavaScript/TypeScript. Use when writing or reviewing performance-sensitive JS/TS code.
Core principles
Keep V8 on the fast path:
Consistent shapes โ same properties, same order, never delete
Stable types โ don't mix parameter types across calls
Minimize allocations โ every {}, [], spread is GC work
Simple operations first โ manual scanning over regex, for over iterators
Optimization workflow
Profile before changing code โ identify whether the bottleneck is CPU, I/O, allocation/GC, startup/module graph, or algorithmic complexity
Benchmark the smallest representative workload โ warm up, consume results so dead-code elimination cannot remove the work, and compare baseline/change in the same run to avoid drift
Use the right Node/V8 diagnostic for the question โ node --prof/--prof-process for CPU, --trace-deopt for deoptimizations, --trace-turbo-inlining for inlining, --log-ic for inline-cache state, heap snapshots or GC traces for memory
Treat every rule below as a hypothesis โ keep the change only when profiler output, benchmark output, or simpler asymptotic behavior pays for the added complexity in the target workload
Object shapes & inline caches
Initialize objects with the same properties in the same order โ V8 assigns hidden classes (shapes); consistent shapes keep functions monomorphic (fastest), 5+ shapes = megamorphic (hash-table fallback)
Never conditionally add properties โ always set the property (to undefined if needed) to keep shapes consistent
Avoid delete obj.prop โ mutates hidden class, forces dictionary mode; set to undefined instead
Watch for megamorphism in shared utility functions โ a helper called with many different object shapes across the codebase goes megamorphic; split into shape-specific fast paths or use a type-tag switch to recover monomorphic performance
Prefer integer/numeric enums over string enums โ integer comparison is O(1) by value, strings are O(n) character-by-character; small integers are stored as Smi (no heap allocation)
Strings & regex
Avoid .split() for simple parsing โ scan with indexOf/slice instead (one pass, no intermediate array allocation)
Don't hand-roll char-by-char splitting to avoid .split() โ native .split() is a C++ builtin and beats a JS accumulation loop (cur += s[i]); even an indexOf/slice loop that still builds the array loses to it. Only skip .split() when you can extract what you need in a single indexOf/slice pass with no array materialization ("".split(sep) returns [""] not [], so guard if you need [])
Use .includes() or .startsWith() before regex โ avoids regex engine for the common non-matching case
Prefer String.fromCharCode() over String.fromCodePoint() for BMP characters (< 0x10000) โ faster path, and characters outside BMP are rare
Use charCodeAt() for character classification in hot loops โ numeric comparison is faster than string operations
Accumulate ranges, then .substring() once โ avoid character-by-character string concatenation
Pre-compute character code constants โ avoid runtime charCodeAt() on string literals in hot paths
Hoist invariant string building out of hot-loop comparisons โ arr.find(d => path.startsWith(${d}/)) rebuilds ${d}/ every iteration; precompute the concatenated forms once and compare against the pre-built strings (applies to any +/template in a loop that doesn't vary with the iteration)
Prefer manual character scanning (indexOf, charCodeAt, loops) over regex for simple patterns โ regex has engine overhead (backtracking, state machines) that manual parsing avoids
Cache compiled regexes outside loops โ dynamic new RegExp() in a loop recompiles every iteration
Reuse a single Intl.Collator instance over repeated localeCompare() calls
Collections
Prefer Map over plain objects for dynamic key-value collections โ large Maps are faster for insertion, key lookup and iteration; use objects only for static/known-shape data
Keep arrays homogeneous โ all integers = PACKED_SMI (fastest), adding a float transitions to PACKED_DOUBLE, adding a string transitions to PACKED_ELEMENTS (slowest); transitions are one-way
Prefer array literals over new Array(n) โ pre-sized arrays start holey and can stay on slower paths; when you need a pre-sized dense array, initialize it immediately with .fill(value)
Don't read out-of-bounds โ forces V8 prototype chain walk
Use Set.has() over Array.includes() for repeated lookups โ O(1) vs O(n)
Don't double-look up a Map โ if (map.has(k)) map.get(k).add(v) hashes the key twice (has and get each run the lookup); take the value once with const inner = map.get(k); if (inner) inner.add(v) (or map.get(k)?.add(v)). Caveat: the single-lookup form treats a missing key as falsy, so only use it when undefined/falsy isn't a valid stored value
Cache repeated lookups over static data โ a linear scan (.find(), .filter()) called per item in an outer loop becomes O(n ร m); memoize when the underlying data doesn't change between calls
Use binary search on sorted arrays instead of .findIndex() โ O(log n) vs O(n)
Prefer TypedArrays for large numeric data โ contiguous memory enables CPU prefetching
Functions & control flow
A function created per hot-loop iteration only costs when it escapes โ stored, registered, or passed where its identity is observed. V8 often inlines or elides the rest (immediately-invoked callbacks, args to builtins like .map/.then, object-property callbacks), so don't flag a non-escaping callback as a cost without evidence it's retained. For merge/compose fan-out, prefer an array of handlers + a plain iteration loop over nested merged = (x) => { prev(x); next(x) } wrappers applied N times: the nested chain gets progressively slower as it deepens, while the array loop stays flat. A one-off 2-way compose is fine (it inlines to two direct calls); don't build deeper wrapper chains expecting a speedup
Push, don't poll, on growing shared collections โ when a producer mutates a Set/Array the consumer needs to react to, fire a callback on each add; polling "did the size grow? then re-iterate" is O(NรM) because each consumer pass re-walks the full collection from the start
Deduplicate repeated registrations in multi-tenant loops โ when the same handler/callback is registered once per iteration (e.g. per workspace, per route, per config entry), identical work multiplies; track what's already registered and skip duplicates
Keep hot functions small โ V8's inlining budgets change by version and code shape; extract cold/error paths when it keeps the hot path simple enough to inline
Prefer for/while over .forEach() in measured tight loops โ callback overhead and inlining limits can matter, but don't flag non-escaping callbacks without profiler or benchmark evidence
Avoid the arguments object โ use rest params (...args); even referencing arguments inhibits optimization
Match function arity at call sites โ mismatched arity creates arguments adaptor frames
Generators have inherent overhead โ provide array-returning alternative for callers that need all results; use generators only when lazy evaluation is needed
A generator's cost is the per-element yield suspend/resume itself โ V8 inlines surrounding identity wrappers, nested closures, and dead constant-guarded branches (if (DEBUG) โฆ) to ~0, so stripping that wrapping for speed does nothing. To remove the cost, return an array instead of yielding (trades a one-time allocation for no per-element suspension). Eliminate the generator, not the wrapping
Don't use try/catch for expected control flow โ use APIs that return null/undefined (e.g. fs.statSync(file, { throwIfNoEntry: false })) because Error objects capture stack traces, which is expensive
Keep try blocks small โ V8 optimizes code outside try blocks more aggressively
Avoid Proxy in hot paths โ V8 falls back from JIT to interpreter
Allocation & GC pressure
Minimize object allocations in hot paths โ every {}, [], new is GC work
Avoid {...spread} for copying objects in hot paths โ allocates + copies all properties; mutate or use a dedicated clone function; structuredClone is even worse โ for shallow copies, Object.assign/spread is dramatically faster than structuredClone
Cache deep property chains in local vars โ const x = obj.a.b.c avoids repeated pointer dereferences
Short-circuit common cases to avoid allocations โ e.g. return single element directly instead of .join() on a one-element array
Reuse objects with reset()/copyFrom() instead of allocating new ones โ swap references instead of creating
Measure before reusing allocations โ object pooling/reuse adds complexity; V8 handles short-lived same-shape objects efficiently via young-generation GC, so per-call new Set() may be cheaper than maintaining reusable state
Memoize allocators whose output is deterministic from their input โ a function returning a fresh Set/array/object built from its args (e.g. getDependencies(name) { return new Set([...a, ...b]) }) reallocates an identical result on every call; cache by input when the same inputs recur. Caveats: the cached value is now shared, so callers must treat it as immutable, and it only pays off when inputs actually repeat
Defer expensive work with lazy getters โ use null sentinel to distinguish "not computed" from "no value", parse on first access only
Never use an unbounded Map/object as a cache โ it's a memory leak; use lru-cache with max + ttl to bound growth, or WeakMap when keys are objects with independent lifetimes
Loops & state machines
Use numeric state variables โ integer comparison is cheaper than string/object state
Use index-based while loops for tight scanning โ faster than iterators
Pre-compute lookup tables (Map/object) for classification โ trade O(1) build time for O(1) lookups in hot paths
Pack multiple boolean flags into a Uint8Array lookup table โ use bitwise & 1, & 2 to test individual flags from a single byte
Cache indexOf results and search forward from last position โ avoid rescanning from the start
Split fast path / slow path โ check for the common simple case first and return early, only fall through to complex parsing when needed
Use character lookahead before committing to state changes โ peek at next char(s) to decide the operation without advancing position
Async
Avoid unnecessary async/await โ async boundaries are not free, especially in hot loops; don't await non-promise values, don't wrap already-async functions in redundant async wrappers, and measure before contorting readable code
Cap concurrency on Promise.all over dynamic-length arrays โ unbounded parallel I/O exhausts memory, file descriptors, and connection pools; use p-limit or p-map with an explicit concurrency limit