Skip to content

Commit 01d0784

Browse files
committed
Optimise context state for less GC pressure
- Avoid Context Map() being copied on every element - Optimise restoring previous context values - Split legacy context into separate state
1 parent 5b25b0a commit 01d0784

File tree

13 files changed

+176
-110
lines changed

13 files changed

+176
-110
lines changed

src/__tests__/visitor.test.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ import { createPortal } from 'react-dom'
99

1010
import {
1111
Dispatcher,
12-
clearCurrentContextMap,
13-
getCurrentContextMap
12+
setCurrentContextStore,
13+
setCurrentContextMap,
14+
getCurrentContextMap,
15+
getCurrentContextStore,
16+
readContextValue
1417
} from '../internals'
18+
1519
import { visitElement } from '../visitor'
1620

1721
const {
@@ -23,7 +27,9 @@ let prevDispatcher = null
2327
beforeEach(() => {
2428
prevDispatcher = ReactCurrentDispatcher.current
2529
ReactCurrentDispatcher.current = Dispatcher
26-
clearCurrentContextMap()
30+
31+
setCurrentContextMap({})
32+
setCurrentContextStore(new Map())
2733
})
2834

2935
afterEach(() => {
@@ -75,14 +81,14 @@ describe('visitElement', () => {
7581
child = visitElement(child, [], () => {})[0]
7682
}
7783

78-
expect(getCurrentContextMap().get(Context)).toBe('testA')
84+
expect(readContextValue(Context)).toBe('testA')
7985
expect(leaf).toHaveBeenCalledWith('testA')
8086

8187
for (let i = 0, child = makeChild('testB'); i <= 3 && child; i++) {
8288
child = visitElement(child, [], () => {})[0]
8389
}
8490

85-
expect(getCurrentContextMap().get(Context)).toBe('testB')
91+
expect(readContextValue(Context)).toBe('testB')
8692
expect(leaf).toHaveBeenCalledWith('testB')
8793
})
8894

@@ -107,6 +113,7 @@ describe('visitElement', () => {
107113

108114
expect(queue[0]).toMatchObject({
109115
contextMap: getCurrentContextMap(),
116+
contextStore: getCurrentContextStore(),
110117
thenable: expect.any(Promise),
111118
kind: 'frame.lazy',
112119
type: Test,

src/index.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ import {
1212
} from './render'
1313

1414
import {
15-
clearCurrentContextMap,
15+
setCurrentContextStore,
1616
setCurrentContextMap,
17-
getCurrentContextMap,
1817
Dispatcher
1918
} from './internals'
2019

@@ -51,12 +50,14 @@ const flushFrames = (queue: Frame[], visitor: Visitor): Promise<void> => {
5150
})
5251
}
5352

54-
const defaultVisitor = () => {};
53+
const defaultVisitor = () => {}
5554

5655
const renderPrepass = (element: Node, visitor?: Visitor): Promise<void> => {
5756
const queue: Frame[] = []
5857
let fn = visitor !== undefined ? visitor : defaultVisitor
59-
clearCurrentContextMap()
58+
59+
setCurrentContextMap({})
60+
setCurrentContextStore(new Map())
6061

6162
try {
6263
prevDispatcher = ReactCurrentDispatcher.current

src/internals/__tests__/state.test.js renamed to src/internals/__tests__/context.test.js

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,44 @@
1-
import { setCurrentContextMap, readContextMap, maskContext } from '../state'
1+
import {
2+
setCurrentContextStore,
3+
setCurrentContextMap,
4+
readContextValue,
5+
maskContext
6+
} from '../context'
27

3-
describe('readContextMap', () => {
8+
describe('readContextValue', () => {
49
it('returns values in a Map by key', () => {
510
const map = new Map()
611
const ctx = {}
7-
setCurrentContextMap(map)
8-
map.set('key', 'value')
12+
setCurrentContextStore(map)
913
map.set(ctx, 'value')
10-
expect(readContextMap('key')).toBe('value')
11-
expect(readContextMap(ctx)).toBe('value')
14+
expect(readContextValue(ctx)).toBe('value')
1215
})
1316

1417
it('returns default values when keys are unknown', () => {
1518
setCurrentContextMap(new Map())
1619
const ctx = { _currentValue: 'default' }
17-
expect(readContextMap('key')).toBe(undefined)
18-
expect(readContextMap(ctx)).toBe('default')
20+
expect(readContextValue(ctx)).toBe('default')
1921
})
2022
})
2123

2224
describe('maskContext', () => {
2325
it('supports contextType', () => {
2426
const map = new Map()
2527
const ctx = {}
26-
setCurrentContextMap(map)
28+
setCurrentContextStore(map)
2729
map.set(ctx, 'value')
2830
expect(maskContext({ contextType: ctx })).toBe('value')
2931
})
3032

3133
it('supports no context', () => {
3234
const map = new Map()
33-
setCurrentContextMap(map)
35+
setCurrentContextStore(map)
3436
expect(maskContext({})).toEqual({})
3537
})
3638

3739
it('supports contextTypes', () => {
38-
const map = new Map()
40+
const map = { a: 'a', b: 'b', c: 'c' }
3941
setCurrentContextMap(map)
40-
map.set('a', 'a')
41-
map.set('b', 'b')
42-
map.set('c', 'c')
4342
expect(maskContext({ contextTypes: { a: null, b: null } })).toEqual({
4443
a: 'a',
4544
b: 'b'

src/internals/context.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// @flow
2+
3+
import type {
4+
AbstractContext,
5+
UserElement,
6+
ContextMap,
7+
ContextStore
8+
} from '../types'
9+
10+
type ContextEntry = [AbstractContext, mixed]
11+
12+
let currentContextStore: ContextStore = new Map()
13+
let currentContextMap: ContextMap = {}
14+
15+
let prevContextMap: void | ContextMap = undefined
16+
let prevContextEntry: void | ContextEntry = undefined
17+
18+
export const getCurrentContextMap = (): ContextMap => currentContextMap
19+
export const getCurrentContextStore = (): ContextStore =>
20+
new Map(currentContextStore)
21+
22+
export const flushPrevContextMap = (): void | ContextMap => {
23+
const prev = prevContextMap
24+
prevContextMap = undefined
25+
return prev
26+
}
27+
28+
export const flushPrevContextStore = (): void | ContextEntry => {
29+
const prev = prevContextEntry
30+
prevContextEntry = undefined
31+
return prev
32+
}
33+
34+
export const restoreContextMap = (prev: ContextMap) => {
35+
currentContextMap = prev
36+
}
37+
38+
export const restoreContextStore = (prev: ContextEntry) => {
39+
currentContextStore.set(prev[0], prev[1])
40+
}
41+
42+
export const setCurrentContextMap = (map: ContextMap) => {
43+
prevContextMap = undefined
44+
currentContextMap = map
45+
}
46+
47+
export const setCurrentContextStore = (store: ContextStore) => {
48+
prevContextEntry = undefined
49+
currentContextStore = store
50+
}
51+
52+
export const assignContextMap = (map: ContextMap) => {
53+
prevContextMap = currentContextMap
54+
currentContextMap = Object.assign({}, currentContextMap, map)
55+
}
56+
57+
export const setContextValue = (context: AbstractContext, value: mixed) => {
58+
prevContextEntry = [context, currentContextStore.get(context)]
59+
currentContextStore.set(context, value)
60+
}
61+
62+
export const readContextValue = (context: AbstractContext) => {
63+
const value = currentContextStore.get(context)
64+
if (value !== undefined) {
65+
return value
66+
}
67+
68+
// Return default if context has no value yet
69+
return context._currentValue
70+
}
71+
72+
const emptyContext = {}
73+
74+
export const maskContext = (type: $PropertyType<UserElement, 'type'>) => {
75+
const { contextType, contextTypes } = type
76+
77+
if (contextType) {
78+
return readContextValue(contextType)
79+
} else if (!contextTypes) {
80+
return emptyContext
81+
}
82+
83+
const maskedContext = {}
84+
for (const name in contextTypes) {
85+
maskedContext[name] = currentContextMap[name]
86+
}
87+
88+
return maskedContext
89+
}

src/internals/dispatcher.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Source: https://github.com/facebook/react/blob/c21c41e/packages/react-dom/src/server/ReactPartialRendererHooks.js
33

44
import is from 'object-is'
5-
import { readContextMap } from './state'
5+
import { readContextValue } from './context'
66

77
import type {
88
AbstractContext,
@@ -143,12 +143,12 @@ function readContext(context: AbstractContext, _: void | number | boolean) {
143143
// NOTE: The warning that is used in ReactPartialRendererHooks is obsolete
144144
// in a prepass, since it'll be caught by a subsequent renderer anyway
145145
// https://github.com/facebook/react/blob/c21c41e/packages/react-dom/src/server/ReactPartialRendererHooks.js#L215-L223
146-
return readContextMap(context)
146+
return readContextValue(context)
147147
}
148148

149149
function useContext(context: AbstractContext, _: void | number | boolean) {
150150
getCurrentIdentity()
151-
return readContextMap(context)
151+
return readContextValue(context)
152152
}
153153

154154
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {

src/internals/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
// @flow
22

3+
export * from './context'
34
export * from './dispatcher'
4-
export * from './state'

src/internals/state.js

Lines changed: 0 additions & 56 deletions
This file was deleted.

src/render/classComponent.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import type {
1414

1515
import {
1616
maskContext,
17-
forkContextMap,
17+
assignContextMap,
1818
setCurrentIdentity,
1919
setCurrentContextMap,
20-
getCurrentContextMap
20+
getCurrentContextMap,
21+
setCurrentContextStore,
22+
getCurrentContextStore
2123
} from '../internals'
2224

2325
const createUpdater = () => {
@@ -95,6 +97,7 @@ const createInstance = (type: any, props: DefaultProps) => {
9597

9698
const makeFrame = (type: any, instance: any, thenable: Promise<any>) => ({
9799
contextMap: getCurrentContextMap(),
100+
contextStore: getCurrentContextStore(),
98101
thenable,
99102
kind: 'frame.class',
100103
instance,
@@ -122,11 +125,8 @@ const render = (type: any, instance: any, queue: Frame[]) => {
122125
typeof instance.getChildContext === 'function'
123126
) {
124127
const childContext = instance.getChildContext()
125-
if (childContext) {
126-
const contextMap = forkContextMap()
127-
for (const name in childContext) {
128-
contextMap.set(name, childContext[name])
129-
}
128+
if (childContext !== null && typeof childContext === 'object') {
129+
assignContextMap(childContext)
130130
}
131131
}
132132

@@ -171,5 +171,6 @@ export const mount = (
171171
export const update = (queue: Frame[], frame: ClassFrame) => {
172172
setCurrentIdentity(null)
173173
setCurrentContextMap(frame.contextMap)
174+
setCurrentContextStore(frame.contextStore)
174175
return render(frame.type, frame.instance, queue)
175176
}

src/render/functionComponent.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import {
1818
maskContext,
1919
makeIdentity,
2020
setCurrentIdentity,
21-
setCurrentContextMap,
2221
getCurrentIdentity,
22+
setCurrentContextStore,
23+
getCurrentContextStore,
24+
setCurrentContextMap,
2325
getCurrentContextMap,
2426
renderWithHooks,
2527
setFirstHook,
@@ -32,6 +34,7 @@ const makeFrame = (
3234
thenable: Promise<any>
3335
) => ({
3436
contextMap: getCurrentContextMap(),
37+
contextStore: getCurrentContextStore(),
3538
id: getCurrentIdentity(),
3639
hook: getFirstHook(),
3740
kind: 'frame.hooks',
@@ -90,5 +93,6 @@ export const update = (queue: Frame[], frame: HooksFrame) => {
9093
setFirstHook(frame.hook)
9194
setCurrentIdentity(frame.id)
9295
setCurrentContextMap(frame.contextMap)
96+
setCurrentContextStore(frame.contextStore)
9397
return render(frame.type, frame.props, queue)
9498
}

0 commit comments

Comments
 (0)