Vue version
3.5.43
Link to minimal reproduction
https://play.vuejs.org/#eNqVVmmO2zYUvsqDUcAyYsueSVoUmgVNigRItxRpmvywjYS2KItjiRRIygscA71FbtCL5CY9ST+SksaTTAJkfozFt35vJQ+9x1UVb2reS3qXZqlFZclwW1fXMzke03PLNbNCrohRynk10pwtrdhwYlqzPSm55FSyNTdkKvDSIT12jDjTqoy8zICYTL0xybf0rFDMPjz3Qi2/rJc5mUJtuaZMabI5p22uCk6vfyRhVMEsj72FV7kwVLEV3BfO2550LQ29rjlF8ELCwwWYuzDNYEhGebtg2qAkJDFv9A1f0Bul11wniDLT3OSt1yFthc29omEl8MDRhmsjFJQN6A5OwfYrrWqZAqMoK6UtHQAgG3aiR1hVJfWR5v5MzuRSSaAwltna0JWTjfovaymR5//++bc/aCW02rb86RzUTlPVGnm/ond3PDYht95yayuTjMfLVMY3JuWF2OhYcjuWVTn+CVjGjYqw+5++OzRgj+NUGHvCirkpRwsHheu40iqFMUTRINFLwDgp+YEKLlc2T+hsMpnQcUjR2yGJAV1d43+rlkEpiuOYeTqLg07LdTVycR9mkqg/dXJaz+njh0ffT/oJRV7p4FslKrglAdnJBX4uCRL4ePBgQE4N6OaA4M1k3p/Wg2+04/VgaNAautvf32ruRPvU6H2jgaaF3vrEdEgPg93p/OJLjvDX+GJxVZs8EoOLe0YvuD52ORdobxQlk3d82U99rYOvNXydn+HX+emEJ2BWXEO4ZFgMsVTbCM4z6f7bgOYzPo2gCDQQMOjkKGJDWoS+AGsBTY19pCXZ6dkE5eyGo5YOsJd8sbjhS+vT+lRaLbiJGhJvjr6nBnHJqiiarofANPea7tvF7gKfD7oGXXCAdAmBlwhUH39g7Uhl3bBF08MRanQ4EjaJktxlCjMMEYWBun9rtk5YhnV166NSxv7OjcF6wxgFBMNG6Ag+hr3V3Pp1BVVX17C7or9f/hYv4cjyEDrOkWM/KdQimoaVMR+iWHZf8YT6lu/s+IZtWNj6ffhAx3XsUqV1wR11JoO7WMkywHN5P1DKLAPbt4trYrci4g0rsCOv2oqs+d5ETjAO8TQVWHst2JDYqgmhBoGdeKON7HQNuD78huy/QXVIncOwQDuX79x2vt1jQyp5Kph05To/Czs/KtED9CtKEna3ZQvaCCMWBY+RXmoSG8NPKSRS6eqCjusSwLVGJyB8HiL4BEI/1IIyJgqeIov0AKJxm7b376lvOPfjguut78K4HIf8477FwfIS94nlOBFdVteHQ3tNHI+XY3cpgwzUuBs3I5FdzXo+683+7Hm+k9DXlza/Vhg23N4KTnBylJDZ7hia6/O2vdvenfjHD+FzDPudJwCBskPirlQHZ9ajBIV3pNgVuMPl5FMXU6D7mEDoiAFdbNUzseNpdI6t8ImEB/xlgaiRoHFnbdBJuyUTpBvsbRj4cBnFN75uC4CjsXtPt3jX4B1wwNsjTXFLJ/So2tHZebXDXsMcjVghVjIhLVa5ddtS2lHGSlHsE6rFqFRSmYotMc7d54XbZDZNMqGNHS1zUXgfp2c3jCfGC57BNrTQMg2u3rBnDbopEyvcykriEedHcdZbqrJCD+oXlas/SpKEIXU8VuCt9YunWV1zfwN5nZwv1/fQb8zO0Wa9P/E24nqDgnY8y/SK28B++tcfgHvCDDvkq8yXHJNQO4xB7AneUYB9IufRPvevHGT+lXm6s1y6Ce+AOslwj856eNb8/JXQb+E+jB95Pcw3svjWmtdhbyCFP8ST+GHv+D/oDq7+
Steps to reproduce
Open the playground link (or run the snippet) and compare the two columns. The arrays being spread and converted are plain, non-reactive arrays.
What is expected?
Operations on plain arrays run at the same speed after a reactive array has been iterated.
What is actually happening?
From the playground, Vue 3.5.43, Chrome 153, macOS, tab visible (median of 21 timings, ms):
| operation |
before |
after one for…of over reactive([{}]) |
[...arr] × 450 (1000 items) |
0.10 |
8.00 |
f(...arr) × 450 |
0.90 |
8.40 |
Array.from(arr) × 450 |
0.10 |
6.20 |
new Float32Array(arr) (450k items, incl. building the array) |
1.50 |
15.40 |
Not affected in our measurements: for…of, f.apply(null, arr), Array.from({ length }, fn), push, map/filter/slice/concat.
Importing Vue alone changes nothing; the first iteration of a deep-reactive array does, and it lasts for the life of the V8 isolate (in Chrome, the renderer process, so same-site pages sharing that process are affected too). In a Nuxt 4 app it's triggered during startup (by the @nuxt/icon and @nuxtjs/supabase plugins iterating reactive arrays), so every page is affected, in dev and production. For us it makes a three.js layout step about 20% slower than the same code where the fast path is intact.
System Info
Chrome 153.0 (macOS), Vue 3.5.43, Nuxt 4.5, macOS Version 26.6.2 (25G83)
Any additional comments?
Cause
iterator() in packages/reactivity/src/arrayInstrumentations.ts (used by [Symbol.iterator], values and entries on reactive arrays) adds an own next property to the native array iterator:
iter._next = iter.next
iter.next = () => { … }
V8 treats an own next on an array iterator as a change to array iteration and permanently invalidates its "array iterator" protector. After that, spread, Array.from of iterables and typed-array construction from arrays take the generic, much slower path.
Possible fix
Return a wrapper iterator instead of mutating the native one. Inheriting from %IteratorPrototype% keeps [Symbol.iterator] and the iterator helpers:
function iterator(self, method, wrapValue) {
const arr = shallowReadArray(self)
const iter = arr[method]()
if (arr !== self && !isShallow(self)) {
const wrapped = Object.create(Object.getPrototypeOf(Object.getPrototypeOf(iter)))
wrapped.next = () => {
const result = iter.next()
if (!result.done) {
result.value = wrapValue(result.value)
}
return result
}
return wrapped
}
return iter
}
Served in place of the original in our app, this kept spread at 0.2 ms and typed-array construction at its normal speed, and the app behaved the same (its computed readouts were identical). We haven't run Vue's own test suite against it.
Vue version
3.5.43
Link to minimal reproduction
https://play.vuejs.org/#eNqVVmmO2zYUvsqDUcAyYsueSVoUmgVNigRItxRpmvywjYS2KItjiRRIygscA71FbtCL5CY9ST+SksaTTAJkfozFt35vJQ+9x1UVb2reS3qXZqlFZclwW1fXMzke03PLNbNCrohRynk10pwtrdhwYlqzPSm55FSyNTdkKvDSIT12jDjTqoy8zICYTL0xybf0rFDMPjz3Qi2/rJc5mUJtuaZMabI5p22uCk6vfyRhVMEsj72FV7kwVLEV3BfO2550LQ29rjlF8ELCwwWYuzDNYEhGebtg2qAkJDFv9A1f0Bul11wniDLT3OSt1yFthc29omEl8MDRhmsjFJQN6A5OwfYrrWqZAqMoK6UtHQAgG3aiR1hVJfWR5v5MzuRSSaAwltna0JWTjfovaymR5//++bc/aCW02rb86RzUTlPVGnm/ond3PDYht95yayuTjMfLVMY3JuWF2OhYcjuWVTn+CVjGjYqw+5++OzRgj+NUGHvCirkpRwsHheu40iqFMUTRINFLwDgp+YEKLlc2T+hsMpnQcUjR2yGJAV1d43+rlkEpiuOYeTqLg07LdTVycR9mkqg/dXJaz+njh0ffT/oJRV7p4FslKrglAdnJBX4uCRL4ePBgQE4N6OaA4M1k3p/Wg2+04/VgaNAautvf32ruRPvU6H2jgaaF3vrEdEgPg93p/OJLjvDX+GJxVZs8EoOLe0YvuD52ORdobxQlk3d82U99rYOvNXydn+HX+emEJ2BWXEO4ZFgMsVTbCM4z6f7bgOYzPo2gCDQQMOjkKGJDWoS+AGsBTY19pCXZ6dkE5eyGo5YOsJd8sbjhS+vT+lRaLbiJGhJvjr6nBnHJqiiarofANPea7tvF7gKfD7oGXXCAdAmBlwhUH39g7Uhl3bBF08MRanQ4EjaJktxlCjMMEYWBun9rtk5YhnV166NSxv7OjcF6wxgFBMNG6Ag+hr3V3Pp1BVVX17C7or9f/hYv4cjyEDrOkWM/KdQimoaVMR+iWHZf8YT6lu/s+IZtWNj6ffhAx3XsUqV1wR11JoO7WMkywHN5P1DKLAPbt4trYrci4g0rsCOv2oqs+d5ETjAO8TQVWHst2JDYqgmhBoGdeKON7HQNuD78huy/QXVIncOwQDuX79x2vt1jQyp5Kph05To/Czs/KtED9CtKEna3ZQvaCCMWBY+RXmoSG8NPKSRS6eqCjusSwLVGJyB8HiL4BEI/1IIyJgqeIov0AKJxm7b376lvOPfjguut78K4HIf8477FwfIS94nlOBFdVteHQ3tNHI+XY3cpgwzUuBs3I5FdzXo+683+7Hm+k9DXlza/Vhg23N4KTnBylJDZ7hia6/O2vdvenfjHD+FzDPudJwCBskPirlQHZ9ajBIV3pNgVuMPl5FMXU6D7mEDoiAFdbNUzseNpdI6t8ImEB/xlgaiRoHFnbdBJuyUTpBvsbRj4cBnFN75uC4CjsXtPt3jX4B1wwNsjTXFLJ/So2tHZebXDXsMcjVghVjIhLVa5ddtS2lHGSlHsE6rFqFRSmYotMc7d54XbZDZNMqGNHS1zUXgfp2c3jCfGC57BNrTQMg2u3rBnDbopEyvcykriEedHcdZbqrJCD+oXlas/SpKEIXU8VuCt9YunWV1zfwN5nZwv1/fQb8zO0Wa9P/E24nqDgnY8y/SK28B++tcfgHvCDDvkq8yXHJNQO4xB7AneUYB9IufRPvevHGT+lXm6s1y6Ce+AOslwj856eNb8/JXQb+E+jB95Pcw3svjWmtdhbyCFP8ST+GHv+D/oDq7+
Steps to reproduce
Open the playground link (or run the snippet) and compare the two columns. The arrays being spread and converted are plain, non-reactive arrays.
What is expected?
Operations on plain arrays run at the same speed after a reactive array has been iterated.
What is actually happening?
From the playground, Vue 3.5.43, Chrome 153, macOS, tab visible (median of 21 timings, ms):
for…ofoverreactive([{}])[...arr]× 450 (1000 items)f(...arr)× 450Array.from(arr)× 450new Float32Array(arr)(450k items, incl. building the array)Not affected in our measurements:
for…of,f.apply(null, arr),Array.from({ length }, fn),push,map/filter/slice/concat.Importing Vue alone changes nothing; the first iteration of a deep-reactive array does, and it lasts for the life of the V8 isolate (in Chrome, the renderer process, so same-site pages sharing that process are affected too). In a Nuxt 4 app it's triggered during startup (by the
@nuxt/iconand@nuxtjs/supabaseplugins iterating reactive arrays), so every page is affected, in dev and production. For us it makes a three.js layout step about 20% slower than the same code where the fast path is intact.System Info
Any additional comments?
Cause
iterator()inpackages/reactivity/src/arrayInstrumentations.ts(used by[Symbol.iterator],valuesandentrieson reactive arrays) adds an ownnextproperty to the native array iterator:V8 treats an own
nexton an array iterator as a change to array iteration and permanently invalidates its "array iterator" protector. After that, spread,Array.fromof iterables and typed-array construction from arrays take the generic, much slower path.Possible fix
Return a wrapper iterator instead of mutating the native one. Inheriting from
%IteratorPrototype%keeps[Symbol.iterator]and the iterator helpers:Served in place of the original in our app, this kept spread at 0.2 ms and typed-array construction at its normal speed, and the app behaved the same (its computed readouts were identical). We haven't run Vue's own test suite against it.