diff --git a/packages/react-reconciler/src/ReactFiberExpirationTime.js b/packages/react-reconciler/src/ReactFiberExpirationTime.js index 8eeb26e781..d79c795b1a 100644 --- a/packages/react-reconciler/src/ReactFiberExpirationTime.js +++ b/packages/react-reconciler/src/ReactFiberExpirationTime.js @@ -48,25 +48,3 @@ function computeExpirationBucket( ); } exports.computeExpirationBucket = computeExpirationBucket; - -// Given the current clock time and an expiration time, returns the -// relative expiration time. Possible values include NoWork, Sync, Task, and -// Never. All other values represent an async expiration time. -function relativeExpirationTime( - currentTime: ExpirationTime, - expirationTime: ExpirationTime, -): ExpirationTime { - switch (expirationTime) { - case NoWork: - case Sync: - case Task: - case Never: - return expirationTime; - } - const delta = expirationTime - currentTime; - if (delta <= 0) { - return Task; - } - return msToExpirationTime(delta); -} -exports.relativeExpirationTime = relativeExpirationTime; diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index 1288fee9bb..6c6b894a4c 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -11,8 +11,10 @@ 'use strict'; import type {Fiber} from 'ReactFiber'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; const {createHostRootFiber} = require('ReactFiber'); +const {NoWork} = require('ReactFiberExpirationTime'); export type FiberRoot = { // Any additional information from the host associated with this root. @@ -21,15 +23,22 @@ export type FiberRoot = { pendingChildren: any, // The currently active root fiber. This is the mutable root of the tree. current: Fiber, - // Determines if this root has already been added to the schedule for work. - isScheduled: boolean, - // The work schedule is a linked list. - nextScheduledRoot: FiberRoot | null, + // Remaining expiration time on this root. + remainingExpirationTime: ExpirationTime, + // Determines if this root can be committed. + isReadyForCommit: boolean, + // A finished work-in-progress HostRoot that's ready to be committed. + // TODO: The reason this is separate from isReadyForCommit is because the + // FiberRoot concept will likely be lifted out of the reconciler and into + // the renderer. + finishedWork: Fiber | null, // Top context object, used by renderSubtreeIntoContainer context: Object | null, pendingContext: Object | null, // Determines if we should attempt to hydrate on the initial mount +hydrate: boolean, + // Linked-list of roots + nextScheduledRoot: FiberRoot | null, }; exports.createFiberRoot = function( @@ -43,11 +52,13 @@ exports.createFiberRoot = function( current: uninitializedFiber, containerInfo: containerInfo, pendingChildren: null, - isScheduled: false, - nextScheduledRoot: null, + remainingExpirationTime: NoWork, + isReadyForCommit: false, + finishedWork: null, context: null, pendingContext: null, hydrate, + nextScheduledRoot: null, }; uninitializedFiber.stateNode = root; return root; diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index c163c1028b..da726ef51e 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -60,7 +60,6 @@ var { Never, msToExpirationTime, computeExpirationBucket, - relativeExpirationTime, } = require('ReactFiberExpirationTime'); var {AsyncUpdates} = require('ReactTypeOfInternalContext'); @@ -151,8 +150,6 @@ if (__DEV__) { }; } -var timeHeuristicForUnitOfWork = 1; - module.exports = function( config: HostConfig, ) { @@ -199,36 +196,17 @@ module.exports = function( // updates in sync mode.) let expirationContext: ExpirationTime = NoWork; - // Keeps track of whether we're currently in a work loop. - let isPerformingWork: boolean = false; - - // Keeps track of whether the current deadline has expired. - let deadlineHasExpired: boolean = false; - - // Keeps track of whether we should should batch sync updates. - let isBatchingUpdates: boolean = false; - - // This is needed for the weird case where the initial mount is synchronous - // even inside batchedUpdates :( - let isUnbatchingUpdates: boolean = false; + let isWorking: boolean = false; // The next work in progress fiber that we're currently working on. let nextUnitOfWork: Fiber | null = null; + let nextRoot: FiberRoot | null = null; // The time at which we're currently rendering work. let nextRenderExpirationTime: ExpirationTime = NoWork; // The next fiber with an effect that we're currently committing. let nextEffect: Fiber | null = null; - let pendingCommit: Fiber | null = null; - - // Linked list of roots with scheduled work on them. - let nextScheduledRoot: FiberRoot | null = null; - let lastScheduledRoot: FiberRoot | null = null; - - // Keep track of which host environment callbacks are scheduled. - let isCallbackScheduled: boolean = false; - // Keep track of which fibers have captured an error that need to be handled. // Work is removed from this collection after componentDidCatch is called. let capturedErrors: Map | null = null; @@ -245,11 +223,6 @@ module.exports = function( let isCommitting: boolean = false; let isUnmounting: boolean = false; - // Use these to prevent an infinite loop of nested updates - const NESTED_UPDATE_LIMIT = 1000; - let nestedUpdateCount: number = 0; - let nextRenderedTree: FiberRoot | null = null; - function resetContextStack() { // Reset the stack reset(); @@ -258,72 +231,6 @@ module.exports = function( resetHostContainer(); } - function resetNextUnitOfWork() { - // Clear out roots with no more work on them, or if they have uncaught errors - while ( - nextScheduledRoot !== null && - nextScheduledRoot.current.expirationTime === NoWork - ) { - // Unschedule this root. - nextScheduledRoot.isScheduled = false; - // Read the next pointer now. - // We need to clear it in case this root gets scheduled again later. - const next = nextScheduledRoot.nextScheduledRoot; - nextScheduledRoot.nextScheduledRoot = null; - // Exit if we cleared all the roots and there's no work to do. - if (nextScheduledRoot === lastScheduledRoot) { - nextScheduledRoot = null; - lastScheduledRoot = null; - nextRenderExpirationTime = NoWork; - return null; - } - // Continue with the next root. - // If there's no work on it, it will get unscheduled too. - nextScheduledRoot = next; - } - - let root = nextScheduledRoot; - let earliestExpirationRoot = null; - let earliestExpirationTime = NoWork; - while (root !== null) { - if ( - root.current.expirationTime !== NoWork && - (earliestExpirationTime === NoWork || - earliestExpirationTime > root.current.expirationTime) - ) { - earliestExpirationTime = root.current.expirationTime; - earliestExpirationRoot = root; - } - // We didn't find anything to do in this root, so let's try the next one. - root = root.nextScheduledRoot; - } - if (earliestExpirationRoot !== null) { - nextRenderExpirationTime = earliestExpirationTime; - // Before we start any new work, let's make sure that we have a fresh - // stack to work from. - // TODO: This call is buried a bit too deep. It would be nice to have - // a single point which happens right before any new work and - // unfortunately this is it. - resetContextStack(); - - nextUnitOfWork = createWorkInProgress( - earliestExpirationRoot.current, - earliestExpirationTime, - ); - if (earliestExpirationRoot !== nextRenderedTree) { - // We've switched trees. Reset the nested update counter. - nestedUpdateCount = 0; - nextRenderedTree = earliestExpirationRoot; - } - return; - } - - nextRenderExpirationTime = NoWork; - nextUnitOfWork = null; - nextRenderedTree = null; - return; - } - function commitAllHostEffects() { while (nextEffect !== null) { if (__DEV__) { @@ -430,30 +337,25 @@ module.exports = function( } } - function commitAllWork(finishedWork: Fiber) { + function commitRoot(finishedWork: Fiber): ExpirationTime { // We keep track of this so that captureError can collect any boundaries // that capture an error during the commit phase. The reason these aren't // local to this function is because errors that occur during cWU are // captured elsewhere, to prevent the unmount from being interrupted. + isWorking = true; isCommitting = true; if (__DEV__) { startCommitTimer(); } - pendingCommit = null; - const root: FiberRoot = (finishedWork.stateNode: any); + const root: FiberRoot = finishedWork.stateNode; invariant( root.current !== finishedWork, 'Cannot commit the same tree as before. This is probably a bug ' + 'related to the return field. This error is likely caused by a bug ' + 'in React. Please file an issue.', ); - - if (nextRenderExpirationTime <= mostRecentCurrentTime) { - // Keep track of the number of iterations to prevent an infinite - // update loop. - nestedUpdateCount++; - } + root.isReadyForCommit = false; // Reset this to null before calling lifecycles ReactCurrentOwner.current = null; @@ -565,6 +467,7 @@ module.exports = function( } isCommitting = false; + isWorking = false; if (__DEV__) { stopCommitLifeCyclesTimer(); stopCommitTimer(); @@ -583,9 +486,20 @@ module.exports = function( commitPhaseBoundaries = null; } - // This tree is done. Reset the unit of work pointer to the root that - // expires soonest. If there's no work left, the pointer is set to null. - resetNextUnitOfWork(); + if (firstUncaughtError !== null) { + const error = firstUncaughtError; + firstUncaughtError = null; + onUncaughtError(error); + } + + const remainingTime = root.current.expirationTime; + + if (remainingTime === NoWork) { + capturedErrors = null; + failedBoundaries = null; + } + + return remainingTime; } function resetExpirationTime( @@ -702,10 +616,9 @@ module.exports = function( workInProgress = returnFiber; continue; } else { - // We've reached the root. Mark the root as pending commit. Depending - // on how much time we have left, we'll either commit it now or in - // the next frame. - pendingCommit = workInProgress; + // We've reached the root. + const root: FiberRoot = workInProgress.stateNode; + root.isReadyForCommit = true; return null; } } @@ -780,26 +693,45 @@ module.exports = function( return next; } - function performDeferredWork(deadline: Deadline) { - performWork(Never, deadline); + function workLoop(expirationTime: ExpirationTime) { + if (capturedErrors !== null) { + // If there are unhandled errors, switch to the slow work loop. + // TODO: How to avoid this check in the fast path? Maybe the renderer + // could keep track of which roots have unhandled errors and call a + // forked version of renderRoot. + slowWorkLoopThatChecksForFailedWork(expirationTime); + return; + } + if ( + nextRenderExpirationTime === NoWork || + nextRenderExpirationTime > expirationTime + ) { + return; + } + + if (nextRenderExpirationTime <= mostRecentCurrentTime) { + // Flush all expired work. + while (nextUnitOfWork !== null) { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork); + } + } else { + // Flush asynchronous work until the deadline runs out of time. + while (nextUnitOfWork !== null && !shouldYield()) { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork); + } + } } - function handleCommitPhaseErrors() { - // This is a special work loop for handling commit phase errors. It's - // similar to the syncrhonous work loop, but does an additional check on - // each fiber to see if it's an error boundary with an unhandled error. If - // so, it uses a forked version of performUnitOfWork that unmounts the - // failed subtree. - // - // The loop stops once the children have unmounted and error lifecycles are - // called. Then we return to the regular flow. - + function slowWorkLoopThatChecksForFailedWork(expirationTime: ExpirationTime) { if ( - capturedErrors !== null && - capturedErrors.size > 0 && - nextRenderExpirationTime !== NoWork && - nextRenderExpirationTime <= mostRecentCurrentTime + nextRenderExpirationTime === NoWork || + nextRenderExpirationTime > expirationTime ) { + return; + } + + if (nextRenderExpirationTime <= mostRecentCurrentTime) { + // Flush all expired work. while (nextUnitOfWork !== null) { if (hasCapturedError(nextUnitOfWork)) { // Use a forked version of performUnitOfWork @@ -807,158 +739,25 @@ module.exports = function( } else { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } - if (nextUnitOfWork === null) { - invariant( - pendingCommit !== null, - 'Should have a pending commit. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - // We just completed a root. Commit it now. - commitAllWork(pendingCommit); - if ( - capturedErrors === null || - capturedErrors.size === 0 || - nextRenderExpirationTime === NoWork || - nextRenderExpirationTime > mostRecentCurrentTime - ) { - // There are no more unhandled errors. We can exit this special - // work loop. If there's still additional work, we'll perform it - // using one of the normal work loops. - break; - } - // The commit phase produced additional errors. Continue working. + } + } else { + // Flush asynchronous work until the deadline runs out of time. + while (nextUnitOfWork !== null && !shouldYield()) { + if (hasCapturedError(nextUnitOfWork)) { + // Use a forked version of performUnitOfWork + nextUnitOfWork = performFailedUnitOfWork(nextUnitOfWork); + } else { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } } } } - function workLoop( - minExpirationTime: ExpirationTime, - deadline: Deadline | null, - ) { - loop: do { - if (pendingCommit !== null) { - commitAllWork(pendingCommit); - handleCommitPhaseErrors(); - } else if (nextUnitOfWork === null) { - resetNextUnitOfWork(); - } - - if ( - nextRenderExpirationTime === NoWork || - nextRenderExpirationTime > minExpirationTime - ) { - return; - } - - if (nextRenderExpirationTime <= mostRecentCurrentTime) { - // Flush all expired work. - while (nextUnitOfWork !== null) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork); - if (nextUnitOfWork === null) { - invariant( - pendingCommit !== null, - 'Should have a pending commit. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - // We just completed a root. Commit it now. - commitAllWork(pendingCommit); - // Clear any errors that were scheduled during the commit phase. - handleCommitPhaseErrors(); - // The render time may have changed. Check again. - if ( - nextRenderExpirationTime === NoWork || - nextRenderExpirationTime > minExpirationTime || - nextRenderExpirationTime > mostRecentCurrentTime - ) { - // We've completed all the expired work. - break; - } - } - } - } else if (deadline !== null) { - // Flush asynchronous work until the deadline runs out of time. - while (nextUnitOfWork !== null && !deadlineHasExpired) { - if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork); - // In a deferred work batch, iff nextUnitOfWork returns null, we just - // completed a root and a pendingCommit exists. Logically, we could - // omit either of the checks in the following condition, but we need - // both to satisfy Flow. - if (nextUnitOfWork === null) { - invariant( - pendingCommit !== null, - 'Should have a pending commit. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - // We just completed a root. If we have time, commit it now. - // Otherwise, we'll commit it in the next frame. - if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { - commitAllWork(pendingCommit); - // Clear any errors that were scheduled during the commit phase. - handleCommitPhaseErrors(); - // The render time may have changed. Check again. - if ( - nextRenderExpirationTime === NoWork || - nextRenderExpirationTime > minExpirationTime || - nextRenderExpirationTime <= mostRecentCurrentTime - ) { - // We've completed all the async work. - break; - } - } else { - deadlineHasExpired = true; - } - } - } else { - deadlineHasExpired = true; - } - } - } - - // There might be work left. Depending on the priority, we should - // either perform it now or schedule a callback to perform it later. - const currentTime = recalculateCurrentTime(); - switch (relativeExpirationTime(currentTime, nextRenderExpirationTime)) { - case NoWork: - // No work left. We can exit. - break loop; - case Sync: - case Task: - // We have remaining synchronous or task work. Keep performing it, - // regardless of whether we're inside a callback. - if (nextRenderExpirationTime <= minExpirationTime) { - // Sometimes minExpirationTime is Sync, which means we should skip - // task work. - continue loop; - } - break loop; - default: - // We have remaining async work. - if (deadline === null) { - // We're not inside a callback. Exit and perform the work during - // the next callback. - break loop; - } - // We are inside a callback. - if ( - !deadlineHasExpired && - nextRenderExpirationTime <= minExpirationTime - ) { - // We still have time. Keep working. - continue loop; - } - // We've run out of time. Exit. - break loop; - } - } while (true); - } - - function performWorkCatchBlock( + function renderRootCatchBlock( + root: FiberRoot, failedWork: Fiber, boundary: Fiber, - minExpirationTime: ExpirationTime, - deadline: Deadline | null, + expirationTime: ExpirationTime, ) { // We're going to restart the error boundary that captured the error. // Conceptually, we're unwinding the stack. We need to unwind the @@ -973,37 +772,53 @@ module.exports = function( nextUnitOfWork = performFailedUnitOfWork(boundary); // Continue working. - workLoop(minExpirationTime, deadline); + workLoop(expirationTime); } - function performWork( - minExpirationTime: ExpirationTime, - deadline: Deadline | null, - ) { + function renderRoot( + root: FiberRoot, + expirationTime: ExpirationTime, + ): Fiber | null { if (__DEV__) { startWorkLoopTimer(); } invariant( - !isPerformingWork, - 'performWork was called recursively. This error is likely caused ' + + !isWorking, + 'renderRoot was called recursively. This error is likely caused ' + 'by a bug in React. Please file an issue.', ); - isPerformingWork = true; + isWorking = true; - nestedUpdateCount = 0; + // We're about to mutate the work-in-progress tree. If the root was pending + // commit, it no longer is: we'll need to complete it again. + root.isReadyForCommit = false; + + // Check if we're starting from a fresh stack, or if we're resuming from + // previously yielded work. + if ( + root !== nextRoot || + expirationTime !== nextRenderExpirationTime || + nextUnitOfWork === null + ) { + // This is a restart. Reset the stack. + resetContextStack(); + nextRoot = root; + nextRenderExpirationTime = expirationTime; + nextUnitOfWork = createWorkInProgress(nextRoot.current, expirationTime); + } let didError = false; let error = null; if (__DEV__) { - invokeGuardedCallback(null, workLoop, null, minExpirationTime, deadline); + invokeGuardedCallback(null, workLoop, null, expirationTime); if (hasCaughtError()) { didError = true; error = clearCaughtError(); } } else { try { - workLoop(minExpirationTime, deadline); + workLoop(expirationTime); } catch (e) { didError = true; error = e; @@ -1046,12 +861,12 @@ module.exports = function( if (__DEV__) { invokeGuardedCallback( null, - performWorkCatchBlock, + renderRootCatchBlock, null, + root, failedWork, boundary, - minExpirationTime, - deadline, + expirationTime, ); if (hasCaughtError()) { didError = true; @@ -1060,12 +875,7 @@ module.exports = function( } } else { try { - performWorkCatchBlock( - failedWork, - boundary, - minExpirationTime, - deadline, - ); + renderRootCatchBlock(root, failedWork, boundary, expirationTime); error = null; } catch (e) { didError = true; @@ -1077,39 +887,22 @@ module.exports = function( break; } - // If we're inside a callback, set this to false, since we just flushed it. - if (deadline !== null) { - isCallbackScheduled = false; - } - // If there's remaining async work, make sure we schedule another callback. - if ( - nextRenderExpirationTime > mostRecentCurrentTime && - !isCallbackScheduled - ) { - scheduleDeferredCallback(performDeferredWork); - isCallbackScheduled = true; - } - - const errorToThrow = firstUncaughtError; + const uncaughtError = firstUncaughtError; // We're done performing work. Time to clean up. - isPerformingWork = false; - deadlineHasExpired = false; + isWorking = false; didFatal = false; firstUncaughtError = null; - capturedErrors = null; - failedBoundaries = null; - nextRenderedTree = null; - nestedUpdateCount = 0; if (__DEV__) { stopWorkLoopTimer(); } - // It's safe to throw any unhandled errors. - if (errorToThrow !== null) { - throw errorToThrow; + if (uncaughtError !== null) { + onUncaughtError(uncaughtError); } + + return root.isReadyForCommit ? root.current.alternate : null; } // Returns the boundary that captured the error, or null if the error is ignored @@ -1310,9 +1103,6 @@ module.exports = function( return; case HostRoot: if (firstUncaughtError === null) { - // If this is the host container, we treat it as a no-op error - // boundary. We'll throw the first uncaught error once it's safe to - // do so, at the end of the batch. firstUncaughtError = capturedError.error; } return; @@ -1354,25 +1144,6 @@ module.exports = function( } } - function scheduleRoot(root: FiberRoot, expirationTime: ExpirationTime) { - if (expirationTime === NoWork) { - return; - } - - if (!root.isScheduled) { - root.isScheduled = true; - if (lastScheduledRoot) { - // Schedule ourselves to the end. - lastScheduledRoot.nextScheduledRoot = root; - lastScheduledRoot = root; - } else { - // We're the only work scheduled. - nextScheduledRoot = root; - lastScheduledRoot = root; - } - } - } - function computeAsyncExpiration() { // Given the current clock time, returns an expiration time. We use rounding // to batch like updates together. @@ -1388,7 +1159,7 @@ module.exports = function( if (expirationContext !== NoWork) { // An explicit expiration context was set; expirationTime = expirationContext; - } else if (isPerformingWork) { + } else if (isWorking) { if (isCommitting) { // Updates that occur during the commit phase should have sync priority // by default. @@ -1412,7 +1183,8 @@ module.exports = function( if ( expirationTime === Sync && - (isBatchingUpdates || (isUnbatchingUpdates && isCommitting)) + isBatchingUpdates && + (!isUnbatchingUpdates || isCommitting) ) { // If we're in a batch, downgrade sync to task. expirationTime = Task; @@ -1433,24 +1205,6 @@ module.exports = function( recordScheduleUpdate(); } - if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { - didFatal = true; - invariant( - false, - 'Maximum update depth exceeded. This can happen when a ' + - 'component repeatedly calls setState inside componentWillUpdate or ' + - 'componentDidUpdate. React limits the number of nested updates to ' + - 'prevent infinite loops.', - ); - } - - if (!isPerformingWork && expirationTime <= nextRenderExpirationTime) { - // We must reset the current unit of work pointer so that we restart the - // search from the root during the next tick, in case there is now higher - // priority work somewhere earlier than before. - nextUnitOfWork = null; - } - if (__DEV__) { if (!isErrorRecovery && fiber.tag === ClassComponent) { const instance = fiber.stateNode; @@ -1459,19 +1213,13 @@ module.exports = function( } let node = fiber; - let shouldContinue = true; - while (node !== null && shouldContinue) { - // Walk the parent path to the root and update each node's expiration - // time. Once we reach a node whose expiration matches (and whose - // alternate's expiration matches) we can exit safely knowing that the - // rest of the path is correct. - shouldContinue = false; + while (node !== null) { + // Walk the parent path to the root and update each node's + // expiration time. if ( node.expirationTime === NoWork || node.expirationTime > expirationTime ) { - // Expiration time did not match. Update and keep going. - shouldContinue = true; node.expirationTime = expirationTime; } if (node.alternate !== null) { @@ -1479,44 +1227,23 @@ module.exports = function( node.alternate.expirationTime === NoWork || node.alternate.expirationTime > expirationTime ) { - // Expiration time did not match. Update and keep going. - shouldContinue = true; node.alternate.expirationTime = expirationTime; } } if (node.return === null) { if (node.tag === HostRoot) { const root: FiberRoot = (node.stateNode: any); - scheduleRoot(root, expirationTime); - if (!isPerformingWork) { - switch (expirationTime) { - case Sync: - if (isUnbatchingUpdates) { - // We're inside unbatchedUpdates, which is inside either - // batchedUpdates or a lifecycle. We should only flush - // synchronous work, not task work. - performWork(Sync, null); - } else { - // Flush both synchronous and task work. - performWork(Task, null); - } - break; - case Task: - invariant( - isBatchingUpdates, - 'Task updates can only be scheduled as a nested update or ' + - 'inside batchedUpdates. This error is likely caused by a ' + - 'bug in React. Please file an issue.', - ); - break; - default: - // This update is async. Schedule a callback. - if (!isCallbackScheduled) { - scheduleDeferredCallback(performDeferredWork); - isCallbackScheduled = true; - } - } + if ( + !isWorking && + root === nextRoot && + expirationTime <= nextRenderExpirationTime + ) { + // This is an interruption. Restart the root from the top. + nextRoot = null; + nextUnitOfWork = null; + nextRenderExpirationTime = NoWork; } + requestWork(root, expirationTime); } else { if (__DEV__) { if (!isErrorRecovery && fiber.tag === ClassComponent) { @@ -1541,55 +1268,6 @@ module.exports = function( return mostRecentCurrentTime; } - function batchedUpdates(fn: (a: A) => R, a: A): R { - const previousIsBatchingUpdates = isBatchingUpdates; - isBatchingUpdates = true; - try { - return fn(a); - } finally { - isBatchingUpdates = previousIsBatchingUpdates; - // If we're not already inside a batch, we need to flush any task work - // that was created by the user-provided function. - if (!isPerformingWork && !isBatchingUpdates) { - performWork(Task, null); - } - } - } - - function unbatchedUpdates(fn: () => A): A { - const previousIsUnbatchingUpdates = isUnbatchingUpdates; - const previousIsBatchingUpdates = isBatchingUpdates; - // This is only true if we're nested inside batchedUpdates. - isUnbatchingUpdates = isBatchingUpdates; - isBatchingUpdates = false; - try { - return fn(); - } finally { - isBatchingUpdates = previousIsBatchingUpdates; - isUnbatchingUpdates = previousIsUnbatchingUpdates; - } - } - - function flushSync(batch: () => A): A { - const previousIsBatchingUpdates = isBatchingUpdates; - const previousExpirationContext = expirationContext; - isBatchingUpdates = true; - expirationContext = Sync; - try { - return batch(); - } finally { - isBatchingUpdates = previousIsBatchingUpdates; - expirationContext = previousExpirationContext; - - invariant( - !isPerformingWork, - 'flushSync was called from inside a lifecycle method. It cannot be ' + - 'called when React is already rendering.', - ); - performWork(Task, null); - } - } - function deferredUpdates(fn: () => A): A { const previousExpirationContext = expirationContext; expirationContext = computeAsyncExpiration(); @@ -1600,13 +1278,326 @@ module.exports = function( } } + function syncUpdates(fn: () => A): A { + const previousExpirationContext = expirationContext; + expirationContext = Sync; + try { + return fn(); + } finally { + expirationContext = previousExpirationContext; + } + } + + // TODO: Everything below this is written as if it has been lifted to the + // renderers. I'll do this in a follow-up. + + // Linked-list of roots + let firstScheduledRoot: FiberRoot | null = null; + let lastScheduledRoot: FiberRoot | null = null; + + let isCallbackScheduled: boolean = false; + let isRendering: boolean = false; + let nextFlushedRoot: FiberRoot | null = null; + let nextFlushedExpirationTime: ExpirationTime = NoWork; + let deadlineDidExpire: boolean = false; + let hasUnhandledError: boolean = false; + let unhandledError: mixed | null = null; + let deadline: Deadline | null = null; + + let isBatchingUpdates: boolean = false; + let isUnbatchingUpdates: boolean = false; + + // Use these to prevent an infinite loop of nested updates + const NESTED_UPDATE_LIMIT = 1000; + let nestedUpdateCount: number = 0; + + const timeHeuristicForUnitOfWork = 1; + + // requestWork is called by the scheduler whenever a root receives an update. + // It's up to the renderer to call renderRoot at some point in the future. + function requestWork(root: FiberRoot, expirationTime: ExpirationTime) { + if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { + invariant( + false, + 'Maximum update depth exceeded. This can happen when a ' + + 'component repeatedly calls setState inside componentWillUpdate or ' + + 'componentDidUpdate. React limits the number of nested updates to ' + + 'prevent infinite loops.', + ); + } + + // Check if this root is already part of the schedule. + if (root.remainingExpirationTime === NoWork) { + // This root is not already scheduled. Add it. + root.remainingExpirationTime = expirationTime; + if (lastScheduledRoot === null) { + firstScheduledRoot = lastScheduledRoot = root; + } else { + lastScheduledRoot.nextScheduledRoot = root; + lastScheduledRoot = root; + } + } else { + // This root is already scheduled, but its priority may have increased. + const remainingExpirationTime = root.remainingExpirationTime; + if ( + remainingExpirationTime === NoWork || + expirationTime < remainingExpirationTime + ) { + // Update the priority. + root.remainingExpirationTime = expirationTime; + } + } + + // If we're not already rendering, schedule work to flush now (if it's + // sync) or later (if it's async). + if (!isRendering) { + // TODO: Remove distinction between sync and task. Maybe we can remove + // these magic numbers entirely by always comparing to the current time? + if (expirationTime === Sync) { + if (isUnbatchingUpdates) { + performWork(Sync, null); + } else { + performWork(Task, null); + } + } else if (!isCallbackScheduled) { + isCallbackScheduled = true; + scheduleDeferredCallback(flushAsyncWork); + } + } + } + + function findHighestPriorityRoot() { + let highestPriorityWork = NoWork; + let highestPriorityRoot = null; + + let previousScheduledRoot = null; + let root = firstScheduledRoot; + while (root !== null) { + const remainingExpirationTime = root.remainingExpirationTime; + if (remainingExpirationTime === NoWork) { + // If this root no longer has work, remove it from the scheduler. + let next = root.nextScheduledRoot; + root.nextScheduledRoot = null; + if (previousScheduledRoot === null) { + firstScheduledRoot = next; + } else { + previousScheduledRoot.nextScheduledRoot = next; + } + if (next === null) { + lastScheduledRoot = null; + } + root = next; + continue; + } else if ( + highestPriorityWork === NoWork || + remainingExpirationTime < highestPriorityWork + ) { + // Update the priority, if it's higher + highestPriorityWork = remainingExpirationTime; + highestPriorityRoot = root; + } + previousScheduledRoot = root; + root = root.nextScheduledRoot; + } + + // If the next root is the same as the previous root, this is a nested + // update. To prevent an infinite loop, increment the nested update count. + const previousFlushedRoot = nextFlushedRoot; + if ( + previousFlushedRoot !== null && + previousFlushedRoot === highestPriorityRoot + ) { + nestedUpdateCount++; + } else { + // Reset whenever we switch roots. + nestedUpdateCount = 0; + } + nextFlushedRoot = highestPriorityRoot; + nextFlushedExpirationTime = highestPriorityWork; + } + + function flushAsyncWork(dl) { + performWork(NoWork, dl); + } + + function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) { + invariant( + !isRendering, + 'performWork was called recursively. This error is likely caused ' + + 'by a bug in React. Please file an issue.', + ); + + isRendering = true; + deadline = dl; + + // Keep working on roots until there's no more work, or until the we reach + // the deadlne. + findHighestPriorityRoot(); + while ( + nextFlushedRoot !== null && + nextFlushedExpirationTime !== NoWork && + (minExpirationTime === NoWork || + nextFlushedExpirationTime <= minExpirationTime) && + !deadlineDidExpire + ) { + // Check if this is async work or sync/expired work. + // TODO: Pass current time as argument to renderRoot, commitRoot + if (nextFlushedExpirationTime <= recalculateCurrentTime()) { + // Flush sync work. + let finishedWork = nextFlushedRoot.finishedWork; + if (finishedWork !== null) { + // This root is already complete. We can commit it. + nextFlushedRoot.finishedWork = null; + nextFlushedRoot.remainingExpirationTime = commitRoot(finishedWork); + } else { + nextFlushedRoot.finishedWork = null; + finishedWork = renderRoot(nextFlushedRoot, nextFlushedExpirationTime); + if (finishedWork !== null) { + // We've completed the root. Commit it. + nextFlushedRoot.remainingExpirationTime = commitRoot(finishedWork); + } + } + } else { + // Flush async work. + let finishedWork = nextFlushedRoot.finishedWork; + if (finishedWork !== null) { + // This root is already complete. We can commit it. + nextFlushedRoot.finishedWork = null; + nextFlushedRoot.remainingExpirationTime = commitRoot(finishedWork); + } else { + nextFlushedRoot.finishedWork = null; + finishedWork = renderRoot(nextFlushedRoot, nextFlushedExpirationTime); + if (finishedWork !== null) { + // We've completed the root. Check the deadline one more time + // before committing. + if (!shouldYield()) { + // Still time left. Commit the root. + nextFlushedRoot.remainingExpirationTime = commitRoot( + finishedWork, + ); + } else { + // There's no time left. Mark this root as complete. We'll come + // back and commit it later. + nextFlushedRoot.finishedWork = finishedWork; + } + } + } + } + // Find the next highest priority work. + findHighestPriorityRoot(); + } + + // We're done flushing work. Either we ran out of time in this callback, + // or there's no more work left with sufficient priority. + + // If we're inside a callback, set this to false since we just completed it. + if (deadline !== null) { + isCallbackScheduled = false; + } + // If there's work left over, schedule a new callback. + if (nextFlushedRoot !== null && !isCallbackScheduled) { + isCallbackScheduled = true; + scheduleDeferredCallback(flushAsyncWork); + } + + // Clean-up. + deadline = null; + deadlineDidExpire = false; + isRendering = false; + nestedUpdateCount = 0; + + if (hasUnhandledError) { + const error = unhandledError; + unhandledError = null; + hasUnhandledError = false; + throw error; + } + } + + // When working on async work, the reconciler asks the renderer if it should + // yield execution. For DOM, we implement this with requestIdleCallback. + function shouldYield() { + if (deadline === null) { + return false; + } + if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { + return false; + } + deadlineDidExpire = true; + return true; + } + + // TODO: Not happy about this hook. Conceptually, renderRoot should return a + // tuple of (isReadyForCommit, didError, error) + function onUncaughtError(error) { + invariant( + nextFlushedRoot !== null, + 'Should be working on a root. This error is likely caused by a bug in ' + + 'React. Please file an issue.', + ); + // Unschedule this root so we don't work on it again until there's + // another update. + nextFlushedRoot.remainingExpirationTime = NoWork; + if (!hasUnhandledError) { + hasUnhandledError = true; + unhandledError = error; + } + } + + // TODO: Batching should be implemented at the renderer level, not inside + // the reconciler. + function batchedUpdates(fn: (a: A) => R, a: A): R { + const previousIsBatchingUpdates = isBatchingUpdates; + isBatchingUpdates = true; + try { + return fn(a); + } finally { + isBatchingUpdates = previousIsBatchingUpdates; + if (!isBatchingUpdates && !isRendering) { + performWork(Task, null); + } + } + } + + // TODO: Batching should be implemented at the renderer level, not inside + // the reconciler. + function unbatchedUpdates(fn: () => A): A { + if (isBatchingUpdates && !isUnbatchingUpdates) { + isUnbatchingUpdates = true; + try { + return fn(); + } finally { + isUnbatchingUpdates = false; + } + } + return fn(); + } + + // TODO: Batching should be implemented at the renderer level, not within + // the reconciler. + function flushSync(fn: () => A): A { + const previousIsBatchingUpdates = isBatchingUpdates; + isBatchingUpdates = true; + try { + return syncUpdates(fn); + } finally { + isBatchingUpdates = previousIsBatchingUpdates; + invariant( + !isRendering, + 'flushSync was called from inside a lifecycle method. It cannot be ' + + 'called when React is already rendering.', + ); + performWork(Task, null); + } + } + return { - computeAsyncExpiration: computeAsyncExpiration, - computeExpirationForFiber: computeExpirationForFiber, - scheduleWork: scheduleWork, - batchedUpdates: batchedUpdates, - unbatchedUpdates: unbatchedUpdates, - flushSync: flushSync, - deferredUpdates: deferredUpdates, + computeAsyncExpiration, + computeExpirationForFiber, + scheduleWork, + batchedUpdates, + unbatchedUpdates, + flushSync, + deferredUpdates, }; }; diff --git a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap index 2e7dcb5371..9b83f7fd4b 100644 --- a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap +++ b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap @@ -6,10 +6,11 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = ` ⚛ AllLifecycles [mount] ⚛ AllLifecycles.componentWillMount ⚛ AllLifecycles.getChildContext - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) - ⚛ AllLifecycles.componentDidMount + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) + ⚛ AllLifecycles.componentDidMount // Update ⚛ (React Tree Reconciliation) @@ -18,17 +19,19 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = ` ⚛ AllLifecycles.shouldComponentUpdate ⚛ AllLifecycles.componentWillUpdate ⚛ AllLifecycles.getChildContext - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 2 Total) - ⚛ AllLifecycles.componentDidUpdate + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 2 Total) + ⚛ AllLifecycles.componentDidUpdate // Unmount ⚛ (React Tree Reconciliation) - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ AllLifecycles.componentWillUnmount - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ AllLifecycles.componentWillUnmount + ⚛ (Calling Lifecycle Methods: 0 Total) " `; @@ -40,44 +43,53 @@ exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduc ⚛ B [update] ⚛ A [update] ⚛ B [update] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 9 Total) - ⚛ (Calling Lifecycle Methods: 9 Total) - ⚛ A.componentDidUpdate - ⚛ B.componentDidUpdate + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 9 Total) + ⚛ (Calling Lifecycle Methods: 9 Total) + ⚛ A.componentDidUpdate + ⚛ B.componentDidUpdate // Because of deduplication, we don't know B was cascading, // but we should still see the warning for the commit phase. -⛔ (React Tree Reconciliation) Warning: There were cascading updates +⚛ (React Tree Reconciliation) ⚛ Parent [update] ⚛ A [update] ⚛ B [update] ⚛ A [update] ⚛ B [update] - ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update - ⚛ (Committing Host Effects: 9 Total) - ⚛ (Calling Lifecycle Methods: 9 Total) - ⚛ A.componentDidUpdate - ⚛ B.componentDidUpdate + +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update + ⚛ (Committing Host Effects: 9 Total) + ⚛ (Calling Lifecycle Methods: 9 Total) + ⚛ A.componentDidUpdate + ⚛ B.componentDidUpdate + +⚛ (React Tree Reconciliation) ⚛ B [update] - ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit - ⚛ (Committing Host Effects: 3 Total) - ⚛ (Calling Lifecycle Methods: 3 Total) - ⚛ B.componentDidUpdate + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 3 Total) + ⚛ (Calling Lifecycle Methods: 3 Total) + ⚛ B.componentDidUpdate " `; exports[`ReactDebugFiberPerf does not schedule an extra callback if setState is called during a synchronous commit phase 1`] = ` -"⛔ (React Tree Reconciliation) Warning: There were cascading updates +"⚛ (React Tree Reconciliation) ⚛ Component [mount] - ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) - ⛔ Component.componentDidMount Warning: Scheduled a cascading update + +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) + ⛔ Component.componentDidMount Warning: Scheduled a cascading update + +⚛ (React Tree Reconciliation) ⚛ Component [update] - ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) " `; @@ -87,18 +99,20 @@ exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascadi ⚛ Parent [mount] ⚛ NotCascading [mount] ⚛ NotCascading.componentWillMount - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) // Should not print a warning ⚛ (React Tree Reconciliation) ⚛ Parent [update] ⚛ NotCascading [update] ⚛ NotCascading.componentWillReceiveProps - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 2 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 2 Total) " `; @@ -107,23 +121,26 @@ exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = ` ⚛ (React Tree Reconciliation) ⚛ Parent [mount] ⚛ Child [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) // Update ⚛ (React Tree Reconciliation) ⚛ Parent [update] ⚛ Child [update] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 2 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 2 Total) // Unmount ⚛ (React Tree Reconciliation) - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) " `; @@ -144,9 +161,10 @@ exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = ` ⚛ Parent [mount] ⚛ B [mount] ⚛ Child [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) " `; @@ -154,35 +172,41 @@ exports[`ReactDebugFiberPerf measures deprioritized work 1`] = ` "// Flush the parent ⚛ (React Tree Reconciliation) ⚛ Parent [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) // Flush the child ⚛ (React Tree Reconciliation) ⚛ Child [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 3 Total) - ⚛ (Calling Lifecycle Methods: 2 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 3 Total) + ⚛ (Calling Lifecycle Methods: 2 Total) " `; exports[`ReactDebugFiberPerf recovers from caught errors 1`] = ` "// Stop on Baddie and restart from Boundary -⛔ (React Tree Reconciliation) Warning: There were cascading updates +⚛ (React Tree Reconciliation) ⚛ Parent [mount] ⛔ Boundary [mount] Warning: An error was thrown inside this error boundary ⚛ Parent [mount] ⚛ Baddie [mount] ⚛ Boundary [mount] - ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) + +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) + +⚛ (React Tree Reconciliation) ⚛ Boundary [update] ⚛ ErrorReport [mount] - ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) " `; @@ -191,17 +215,19 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = ` ⚛ (React Tree Reconciliation) ⚛ Parent [mount] ⚛ Baddie [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) // Will reconcile from a clean state ⚛ (React Tree Reconciliation) ⚛ Parent [mount] ⚛ Child [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) " `; @@ -210,9 +236,10 @@ exports[`ReactDebugFiberPerf skips parents during setState 1`] = ` ⚛ (React Tree Reconciliation) ⚛ A [update] ⚛ B [update] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 6 Total) - ⚛ (Calling Lifecycle Methods: 6 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 6 Total) + ⚛ (Calling Lifecycle Methods: 6 Total) " `; @@ -226,9 +253,10 @@ exports[`ReactDebugFiberPerf supports coroutines 1`] = ` ⚛ CoChild [mount] ⚛ Continuation [mount] ⚛ Continuation [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 3 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 3 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) " `; @@ -236,40 +264,49 @@ exports[`ReactDebugFiberPerf supports portals 1`] = ` "⚛ (React Tree Reconciliation) ⚛ Parent [mount] ⚛ Child [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) " `; exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = ` "// Should print a warning -⛔ (React Tree Reconciliation) Warning: There were cascading updates +⚛ (React Tree Reconciliation) ⚛ Parent [mount] ⚛ Cascading [mount] - ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) - ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update + +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) + ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update + +⚛ (React Tree Reconciliation) ⚛ Cascading [update] - ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 2 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 2 Total) " `; exports[`ReactDebugFiberPerf warns on cascading renders from top-level render 1`] = ` "// Rendering the first root -⛔ (React Tree Reconciliation) Warning: There were cascading updates +⚛ (React Tree Reconciliation) ⚛ Cascading [mount] - ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) - ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update - // Scheduling another root from componentDidMount + +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) + ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update + // Scheduling another root from componentDidMount + +⚛ (React Tree Reconciliation) ⚛ Child [mount] - ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) " `; diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index b60e29c156..513aadf84e 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -17,36 +17,36 @@ "gzip": 2364 }, "React-dev.js (FB_DEV)": { - "size": 43016, - "gzip": 11325 + "size": 43033, + "gzip": 11331 }, "React-prod.js (FB_PROD)": { - "size": 24831, - "gzip": 6707 + "size": 24848, + "gzip": 6713 }, "react-dom.development.js (UMD_DEV)": { - "size": 631104, - "gzip": 144685 + "size": 629078, + "gzip": 144076 }, "react-dom.production.min.js (UMD_PROD)": { - "size": 100038, - "gzip": 31532 + "size": 101020, + "gzip": 31886 }, "react-dom.development.js (NODE_DEV)": { - "size": 593383, - "gzip": 135883 + "size": 591361, + "gzip": 135305 }, "react-dom.production.min.js (NODE_PROD)": { - "size": 106647, - "gzip": 33435 + "size": 107254, + "gzip": 33591 }, "ReactDOMFiber-dev.js (FB_DEV)": { - "size": 590572, - "gzip": 135237 + "size": 588633, + "gzip": 134646 }, "ReactDOMFiber-prod.js (FB_PROD)": { - "size": 419911, - "gzip": 93354 + "size": 418791, + "gzip": 93017 }, "react-dom-test-utils.development.js (NODE_DEV)": { "size": 41743, @@ -73,96 +73,96 @@ "gzip": 4510 }, "ReactDOMUnstableNativeDependencies-dev.js (FB_DEV)": { - "size": 80190, - "gzip": 19882 + "size": 80207, + "gzip": 19890 }, "ReactDOMUnstableNativeDependencies-prod.js (FB_PROD)": { - "size": 65164, - "gzip": 15538 + "size": 65181, + "gzip": 15545 }, "react-dom-server.browser.development.js (UMD_DEV)": { - "size": 124910, - "gzip": 32260 + "size": 124902, + "gzip": 32255 }, "react-dom-server.browser.production.min.js (UMD_PROD)": { - "size": 15345, - "gzip": 5968 + "size": 15337, + "gzip": 5967 }, "react-dom-server.browser.development.js (NODE_DEV)": { - "size": 94830, - "gzip": 25139 + "size": 94822, + "gzip": 25134 }, "react-dom-server.browser.production.min.js (NODE_PROD)": { - "size": 15071, - "gzip": 5902 + "size": 15063, + "gzip": 5899 }, "ReactDOMServer-dev.js (FB_DEV)": { - "size": 94490, - "gzip": 25063 + "size": 94499, + "gzip": 25066 }, "ReactDOMServer-prod.js (FB_PROD)": { - "size": 42454, - "gzip": 11844 + "size": 42463, + "gzip": 11846 }, "react-dom-server.node.development.js (NODE_DEV)": { - "size": 97092, - "gzip": 25688 + "size": 97084, + "gzip": 25683 }, "react-dom-server.node.production.min.js (NODE_PROD)": { - "size": 15996, - "gzip": 6238 + "size": 15988, + "gzip": 6235 }, "react-art.development.js (UMD_DEV)": { - "size": 378408, - "gzip": 83017 + "size": 376390, + "gzip": 82374 }, "react-art.production.min.js (UMD_PROD)": { - "size": 82432, - "gzip": 25592 + "size": 83413, + "gzip": 25876 }, "react-art.development.js (NODE_DEV)": { - "size": 302763, - "gzip": 63846 + "size": 300749, + "gzip": 63250 }, "react-art.production.min.js (NODE_PROD)": { - "size": 53897, - "gzip": 16869 + "size": 54504, + "gzip": 17048 }, "ReactARTFiber-dev.js (FB_DEV)": { - "size": 301605, - "gzip": 63774 + "size": 299674, + "gzip": 63165 }, "ReactARTFiber-prod.js (FB_PROD)": { - "size": 225472, - "gzip": 46455 + "size": 224360, + "gzip": 46067 }, "ReactNativeFiber-dev.js (RN_DEV)": { - "size": 285948, - "gzip": 49225 + "size": 286193, + "gzip": 49282 }, "ReactNativeFiber-prod.js (RN_PROD)": { - "size": 223676, - "gzip": 38463 + "size": 223893, + "gzip": 38520 }, "react-test-renderer.development.js (NODE_DEV)": { - "size": 306451, - "gzip": 64239 + "size": 304437, + "gzip": 63667 }, "ReactTestRendererFiber-dev.js (FB_DEV)": { - "size": 305254, - "gzip": 64170 + "size": 303323, + "gzip": 63582 }, "react-test-renderer-shallow.development.js (NODE_DEV)": { "size": 9364, "gzip": 2335 }, "ReactShallowRenderer-dev.js (FB_DEV)": { - "size": 9020, - "gzip": 2254 + "size": 9037, + "gzip": 2262 }, "react-noop-renderer.development.js (NODE_DEV)": { - "size": 295660, - "gzip": 61436 + "size": 293646, + "gzip": 60851 }, "react-dom-server.development.js (UMD_DEV)": { "size": 120897, @@ -189,16 +189,16 @@ "gzip": 7520 }, "ReactNativeRTFiber-dev.js (RN_DEV)": { - "size": 217811, - "gzip": 36735 + "size": 218056, + "gzip": 36811 }, "ReactNativeRTFiber-prod.js (RN_PROD)": { - "size": 165325, - "gzip": 27464 + "size": 165542, + "gzip": 27540 }, "react-test-renderer.production.min.js (NODE_PROD)": { - "size": 55449, - "gzip": 17169 + "size": 56061, + "gzip": 17256 }, "react-test-renderer-shallow.production.min.js (NODE_PROD)": { "size": 4630, @@ -209,20 +209,20 @@ "gzip": 4241 }, "react-reconciler.development.js (NODE_DEV)": { - "size": 281332, - "gzip": 58301 + "size": 279318, + "gzip": 57691 }, "react-reconciler.production.min.js (NODE_PROD)": { - "size": 37658, - "gzip": 11762 + "size": 38320, + "gzip": 11959 }, "ReactNativeCSFiber-dev.js (RN_DEV)": { - "size": 210232, - "gzip": 34986 + "size": 210477, + "gzip": 35055 }, "ReactNativeCSFiber-prod.js (RN_PROD)": { - "size": 160320, - "gzip": 26275 + "size": 160537, + "gzip": 26346 } } } \ No newline at end of file