Skip to content

Commit df897c3

Browse files
committed
Implement optional strings-only shallow copies for perf
1 parent d6e1eb8 commit df897c3

File tree

2 files changed

+32
-4
lines changed

2 files changed

+32
-4
lines changed

src/core/immerClass.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,17 @@ interface ProducersFns {
3131
produceWithPatches: IProduceWithPatches
3232
}
3333

34-
export type StrictMode = boolean | "class_only"
34+
export type StrictMode =
35+
| boolean
36+
| "class_only"
37+
| "strings_only"
38+
| "with_symbols"
39+
| "full"
3540

3641
export class Immer implements ProducersFns {
3742
autoFreeze_: boolean = true
3843
useStrictShallowCopy_: StrictMode = false
39-
useStrictIteration_: boolean = true
44+
useStrictIteration_: boolean = false
4045

4146
constructor(config?: {
4247
autoFreeze?: boolean
@@ -45,7 +50,7 @@ export class Immer implements ProducersFns {
4550
}) {
4651
if (typeof config?.autoFreeze === "boolean")
4752
this.setAutoFreeze(config!.autoFreeze)
48-
if (typeof config?.useStrictShallowCopy === "boolean")
53+
if (typeof config?.useStrictShallowCopy !== "undefined")
4954
this.setUseStrictShallowCopy(config!.useStrictShallowCopy)
5055
if (typeof config?.useStrictIteration === "boolean")
5156
this.setUseStrictIteration(config!.useStrictIteration)

src/utils/common.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,11 @@ export function shallowCopy(base: any, strict: StrictMode) {
164164

165165
const isPlain = isPlainObject(base)
166166

167-
if (strict === true || (strict === "class_only" && !isPlain)) {
167+
if (
168+
strict === true ||
169+
strict === "full" ||
170+
(strict === "class_only" && !isPlain)
171+
) {
168172
// Perform a strict copy
169173
const descriptors = Object.getOwnPropertyDescriptors(base)
170174
delete descriptors[DRAFT_STATE as any]
@@ -192,6 +196,25 @@ export function shallowCopy(base: any, strict: StrictMode) {
192196
// perform a sloppy copy
193197
const proto = getPrototypeOf(base)
194198
if (proto !== null && isPlain) {
199+
// v8 has a perf cliff at 1020 properties where it
200+
// switches from "fast properties" to "dictionary mode" at 1020 keys:
201+
// - https://github.com/v8/v8/blob/754e7ba956b06231c487e09178aab9baba1f46fe/src/objects/property-details.h#L242-L247
202+
// - https://github.com/v8/v8/blob/754e7ba956b06231c487e09178aab9baba1f46fe/test/mjsunit/dictionary-prototypes.js
203+
// At that size, object spread gets drastically slower,
204+
// and an `Object.keys()` loop becomes _faster_.
205+
// Immer currently expects that we also copy symbols. That would require either a `Reflect.ownKeys()`,
206+
// or `.keys()` + `.getOwnPropertySymbols()`.
207+
// For v10.x, we can keep object spread the default,
208+
// and offer an option to switch to just strings to enable better perf
209+
// with larger objects. For v11, we can flip those defaults.
210+
if (strict === "strings_only") {
211+
const copy: Record<string | symbol, any> = {}
212+
Object.keys(base).forEach(key => {
213+
copy[key] = base[key]
214+
})
215+
return copy
216+
}
217+
195218
return {...base} // assumption: better inner class optimization than the assign below
196219
}
197220
const obj = Object.create(proto)

0 commit comments

Comments
 (0)