47
}
48
}
49
50
+const FALLBACK_THROTTLE_MS = 300;
51
+
52
export function completeBoundary(suspenseBoundaryID, contentID) {
51
- const contentNode = document.getElementById(contentID);
52
- if (!contentNode) {
53
+ const contentNodeOuter = document.getElementById(contentID);
54
+ if (!contentNodeOuter) {
55
// If the client has failed hydration we may have already deleted the streaming
56
// segments. The server may also have emitted a complete instruction but cancelled
57
// the segment. Regardless we can ignore this case.
59
}
60
// We'll detach the content node so that regardless of what happens next we don't leave in the tree.
61
// This might also help by not causing recalcing each time we move a child from here to the target.
60
- contentNode.parentNode.removeChild(contentNode);
62
+ contentNodeOuter.parentNode.removeChild(contentNodeOuter);
63
64
// Find the fallback's first element.
63
- const suspenseIdNode = document.getElementById(suspenseBoundaryID);
64
- if (!suspenseIdNode) {
65
+ const suspenseIdNodeOuter = document.getElementById(suspenseBoundaryID);
66
+ if (!suspenseIdNodeOuter) {
67
// The user must have already navigated away from this tree.
68
// E.g. because the parent was hydrated. That's fine there's nothing to do
69
// but we have to make sure that we already deleted the container node.
70
return;
71
}
70
- // Find the boundary around the fallback. This is always the previous node.
71
- const suspenseNode = suspenseIdNode.previousSibling;
72
73
- // Clear all the existing children. This is complicated because
74
- // there can be embedded Suspense boundaries in the fallback.
75
- // This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.
76
- // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
77
- // They never hydrate anyway. However, currently we support incrementally loading the fallback.
78
- const parentInstance = suspenseNode.parentNode;
79
- let node = suspenseNode.nextSibling;
80
- let depth = 0;
81
- do {
82
- if (node && node.nodeType === COMMENT_NODE) {
83
- const data = node.data;
84
- if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
85
- if (depth === 0) {
86
- break;
87
- } else {
88
- depth--;
89
- }
90
- } else if (
91
- data === SUSPENSE_START_DATA ||
92
- data === SUSPENSE_PENDING_START_DATA ||
93
- data === SUSPENSE_FALLBACK_START_DATA ||
94
- data === ACTIVITY_START_DATA
95
- ) {
96
- depth++;
73
+ function revealCompletedBoundaries() {
74
+ window['$RT'] = performance.now();
75
+ const batch = window['$RB'];
76
+ window['$RB'] = [];
77
+ for (let i = 0; i < batch.length; i += 2) {
78
+ const suspenseIdNode = batch[i];
79
+ const contentNode = batch[i + 1];
80
+
81
+ // Clear all the existing children. This is complicated because
82
+ // there can be embedded Suspense boundaries in the fallback.
83
+ // This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.
84
+ // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
85
+ // They never hydrate anyway. However, currently we support incrementally loading the fallback.
86
+ const parentInstance = suspenseIdNode.parentNode;
87
+ if (!parentInstance) {
88
+ // We may have client-rendered this boundary already. Skip it.
89
+ continue;
90
}
98
- }
91
100
- const nextNode = node.nextSibling;
101
- parentInstance.removeChild(node);
102
- node = nextNode;
103
- } while (node);
92
+ // Find the boundary around the fallback. This is always the previous node.
93
+ const suspenseNode = suspenseIdNode.previousSibling;
94
105
- const endOfBoundary = node;
95
+ let node = suspenseIdNode;
96
+ let depth = 0;
97
+ do {
98
+ if (node && node.nodeType === COMMENT_NODE) {
99
+ const data = node.data;
100
+ if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
101
+ if (depth === 0) {
102
+ break;
103
+ } else {
104
+ depth--;
105
+ }
106
+ } else if (
107
+ data === SUSPENSE_START_DATA ||
108
+ data === SUSPENSE_PENDING_START_DATA ||
109
+ data === SUSPENSE_FALLBACK_START_DATA ||
110
+ data === ACTIVITY_START_DATA
111
+ ) {
112
+ depth++;
113
+ }
114
+ }
115
+
116
+ const nextNode = node.nextSibling;
117
+ parentInstance.removeChild(node);
118
+ node = nextNode;
119
+ } while (node);
120
+
121
+ const endOfBoundary = node;
122
+
123
+ // Insert all the children from the contentNode between the start and end of suspense boundary.
124
+ while (contentNode.firstChild) {
125
+ parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
126
+ }
127
107
- // Insert all the children from the contentNode between the start and end of suspense boundary.
108
- while (contentNode.firstChild) {
109
- parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
128
+ suspenseNode.data = SUSPENSE_START_DATA;
129
+ if (suspenseNode['_reactRetry']) {
130
+ suspenseNode['_reactRetry']();
131
+ }
132
+ }
133
}
134
112
- suspenseNode.data = SUSPENSE_START_DATA;
135
+ // Queue this boundary for the next batch
136
+ window['$RB'].push(suspenseIdNodeOuter, contentNodeOuter);
137
114
- if (suspenseNode['_reactRetry']) {
115
- suspenseNode['_reactRetry']();
138
+ if (window['$RB'].length === 2) {
139
+ // This is the first time we've pushed to the batch. We need to schedule a callback
140
+ // to flush the batch. This is delayed by the throttle heuristic.
141
+ const globalMostRecentFallbackTime =
142
+ typeof window['$RT'] !== 'number' ? 0 : window['$RT'];
143
+ const msUntilTimeout =
144
+ globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - performance.now();
145
+ // We always schedule the flush in a timer even if it's very low or negative to allow
146
+ // for multiple completeBoundary calls that are already queued to have a chance to
147
+ // make the batch.
148
+ setTimeout(revealCompletedBoundaries, msUntilTimeout);
149
}
150
}
151