Loncat ke konten

Notable V8 changes from Node.js 26 to 27 (Alpha)

Joyee Cheung

The Node.js 27 Alpha release upgrades V8 to version 15.6, from the 14.6 used by Node.js 26, bringing several new JavaScript language features and improvements. In this post, let's explore the notable V8 changes introduced in this release that are interesting for Node.js developers.

Before we dive in, keep in mind that after Node.js switches to a new release schedule with an Alpha phase, Node.js may introduce breaking V8 changes, such as a bulk V8 upgrade, during the Alpha and Current phases. Breakages that affect the native addon ABI will come with a NODE_MODULE_VERSION bump, allowing toolchains that select prebuilt native addons by ABI version to continue working. Once the 27 release enters LTS, the V8 version in this release line is expected to remain stable, and no ABI or API breaking changes are expected except in exceptional circumstances, such as critical security fixes.

JavaScript language features

New language features available by default

Math.sumPrecise(iterable)

Compared to summing up numbers in a loop, Math.sumPrecise reduces the accumulation of floating-point errors.

[0.1, 0.2, 0.3].((, ) =>  + ); // 0.6000000000000001
.sumPrecise([0.1, 0.2, 0.3]); // 0.6

Intl.Locale.prototype.variants

The new Intl.Locale.prototype.variants getter provides access to the variants of a locale.

Iterator methods

Several new iterator methods have been added in this release:

[1, 2, 3]
  .()
  .( =>  * 2)
  .join('-'); // '2-4-6'
new (['a', 'b']).().includes('b'); // true
.zip([
  [1, 2],
  ['a', 'b'],
]).toArray(); // [[1, 'a'], [2, 'b']]
.zipKeyed({ : [1, 2], : ['a', 'b'] }).toArray();
// [{ id: 1, name: 'a' }, { id: 2, name: 'b' }]

Experimental language features behind a flag

Some fixes have been made to these experimental language features that were already available in v26:

  • --js-defer-import-eval: implements import defer. The static version already works in Node.js, while support for dynamic import.defer() is being implemented in node#66727.
  • --js-immutable-arraybuffer: implements Immutable ArrayBuffer.

Changes to Math

The new version of Node.js uses LLVM's libm for some Math methods, which leads to slightly different results for some inputs compared to the previous implementation. Code that compares floating point results to hard-coded values from an older version of Node.js may be broken by a difference in the last bit.

.(18);
// v26: -0.7509872467716762
// v27: -0.750987246771676
.(3);
// v26: 1.0986122886681096
// v27: 1.0986122886681098

The sequence of values returned by Math.random() now changes for a given --random-seed.

// node --random-seed=42
.();
// v26: 0.6159659788862542
// v27: 0.22876995452731475

Changes to Date

The value returned by Date.prototype.getTimezoneOffset() is no longer truncated to whole minutes. For historical dates when the time zone used a local mean time offset that is not a whole number of minutes (typically before the zone adopted standard time), the returned value can contain a fraction.

// TZ=America/Los_Angeles node
const  = new (.(1675, 0, 1));
.();
// v26: 472
// v27: 472.96666666666664
const  = new (.() + .() * 60000);
.();
// v26: '12/31/1674, 11:59:02 PM'
// v27: '1/1/1675, 12:00:00 AM'

New sorting algorithm for arrays

V8 now uses PowerSort instead of TimSort in Array.prototype.sort() for bigger arrays. The sort is still stable and the result from using a consistent comparator remains the same, but tests that record comparator calls or comparators with side effects can now behave differently.

const  = .({ : 2000 }, (, ) => ( * 7919) % 1009);
let  = 0;
.((, ) => {
  ++;
  return  - ;
});
;
// v26: 19220
// v27: 19249

Changes to error.stack access

The error.stack getter no longer looks up the captured stack trace from the prototype chain. Errors created with new, error subclasses and objects passed to Error.captureStackTrace are not affected.

const  = new ('boom');
typeof .().stack; // No longer looks up the prototype chain
// v26: 'string'
// v27: 'undefined'

Changes to Promises and async/await

When an ES module with top-level await is evaluated, V8 no longer calls Promise.prototype.then() in a way that looks up constructor and Symbol.species on the promise, as the specification requires.

When the callback passed to Promise.try returns a promise whose constructor is Promise in the same realm, Promise.try now returns that promise directly, see ecma262#3883.

