Skip to content

Commit fe35cf4

Browse files
committed
Implement frame.yield to yield to split up synchronous work
The problem with the current implementation is that like with renderToString we never yield to the event loop. When a large app is rendered a lot of the work happens synchronously. This reimplements the visitor to not be recursive and yield when it's taking longer than 10ms. It does this by putting a YieldFrame onto the suspense queue that preserves the visitor's state and returns to it like with other suspense frames.
1 parent 76ce717 commit fe35cf4

File tree

5 files changed

+120
-44
lines changed

5 files changed

+120
-44
lines changed

src/index.js

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import React, { type Node, type Element } from 'react'
44
import type { Visitor, Frame, AbstractElement } from './types'
5-
import { visitChildren } from './visitor'
5+
import { visitChildren, resumeVisitChildren } from './visitor'
66
import { getChildrenArray } from './element'
77

88
import {
@@ -42,20 +42,31 @@ const flushFrames = (queue: Frame[], visitor: Visitor): Promise<void> => {
4242
let children = []
4343

4444
// Update the component after we've suspended to rerender it,
45-
// at which point we'll actually get its children
45+
// at which point we'll actually get its children and continue
46+
// walking the tree
4647
if (frame.kind === 'frame.class') {
47-
children = getChildrenArray(updateClassComponent(queue, frame))
48+
visitChildren(
49+
getChildrenArray(updateClassComponent(queue, frame)),
50+
queue,
51+
visitor
52+
)
4853
} else if (frame.kind === 'frame.hooks') {
49-
children = getChildrenArray(updateFunctionComponent(queue, frame))
54+
visitChildren(
55+
getChildrenArray(updateFunctionComponent(queue, frame)),
56+
queue,
57+
visitor
58+
)
5059
} else if (frame.kind === 'frame.lazy') {
51-
children = getChildrenArray(updateLazyComponent(queue, frame))
60+
visitChildren(
61+
getChildrenArray(updateLazyComponent(queue, frame)),
62+
queue,
63+
visitor
64+
)
65+
} else if (frame.kind === 'frame.yield') {
66+
resumeVisitChildren(frame, queue, visitor)
5267
}
5368

54-
// Now continue walking the previously suspended component's
55-
// children (which might also suspend)
56-
visitChildren(children, queue, visitor)
5769
ReactCurrentDispatcher.current = prevDispatcher
58-
5970
return flushFrames(queue, visitor)
6071
})
6172
}

src/internals/context.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import type {
44
AbstractContext,
55
UserElement,
66
ContextMap,
7-
ContextStore
7+
ContextStore,
8+
ContextEntry
89
} from '../types'
910

1011
/** The context is kept as a Map from a Context value to the current
@@ -17,8 +18,6 @@ import type {
1718
After walking the children they can be restored.
1819
This way the context recursively restores itself on the way up. */
1920

20-
type ContextEntry = [AbstractContext, mixed]
21-
2221
let currentContextStore: ContextStore = new Map()
2322
let currentContextMap: ContextMap = {}
2423

@@ -42,12 +41,16 @@ export const flushPrevContextStore = (): void | ContextEntry => {
4241
return prev
4342
}
4443

