Перейти до вмісту

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 in Node.js 27

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 method 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'

Changes to Array

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 errors and stack traces

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'

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.

Changes to Promises and async/await

When the callback passed to Promise.try returns a promise, Promise.try now returns that promise directly, see ecma262#3883.

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

There are also a few edge cases that have been fixed:

  • When ESM with top-level await are 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.
  • Re-entrant Atomics.wait on the same thread now throws TypeError instead of crashing the process. This should only happen when a debugger evaluates code on a blocked worker.

Changes to Iterator helpers

take() and drop() with a limit above 2^53-1 throw RangeError and close the iterator.

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

AsyncIterator.prototype[Symbol.asyncDispose] now calls return() with no arguments instead of one undefined argument, 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 is now a 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 typed array subclass over a resizable ArrayBuffer, now only passes the buffer and the byte offset to the subclass constructor, and omits the third undefined length, as the specification requires, so the new view also follows the length of the buffer.

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 are now a SyntaxError.

class A { static { return; } }         // SyntaxError in both versions
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

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 in Node.js 27

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

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 heap size limit is now larger on machines with 4 GB-15 GB of RAM. When --max-old-space-size is not specified, the default maximum heap 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 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 is now dropped after it has not run for 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 ~90 KB more memory, which 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 is now cheaper because the stack frame details are only computed when error.stack is read.
  • There are now fewer full garbage collections at startup. The heap may grow to 32 MB before the first full collection.

V8 performance changes in Node.js 27

V8 WebAssembly changes in Node.js 27

  • 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.
  • --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.
  • --experimental-wasm-exnref, --experimental-wasm-branch-hinting and --experimental-wasm-rab-integration have been removed, as the features they enabled are now always on.
  • 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 several 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 in Node.js 27

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 every loaded script instead of only the first 1024 characters. They also include new scopes, scope_context_vars and scope_uses sections that describe which variables each scope uses. Newer DevTools use the scope data to find unused captured variables.
  • 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.
  • Profile retrieval can be faster by about 30% due to more efficient use of strings.

Inspector and debugging

  • Debugger.setInstrumentationBreakpoint now works for Node.js CommonJS modules.
  • 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 in Node.js 27

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
  • --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