Repository: immerjs/immer (js)

Issue:
Performance of mutating large collections: arrays vs objects
## 🙋‍♂ Question

Hello!

When I first read the immer [performance docs](https://immerjs.github.io/immer/docs/performance), it didn't occur to me at the time that choice of Array vs Object for storing data might make a large impact on performance.

I use `immer` to manage a redux store in which [records are keyed by their ID inside of an object](https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape#designing-a-normalized-state). This performs perfectly well when the number of records is in the low 1000s, however we are looking to push our application to the level of supporting tens or possibly even hundreds of thousands of records. What I've found is that for sufficiently large collections it can take _vastly_ longer to update **a single record** if the collection is an object as opposed to an array.

1) Is there any known workaround such that I can get array-like performance, but continue to use an object? (Or some other data structure that gives us O(1) retrieval by ID?)
2) If there is no such workaround, would you accept a PR to the docs clarifying this?

## Link to repro

https://codesandbox.io/s/immer-sandbox-g64y4

In this demo, I create an array collection and an object collection of todos (just like in the `test:perf` code) but each with size 200,000, and then measure the time it takes to update a single record within the collection.

![image](https://user-images.githubusercontent.com/5460144/88980511-e5759280-d291-11ea-905a-add8265e565b.png)

**Update**: `Map`s seem to be _about_ an order of magnitude faster than objects but still a few times slower than arrays (I tried out a few different collection sizes). However, I don't think that I would want to take the hit of using an unserializable data structure in redux.

https://codesandbox.io/s/immer-sandbox-09zyb

![image](https://user-images.githubusercontent.com/5460144/88982830-4ce21100-d297-11ea-8649-406fe310cb6f.png)


## Environment

We only accept questions against the latest Immer version.

- **Immer version:** 7.0.7
- [x] Occurs with `setUseProxies(true)`
- [x] Occurs with `setUseProxies(false)` (ES5 only)

P.S. thanks for this awesome package <3


Pull request:
feature: Always freeze by default
BREAKING CHANGE: always freeze by default, even in production mode. Use `setAutoFreeze(process.env.NODE_ENV !== 'production')` for the old behavior. See https://github.com/immerjs/immer/issues/687#issuecomment-728881754 for the rationale. Fixes #649, #681, #687 

Patch:
diff --git a/.travis.yml b/.travis.yml
index 22a2053..a07a08a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,7 @@
 language: node_js
 node_js:
-  # - "10.18.1"
-  - "node"
+  - "10.18.1"
+  # - "node"
 env:
   - NODE_ENV=TEST
 cache:
diff --git a/docs/api.md b/docs/api.md
index 9e2b05d..81711d2 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -21,6 +21,7 @@ title: API overview
 | `enableMapSet()` | Enables support for `Map` and `Set` collections. | [Installation](installation#pick-your-immer-version) |
 | `enablePatches()` | Enables support for JSON patches. | [Installation](installation#pick-your-immer-version) |
 | `finishDraft` | Given an draft created using `createDraft`, seals the draft and produces and returns the next immutable state that captures all the changes | [Async](async.md) |
+| `freeze(obj, deep?)` | Freezes draftable objects. Returns the original object. By default freezes shallowly, but if the second argument is `true` it will freeze recursively. |
 | `Immer` | constructor that can be used to create a second "immer" instance (exposing all APIs listed in this instance), that doesn't share its settings with global instance. |
 | `immerable` | Symbol that can be added to a constructor or prototype, to indicate that Immer should treat the class as something that can be safely drafted | [Classes](complex-objects.md) |
 | `Immutable<T>` | Exposed TypeScript type to convert mutable types to immutable types |  |
@@ -31,7 +32,7 @@ title: API overview
 | `Patch` | Exposed TypeScript type, describes the shape of an (inverse) patch object | [Patches](patches.md) |
 | `produce` | The core API of Immer, also exposed as the `default` export | [Produce](produce.md) |
 | `produceWithPatches` | Works the same as `produce`, but instead of just returning the produced object, it returns a tuple, consisting of `[result, patches, inversePatches]`. | [Patches](patches.md) |
-| `setAutoFreeze` | Enables / disables automatic freezing of the trees produces. By default enabled in development builds | [Freezing](freezing.md) |
+| `setAutoFreeze` | Enables / disables automatic freezing of the trees produces. By default enabled. | [Freezing](freezing.md) |
 | `setUseProxies` | Can be used to disable or force the use of `Proxy` objects. Useful when filing bug reports. |  |
 
 ## Importing immer
diff --git a/docs/freezing.md b/docs/freezing.md
index 23d1712..5eef247 100644
--- a/docs/freezing.md
+++ b/docs/freezing.md
@@ -16,8 +16,10 @@ title: Auto freezing
     <a style="font-style:italic;padding:5px;margin:5px;"  href="https://egghead.io/lessons/javascript-produces-immutable-data-and-avoid-unnecessary-creation-of-new-data-trees-with-immer">Hosted on egghead.io</a>
 </details>
 
-Immer automatically freezes any state trees that are modified using `produce`. This protects against accidental modifications of the state tree outside of a producer. This comes with a performance impact, so it is recommended to disable this option in production. By default, it is turned on during local development and turned off in production. Use `setAutoFreeze(true / false)` to explicitly turn this feature on or off.
+Immer automatically freezes any state trees that are modified using `produce`. This protects against accidental modifications of the state tree outside of a producer. In most cases this provides the most optimal behavior, but `setAutoFreeze(true / false)` can be used to explicitly turn this feature on or off.
 
 Immer will never freeze (the contents of) non-enumerable, non-own or symbolic properties, unless their content was drafted.
 
+_⚠️ Immer freezes everything recursively, for large data objects that won't be changed in the future this might be over-kill, in that case it can be more efficient to shallowly pre-freeze data using the `freeze` utility.⚠️_
+
 _⚠️ If auto freezing is enabled, recipes are not entirely side-effect free: Any plain object or array that ends up in the produced result, will be frozen, even when these objects were not frozen before the start of the producer! ⚠️_
diff --git a/docs/performance.md b/docs/performance.md
index 5d8738d..7c679d9 100644
--- a/docs/performance.md
+++ b/docs/performance.md
@@ -44,7 +44,7 @@ Most important observation:
 
 ### Pre-freeze data
 
-When adding a large data set to the state tree in an Immer producer (for example data received from a JSON endpoint), it is worth to call `Object.freeze(json)` on the root of the data to be added first. This will allow Immer to add the new data to the tree faster, as it will skip freezing it, or searching the tree for any changes (drafts) that might be made.
+When adding a large data set to the state tree in an Immer producer (for example data received from a JSON endpoint), it is worth to call `freeze(json)` on the root of the data to be added first. This will allow Immer to add the new data to the tree faster, as it will skip _recursively_ freezing it, or searching the new data for any changes (drafts) that might be made.
 
 ### You can always opt-out
 
diff --git a/src/core/immerClass.ts b/src/core/immerClass.ts
index e6c1bdc..3e1c53d 100644
--- a/src/core/immerClass.ts
+++ b/src/core/immerClass.ts
@@ -17,7 +17,6 @@ import {
 	getPlugin,
 	die,
 	hasProxies,
-	isMinified,
 	enterScope,
 	revokeScope,
 	leaveScope,
@@ -36,7 +35,7 @@ interface ProducersFns {
 export class Immer implements ProducersFns {
 	useProxies_: boolean = hasProxies
 
-	autoFreeze_: boolean = __DEV__ ? true /* istanbul ignore next */ : !isMinified
+	autoFreeze_: boolean = true
 
 	constructor(config?: {useProxies?: boolean; autoFreeze?: boolean}) {
 		if (typeof config?.useProxies === "boolean")
diff --git a/src/immer.ts b/src/immer.ts
index ea3fc1e..5345549 100644
--- a/src/immer.ts
+++ b/src/immer.ts
@@ -16,7 +16,8 @@ export {
 	isDraft,
 	isDraftable,
 	NOTHING as nothing,
-	DRAFTABLE as immerable
+	DRAFTABLE as immerable,
+	freeze
 } from "./internal"
 
 const immer = new Immer()
diff --git a/src/types/index.js.flow b/src/types/index.js.flow
index 29758fb..1be017a 100644
--- a/src/types/index.js.flow
+++ b/src/types/index.js.flow
@@ -109,3 +109,5 @@ declare export function enableES5(): void
 declare export function enableMapSet(): void
 declare export function enablePatches(): void
 declare export function enableAllPlugins(): void
+
+declare export function freeze<T>(obj: T, freeze?: boolean): T
diff --git a/src/utils/common.ts b/src/utils/common.ts
index 6798074..16f9430 100644
--- a/src/utils/common.ts
+++ b/src/utils/common.ts
@@ -179,13 +179,22 @@ export function shallowCopy(base: any) {
 	return Object.create(Object.getPrototypeOf(base), descriptors)
 }
 
-export function freeze(obj: any, deep: boolean): void {
-	if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj)) return
+/**
+ * Freezes draftable objects. Returns the original object.
+ * By default freezes shallowly, but if the second argument is `true` it will freeze recursively.
+ *
+ * @param obj
+ * @param deep
+ */
+export function freeze<T>(obj: T, deep?: boolean): T
+export function freeze<T>(obj: any, deep: boolean = false): T {
+	if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj)) return obj
 	if (getArchtype(obj) > 1 /* Map or Set */) {
 		obj.set = obj.add = obj.clear = obj.delete = dontMutateFrozenCollections as any
 	}
 	Object.freeze(obj)
 	if (deep) each(obj, (key, value) => freeze(value, true), true)
+	return obj
 }
 
 function dontMutateFrozenCollections() {
diff --git a/src/utils/env.ts b/src/utils/env.ts
index c846d09..4a3d306 100644
--- a/src/utils/env.ts
+++ b/src/utils/env.ts
@@ -12,10 +12,6 @@ export const hasProxies =
 	typeof Proxy.revocable !== "undefined" &&
 	typeof Reflect !== "undefined"
 
-/* istanbul ignore next */
-function mini() {}
-export const isMinified = mini.name !== "mini"
-
 /**
  * The sentinel value returned by producers to replace the draft with undefined.
  */
diff --git a/website/static/css/custom.css b/website/static/css/custom.css
index 00eca27..57fe732 100755
--- a/website/static/css/custom.css
+++ b/website/static/css/custom.css
@@ -22,16 +22,3 @@ a {
 a:hover {
 	text-decoration: underline;
 }
-
-/* BLM */
-.slidingNav::before {
-	content: "Black Lives Matter";
-	position: fixed;
-	left: 0;
-	width: 100vw;
-	top: 13px;
-	text-align: center;
-	pointer-events: none;
-	text-transform: uppercase;
-	font-weight: bold;
-}


Test patch:
diff --git a/__tests__/frozen.js b/__tests__/frozen.js
index d1571db..04439d4 100644
--- a/__tests__/frozen.js
+++ b/__tests__/frozen.js
@@ -2,7 +2,8 @@
 import produce, {
 	setUseProxies,
 	setAutoFreeze,
-	enableAllPlugins
+	enableAllPlugins,
+	freeze
 } from "../src/immer"
 
 enableAllPlugins()
@@ -246,3 +247,21 @@ function runTests(name, useProxies) {
 		})
 	})
 }
+
+test("freeze - shallow", () => {
+	const obj1 = {hello: {world: true}}
+	const res = freeze(obj1)
+
+	expect(res).toBe(obj1)
+	expect(Object.isFrozen(res)).toBe(true)
+	expect(Object.isFrozen(res.hello)).toBe(false)
+})
+
+test("freeze - deep", () => {
+	const obj1 = {hello: {world: true}}
+	const res = freeze(obj1, true)
+
+	expect(res).toBe(obj1)
+	expect(Object.isFrozen(res)).toBe(true)
+	expect(Object.isFrozen(res.hello)).toBe(true)
+})