const  = .(1);
.(() => ) === ;
// v26: false
// v27: true

Changes to Atomics

Calling Atomics.wait() on a worker thread that is already blocked by Atomics.wait() now throws a TypeError instead of crashing the process. This usually only happens when a debugger evaluates code on a worker thread blocked by Atomics.wait().

Atomics.notify(), Atomics.wait() and Atomics.waitAsync() now read the length of the typed array before coercing the index, as the specification requires. If the coercion leads to a growth of the buffer, an index that is originally out of bounds still results in RangeError.

const  = new (0, { : 4 });
const  = new ();
.(, {
  () {
    .(4);
    return 0;
  },
});
// v26: 0
// v27: RangeError: Invalid atomic access index

Changes to Iterator helpers

Passing a finite limit above 2^53-1 to take() and drop() leads to RangeError and closes the iterator. An Infinity limit is still valid.

[1, 2]
  .()
  .(2 ** 53)
  .();
// v26: [1, 2]
// v27: RangeError: 9007199254740992 must be positive

AsyncIterator.prototype[Symbol.asyncDispose] now calls return() with no arguments, as the explicit resource management proposal specifies.

const  = {
  () {
    return .({ : true });
  },
  (...) {
    .(`return() is called with ${.} argument(s)`);
    return .({ : true });
  },
};
const  = .(
  .(.((async function* () {})()))
);
await [.].call();
// v26: return() is called with 1 argument(s)
// v27: return() is called with 0 argument(s)

Changes to Modules

new import(...).x now correctly leads to SyntaxError, like new import(...):

new ('new import("x").x');
// v26: parses
// v27: SyntaxError: Cannot use new with import

A few edge cases (1, 2) that used to result in crashes during module loading now reject with appropriate errors instead.

Changes to Regular expressions

Negative lookahead and lookbehind assertions no longer match inside surrogate pairs in /u and /v mode.

[...'😀😀'.(/(?!.)/gv)].( => .);
// v26: [1, 3, 4]
// v27: [4]
[...'😀😀'.(/(?<!.)/gu)].( => .);
// v26: [0, 3]
// v27: [0]

Changes to Typed arrays

TypedArray.prototype.subarray(), when called on a length-tracking typed array subclass over a resizable ArrayBuffer without an end argument, now only passes the buffer and the byte offset to the subclass constructor, and omits the third undefined length argument, as the specification requires.

const  = [];
class  extends  {
  constructor(...) {
    .(.);
    super(...);
  }
}
const  = new (4, { : 8 });
new ().(1);
[1]; // arguments passed to the constructor of the subarray
// v26: 3
// v27: 2

Float16Array.prototype.indexOf(), lastIndexOf() and includes() now support negative values.

const  = new ([-1.5, 2]);
.(-1.5);
// v26: false
// v27: true

TypedArray.prototype.set() with a negative offset on an immutable buffer now throws TypeError before RangeError.

// node --js-immutable-arraybuffer
const  = new (new (4).transferToImmutable());
.([1], -1);
// v26: RangeError: offset is out of bounds
// v27: TypeError: Cannot perform %TypedArray%.prototype.set on a detached or out-of-bounds or immutable ArrayBuffer

Changes to Syntax and semantics

await and return inside static class initialization blocks after instance members now correctly lead to SyntaxError.

// SyntaxError in both versions
class A { static { return; } }

class B { x = 1; static { return; } }
// v26: parses
// v27: SyntaxError

using in a C-style for loop initializer is now disposed once and treated as immutable, according to the explicit resource management proposal.

let  = 0;
let  = 0;
for (
  using  = {
    [.]() {
      ++;
    },
  };
   < 3;
  ++
) {}
;
// v26: 5
// v27: 1

Escaped get and set as class field names followed by a newline now get automatic semicolon insertion. See test262#5134.

class C {
  g\u0065t
  a = 1;
}
Object.keys(new C());
// v26: SyntaxError: Keyword must not contain escaped characters
// v27: ['get', 'a']

delete super[key] now checks the this binding before evaluating key.

class  extends  {
  constructor() {
    delete super[(.('key evaluated'), 'k')]; // before super()
    super();
  }
}
new ();
// v26: prints "key evaluated", then throws ReferenceError
// v27: throws ReferenceError without evaluating the key

new Function(...) now consults the code generation policy before converting arguments.