45-
export const restoreContextMap = (prev: ContextMap) => {
46-
Object.assign(currentContextMap, prev)
44+
export const restoreContextMap = (prev: void | ContextMap) => {
45+
if (prev !== undefined) {
46+
Object.assign(currentContextMap, prev)
47+
}
4748
}
4849

49-
export const restoreContextStore = (prev: ContextEntry) => {
50-
currentContextStore.set(prev[0], prev[1])
50+
export const restoreContextStore = (prev: void | ContextEntry) => {
51+
if (prev !== undefined) {
52+
currentContextStore.set(prev[0], prev[1])
53+
}
5154
}
5255

5356
export const setCurrentContextMap = (map: ContextMap) => {

src/types/frames.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import type { ComponentType } from 'react'
44
import type { Identity } from '../internals'
55
import type { LazyComponent } from '../types'
6-
import type { DefaultProps, ComponentStatics } from './element'
7-
import type { ContextMap, ContextStore, Hook } from './state'
6+
import type { ContextMap, ContextStore, ContextEntry, Hook } from './state'
7+
import type { AbstractElement, DefaultProps, ComponentStatics } from './element'
88

99
export type BaseFrame = {
1010
contextMap: ContextMap,
@@ -35,4 +35,13 @@ export type HooksFrame = BaseFrame & {
3535
hook: Hook | null
3636
}
3737

38-
export type Frame = ClassFrame | HooksFrame | LazyFrame
38+
/** Description of a pause to yield to the event loop */
39+
export type YieldFrame = BaseFrame & {
40+
kind: 'frame.yield',
41+
children: AbstractElement[][],
42+
index: number[],
43+
map: Array<void | ContextMap>,
44+
store: Array<void | ContextEntry>
45+
}
46+
47+
export type Frame = ClassFrame | HooksFrame | LazyFrame | YieldFrame

src/types/state.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { AbstractContext } from './element'
44

55
export type ContextStore = Map<AbstractContext, mixed>
66
export type ContextMap = { [name: string]: mixed }
7+
export type ContextEntry = [AbstractContext, mixed]
78

89
export type Dispatch<A> = A => void
910

src/visitor.js

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import {
1111

1212
import type {
1313
Visitor,
14+
YieldFrame,
1415
Frame,
1516
ContextMap,
17+
ContextEntry,
1618
DefaultProps,
1719
ComponentStatics,
1820
LazyElement,
@@ -28,12 +30,17 @@ import type {
2830
} from './types'
2931

3032
import {
33+
getCurrentContextMap,
34+
getCurrentContextStore,
35+
setCurrentContextMap,
36+
setCurrentContextStore,
3137
flushPrevContextMap,
3238
flushPrevContextStore,
3339
restoreContextMap,
3440
restoreContextStore,
3541
readContextValue,
36-
setContextValue
42+
setContextValue,
43+
setCurrentIdentity
3744
} from './internals'
3845

3946
import {
@@ -51,6 +58,12 @@ import {
5158
REACT_LAZY_TYPE
5259
} from './symbols'
5360

61+
const makeImmediatePromise = () => {
62+
return new Promise(resolve => {
63+
setImmediate(resolve)
64+
})
65+
}
66+
5467
const render = (
5568
type: ComponentType<DefaultProps> & ComponentStatics,
5669
props: DefaultProps,
@@ -148,39 +161,78 @@ export const visitElement = (
148161
}
149162
}
150163

151-
/** The context (legacy and createContext are separate) are kept
152-
as global state. As we're walking the tree depth-first, we
153-
simply overwrite it with new values. This is recursive and
154-
as we walk back upwards (after visitChildren) we restore
155-
the value that we have previously overwritten. */
156-
const visitChild = (
157-
child: AbstractElement,
164+
const visitLoop = (
165+
traversalChildren: AbstractElement[][],
166+
traversalIndex: number[],
167+
traversalMap: Array<void | ContextMap>,
168+
traversalStore: Array<void | ContextEntry>,
158169
queue: Frame[],
159170
visitor: Visitor
160171
) => {
161-
const children = visitElement(child, queue, visitor)
162-
// Flush changes (if any) that have been made to the context
163-
const prevMap = flushPrevContextMap()
164-
const prevStore = flushPrevContextStore()
165-
166-
visitChildren(children, queue, visitor)
167-
168-
// Restore context changes after children have been walked
169-
if (prevMap !== undefined) {
170-
restoreContextMap(prevMap)
171-
}
172-
173-
if (prevStore !== undefined) {
174-
restoreContextStore(prevStore)
172+
const start = Date.now()
173+
174+
while (traversalChildren.length > 0 && Date.now() - start <= 10) {
175+
const currChildren = traversalChildren[traversalChildren.length - 1]
176+
const currIndex = traversalIndex[traversalIndex.length - 1]++
177+
178+
if (currIndex < currChildren.length) {
179+
const element = currChildren[currIndex]
180+
const children = visitElement(element, queue, visitor)
181+
182+
traversalChildren.push(children)
183+
traversalIndex.push(0)
184+
traversalMap.push(flushPrevContextMap())
185+
traversalStore.push(flushPrevContextStore())
186+
} else {
187+
traversalChildren.pop()
188+
traversalIndex.pop()
189+
restoreContextMap(traversalMap.pop())
190+
restoreContextStore(traversalStore.pop())
191+
}
175192
}
176193
}
177194

178195
export const visitChildren = (
179-
children: AbstractElement[],
196+
init: AbstractElement[],
180197
queue: Frame[],
181198
visitor: Visitor
182199
) => {
183-
for (let i = 0, l = children.length; i < l; i++) {
184-
visitChild(children[i], queue, visitor)
200+
const traversalChildren: AbstractElement[][] = [init]
201+
const traversalIndex: number[] = [0]
202+
const traversalMap: Array<void | ContextMap> = [undefined]
203+
const traversalStore: Array<void | ContextEntry> = [undefined]
204+
205+
visitLoop(
206+
traversalChildren,
207+
traversalIndex,
208+
traversalMap,
209+
traversalStore,
210+
queue,
211+
visitor
212+
)
213+
214+
if (traversalChildren.length > 0) {
215+
queue.push({
216+
contextMap: getCurrentContextMap(),
217+
contextStore: getCurrentContextStore(),
218+
kind: 'frame.yield',
219+
thenable: makeImmediatePromise(),
220+
children: traversalChildren,
221+
index: traversalIndex,
222+
map: traversalMap,
223+
store: traversalStore
224+
})
185225
}
186226
}
227+
228+
export const resumeVisitChildren = (
229+
frame: YieldFrame,
230+
queue: Frame[],
231+
visitor: Visitor
232+
) => {
233+
setCurrentIdentity(null)
234+
setCurrentContextMap(frame.contextMap)
235+
setCurrentContextMap(frame.contextStore)
236+
237+
visitLoop(frame.children, frame.index, frame.map, frame.store, queue, visitor)
238+
}

0 commit comments

Comments
 (0)