// node --disallow-code-generation-from-strings
const  = {
  () {
    .('toString called');
    return 'x';
  },
};
new (, 'return x');
// v26: prints "toString called", then throws EvalError
// v27: throws EvalError without calling toString

Fatal error handling

In Node.js 27, more fatal V8 errors are now handled by Node.js's fatal error handling before aborting, so --report-on-fatalerror can produce a diagnostic report in cases where Node.js would previously abort without one.

Some extreme use of language features such as overly long template literals, classes with too many elements, Object.defineProperties with too many properties, a regexp that is too large to compile etc. now throw RangeError instead of crashing the process. This is not guaranteed and should not be relied upon for program logic, however.

V8 serialization changes

v8.serialize(), structuredClone() and postMessage now use a new serialization format. Older Node.js releases cannot deserialize data serialized by this version and will fail with an "invalid or unsupported version" error. This version can still deserialize data serialized by older releases. The new format supports typed arrays and resizable ArrayBuffers larger than 4 GB in structuredClone() and postMessage(). v8.serialize() currently still retains the 4 GB limit.

The v8.cachedDataVersionTag() now changes whenever Node.js applies its own patches on top of V8, not only when the V8 version changes. In addition, the built-in module compilation cache and cached data from vm.Script/vm.Module can be invalidated more often across Node.js releases. This improves safety, since a patched V8 can produce different compiled code, but it does introduce more cache misses across Node.js patch and minor releases.

  • The default old-generation size limit is now larger on machines with 4 GB-15 GB of RAM. When the heap size limit is not specified, the default maximum old-generation size is half of physical memory/cgroup memory limit clamped between 256 MB and 4 GB. Previously this was half of physical memory capped at 2 GB, or if the machine has at least 15 GB of RAM it's capped at 4 GB. On an 8 GB machine, the default old-generation limit is now 4 GB instead of 2 GB, and on a 6 GB machine it's now 3 GB instead of 2 GB. Applications that relied on the old default to restrict the amount of memory used should set --max-old-space-size explicitly.
  • Memory held by Buffers and ArrayBuffers is now counted in V8's global allocation limit. This changes when garbage collection gets scheduled for applications that allocate a lot of buffers outside the JavaScript heap.
  • Unused bytecode is now flushed based on time instead of garbage collection count. The bytecode of a function used to be dropped after it had not run for several major garbage collections. It can now be dropped during major garbage collection after it has not run for about 3 minutes, and is recompiled on the next call. This can change the memory usage pattern of applications with a lot of rarely used code.
  • Each native object now uses more memory: each JavaScript object that wraps a native resource, such as a socket, file handle, stream, hash or blob, may use about 16 bytes more on the JavaScript heap and about 50 bytes more outside the heap.
  • Each isolate now uses more memory for property lookup caches. In the official Node.js release, each isolate uses about 180 KB more memory. This increases the memory footprint for applications that create many worker threads.
  • The garbage collection pauses are now shorter in some worst cases, reduced from ~45 ms to ~10 ms in V8's benchmarks.
  • Creating an Error can now use less memory because the stack frame details are only computed when error.stack is read.
  • There can now be fewer full garbage collections at startup. The default initial old-generation allocation limit is now 32 MB, which may delay the first full GC, depending on the workload and memory pressure.

V8 performance changes

The performance impact described here can vary depending on platform, workload and build configurations. The numbers referenced below are only relevant to the benchmarks mentioned.

Array

Strings

Object property access

Async/promises

RegExp

JSON

Math and BigInt

Modules and scripts

Maglev

WebAssembly changes

WebAssembly removals and deprecations

  • asm.js is no longer compiled to WebAssembly. This means asm.js modules can still run as ordinary JavaScript, but code that depended on asm.js optimizations may be slower, and users will no longer see the "Converted asm.js to WebAssembly" messages.
  • WebAssembly.Function behind --experimental-wasm-type-reflection from the type reflection proposal was removed.

WebAssembly flag changes

  • --experimental-wasm-<name> flags are now renamed --wasm-<name>. The old names are no longer accepted.
  • --wasm-lazy-validation was removed. Invalid functions are now always reported when a module is compiled and this can no longer be disabled.
  • --wasm-async-compilation was removed, which means there's no longer a --no-wasm-async-compilation that makes the asynchronous compilation API synchronous.
  • --experimental-wasm-exnref, --experimental-wasm-branch-hinting and --experimental-wasm-rab-integration have been removed, as the features they enabled are now always on.

WebAssembly optimizations

  • The memory.copy and memory.fill instructions, which are typically used in compiled code for memcpy and memset, are now inlined in optimized code instead of calling into the runtime, resulting in a ~12% speedup on a bulk memory benchmark.
  • Calling exported functions that take or return structs, arrays and other reference types is about twice as fast on a microbenchmark.
  • new WebAssembly.Table({ element, initial }, value) now fills the table faster, and an invalid value now throws before the table is allocated.
  • On arm64, more optimizations for Wasm SIMD code are now enabled by default. They were previously behind --wasm-simd-opt. On x64 these optimizations are not available.

V8 diagnostics and tooling changes

Heap snapshots

Heap snapshots now include more detailed information about the JavaScript heap. Taking a snapshot can take about twice as long and the file is about twice as large.

  • Heap snapshots now include internal objects and numeric values. The exposeInternals and exposeNumericValues options of v8.writeHeapSnapshot() and v8.getHeapSnapshot() are ignored and considered always true. This may also increase the size of the snapshots taken.
  • Heap snapshots now contain the full source of scripts instead of only the first 1024 characters. They also include new scopes, scope_context_vars and scope_uses sections that describe the variables in the scopes. Newer versions of DevTools use this data to find unused captured variables. This can expose more application code and secrets embedded in source, and as usual, heap snapshots should be treated as sensitive data.
  • The global object and the global proxy are now named global [JSGlobalObject] and global [JSGlobalProxy] instead of global (global) and global (global*). Tools that parse the older names from the heap snapshots should be updated.

CPU and heap profiles

  • Function names and URLs are now escaped correctly in profiles serialized as JSON.
  • perf inject can now see inlined frames from other scripts instead of [unknown] when using --perf-prof.
  • Sampling heap profile retrieval can be faster by about 30% in local benchmarks due to more efficient use of strings.

Inspector and debugging

  • Debugger.setInstrumentationBreakpoint now works for Node.js CommonJS modules.
  • Support for live editing via the inspector has been fully removed. Debugger.setScriptSource now always fails with "setScriptSource functionality no longer available". This feature was previously deprecated and put behind the --inspector-live-edit flag, it has now been completely removed.
  • New protocol capabilities:
    • Debugger.evaluateOnCallFrame accepts a scopeNumber to evaluate in an outer scope.
    • Scopes now report an experimental empty flag.
  • Several crashes that could occur while using the debugger have been fixed.

Tracing

  • In the 27.0.0 alpha build, most V8 trace events now no longer appear in the files written by --trace-event-categories due to V8's migration to Perfetto-style macros. Support for building with Perfetto by default is in progress. --trace-gc and the gc entries of perf_hooks are not affected. Downstream distributors who build with Perfetto can configure Node.js with --shared-perfetto to link to a shared Perfetto SDK.
  • The disabled-by-default-v8.gc trace category now produces fewer events. The more frequent per-step garbage collection events have been moved to the disabled-by-default-v8.gc_extra category.

Removed and renamed V8 flags

Renamed

OldNew
--experimental-wasm-<name>--wasm-<name>
--experimental-wasm-simd-opt (arm64 only)--future-wasm-simd-opt
--abort-on-contradictory-flags--flag-processing-mode=abort-on-error
--exit-on-contradictory-flags--flag-processing-mode=exit-on-error
--allow-overwriting-for-next-flag--flag-processing-mode=ignore-contradictions

Removed

  • --js-upsert
  • --js-regexp-escape
  • --js-promise-try
  • --js-error-iserror
  • --js-base-64
  • --js-explicit-resource-management
  • --js-sum-precise
  • --js-intl-locale-variants
  • --js-atomics-pause
  • --js-regexp-duplicate-named-groups
  • --js-regexp-modifiers
  • --bytecode-old-age
  • --flush-code-based-on-time
  • --detailed-error-stack-trace
  • --heap-profiler-show-hidden-objects
  • --expose-ignition-statistics
  • --riscv-c-extension (RISC-V only)
  • --turbo-compress-frame-translations
  • --inspector-live-edit
  • --max-fast-properties
  • --validate-asm
  • --wasm-lazy-validation
  • --wasm-async-compilation