Skip to content

Commit

Permalink
Model Float on Hoistables semantics (#26106)
Browse files Browse the repository at this point in the history
## Hoistables

In the original implementation of Float, all hoisted elements were
treated like Resources. They had deduplication semantics and hydrated
based on a key. This made certain kinds of hoists very challenging such
as sequences of meta tags for `og:image:...` metadata. The reason is
each tag along is not dedupable based on only it's intrinsic properties.
two identical tags may need to be included and hoisted together with
preceding meta tags that describe a semantic object with a linear set of
html nodes.

It was clear that the concept of Browser Resources (stylesheets /
scripts / preloads) did not extend universally to all hositable tags
(title, meta, other links, etc...)

Additionally while Resources benefit from deduping they suffer an
inability to update because while we may have multiple rendered elements
that refer to a single Resource it isn't unambiguous which element owns
the props on the underlying resource. We could try merging props, but
that is still really hard to reason about for authors. Instead we
restrict Resource semantics to freezing the props at the time the
Resource is first constructed and warn if you attempt to render the same
Resource with different props via another rendered element or by
updating an existing element for that Resource.

This lack of updating restriction is however way more extreme than
necessary for instances that get hoisted but otherwise do not dedupe;
where there is a well defined DOM instance for each rendered element. We
should be able to update props on these instances.

Hoistable is a generalization of what Float tries to model for hoisting.
Instead of assuming every hoistable element is a Resource we now have
two distinct categories, hoistable elements and hoistable resources. As
one might guess the former has semantics that match regular Host
Components except the placement of the node is usually in the <head>.
The latter continues to behave how the original implementation of
HostResource behaved with the first iteration of Float

### Hoistable Element
On the server hoistable elements render just like regular tags except
the output is stored in special queues that can be emitted in the stream
earlier than they otherwise would be if rendered in place. This also
allow for instance the ability to render a hoistable before even
rendering the <html> tag because the queues for hoistable elements won't
flush until after we have flushed the preamble (`<DOCTYPE
html><html><head>`).

On the client, hoistable elements largely operate like HostComponents.
The most notable difference is in the hydration strategy. If we are
hydrating and encounter a hoistable element we will look for all tags in
the document that could potentially be a match and we check whether the
attributes match the props for this particular instance. We also do this
in the commit phase rather than the render phase. The reason hydration
can be done for HostComponents in render is the instance will be removed
from the document if hydration fails so mutating it in render is safe.
For hoistables the nodes are not in a hydration boundary (Root or
SuspenseBoundary at time of writing) and thus if hydration fails and we
may have an instance marked as bound to some Fiber when that Fiber never
commits. Moving the hydration matching to commit ensures we will always
succeed in pairing the hoisted DOM instance with a Fiber that has
committed.

### Hoistable Resource
On the server and client the semantics of Resources are largely the same
they just don't apply to title, meta, and most link tags anymore.
Resources hoist and dedupe via an `href` key and are ref counted. In a
future update we will add a garbage collector so we can clean up
Resources that no longer have any references

## `<style>` support
In earlier implementations there was no support for <style> tags. This
PR adds support for treating `<style href="..."
precedence="...">...</style>` as a Resource analagous to `<link
rel="stylesheet" href="..." precedence="..." />`

It may seem odd at first to require an href to get Resource semantics
for a style tag. The rationale is that these are for inlining of actual
external stylesheets as an optimization and for URI like scoping of
inline styles for css-in-js libraries. The href indicates that the key
space for `<style>` and `<link rel="stylesheet" />` Resources is shared.
and the precedence is there to allow for interleaving of both kinds of
Style resources. This is an advanced feature that we do not expect most
app developers to use directly but will be quite handy for various
styling libraries and for folks who want to inline as much as possible
once Fizz supports this feature.

## refactor notes
* HostResource Fiber type is renamed HostHoistable to reflect the
generalization of the concept
* The Resource object representation is modified to reduce hidden class
checks and to use less memory overall
* The thing that distinguishes a resource from an element is whether the
Fiber has a memoizedState. If it does, it will use resource semantics,
otherwise element semantics
* The time complexity of matching hositable elements for hydration
should be improved

DiffTrain build for [6396b66](6396b66)
[View git log for this commit](https://github.com/facebook/react/commits/6396b664118442f3c2eae7bf13732fcb27bda98f)
  • Loading branch information
gnoff committed Feb 10, 2023
1 parent 6c6f5d0 commit a1c26b1
Show file tree
Hide file tree
Showing 34 changed files with 18,420 additions and 21,194 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ef9f6e77b8ef968eee659ae797da4bdc07bbbde3
6396b664118442f3c2eae7bf13732fcb27bda98f
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION_TRANSFORMS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ef9f6e77b8ef968eee659ae797da4bdc07bbbde3
6396b664118442f3c2eae7bf13732fcb27bda98f
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-dev.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ if (
}
"use strict";

var ReactVersion = "18.3.0-www-classic-ef9f6e77b-20230209";
var ReactVersion = "18.3.0-www-classic-6396b6641-20230209";

// ATTENTION
// When adding new symbols to this file,
Expand Down
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ if (
}
"use strict";

var ReactVersion = "18.3.0-www-modern-ef9f6e77b-20230209";
var ReactVersion = "18.3.0-www-modern-6396b6641-20230209";

// ATTENTION
// When adding new symbols to this file,
Expand Down
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-prod.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -646,4 +646,4 @@ exports.useSyncExternalStore = function (
);
};
exports.useTransition = useTransition;
exports.version = "18.3.0-www-classic-ef9f6e77b-20230209";
exports.version = "18.3.0-www-classic-6396b6641-20230209";
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-prod.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -638,4 +638,4 @@ exports.useSyncExternalStore = function (
);
};
exports.useTransition = useTransition;
exports.version = "18.3.0-www-modern-ef9f6e77b-20230209";
exports.version = "18.3.0-www-modern-6396b6641-20230209";
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-profiling.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ exports.useSyncExternalStore = function (
);
};
exports.useTransition = useTransition;
exports.version = "18.3.0-www-classic-ef9f6e77b-20230209";
exports.version = "18.3.0-www-classic-6396b6641-20230209";

/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
if (
Expand Down
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-profiling.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ exports.useSyncExternalStore = function (
);
};
exports.useTransition = useTransition;
exports.version = "18.3.0-www-modern-ef9f6e77b-20230209";
exports.version = "18.3.0-www-modern-6396b6641-20230209";

/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
if (
Expand Down
53 changes: 28 additions & 25 deletions compiled/facebook-www/ReactART-dev.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function _assertThisInitialized(self) {
return self;
}

var ReactVersion = "18.3.0-www-classic-ef9f6e77b-20230209";
var ReactVersion = "18.3.0-www-classic-6396b6641-20230209";

var LegacyRoot = 0;
var ConcurrentRoot = 1;
Expand Down Expand Up @@ -229,7 +229,7 @@ var OffscreenComponent = 22;
var LegacyHiddenComponent = 23;
var CacheComponent = 24;
var TracingMarkerComponent = 25;
var HostResource = 26;
var HostHoistable = 26;
var HostSingleton = 27;

// ATTENTION
Expand Down Expand Up @@ -431,7 +431,7 @@ function getComponentNameFromFiber(fiber) {
case Fragment:
return "Fragment";

case HostResource:
case HostHoistable:
case HostSingleton:
case HostComponent:
// Host component type is the display name (e.g. "div", "View")
Expand Down Expand Up @@ -877,7 +877,7 @@ function findCurrentHostFiberImpl(node) {

if (
tag === HostComponent ||
tag === HostResource ||
tag === HostHoistable ||
tag === HostSingleton ||
tag === HostText
) {
Expand Down Expand Up @@ -4962,7 +4962,7 @@ function describeFiber(fiber) {
var source = fiber._debugSource;

switch (fiber.tag) {
case HostResource:
case HostHoistable:
case HostSingleton:
case HostComponent:
return describeBuiltInComponentFrame(fiber.type);
Expand Down Expand Up @@ -15856,7 +15856,6 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
}
break;

case HostResource:
case HostSingleton:
case HostComponent:
pushHostContext(workInProgress);
Expand Down Expand Up @@ -16202,7 +16201,7 @@ function beginWork(current, workInProgress, renderLanes) {
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);

case HostResource:
case HostHoistable:

// eslint-disable-next-line no-fallthrough

Expand Down Expand Up @@ -17911,7 +17910,7 @@ function completeWork(current, workInProgress, renderLanes) {
return null;
}

case HostResource:
case HostHoistable:
// eslint-disable-next-line-no-fallthrough

case HostSingleton:
Expand Down Expand Up @@ -18601,7 +18600,7 @@ function unwindWork(current, workInProgress, renderLanes) {
return null;
}

case HostResource:
case HostHoistable:
case HostSingleton:
case HostComponent: {
// TODO: popHydrationState
Expand Down Expand Up @@ -18732,7 +18731,7 @@ function unwindInterruptedWork(current, interruptedWork, renderLanes) {
break;
}

case HostResource:
case HostHoistable:
case HostSingleton:
case HostComponent: {
popHostContext(interruptedWork);
Expand Down Expand Up @@ -19178,7 +19177,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork) {
}

case HostComponent:
case HostResource:
case HostHoistable:
case HostSingleton:
case HostText:
case HostPortal:
Expand Down Expand Up @@ -19774,7 +19773,7 @@ function commitLayoutEffectOnFiber(
break;
}

case HostResource:
case HostHoistable:
// eslint-disable-next-line-no-fallthrough

case HostSingleton:
Expand Down Expand Up @@ -20224,7 +20223,7 @@ function commitAttachRef(finishedWork) {
var instanceToUse;

switch (finishedWork.tag) {
case HostResource:
case HostHoistable:
case HostSingleton:
case HostComponent:
instanceToUse = getPublicInstance(instance);
Expand Down Expand Up @@ -20600,7 +20599,7 @@ function commitDeletionEffectsOnFiber(
// that don't modify the stack.

switch (deletedFiber.tag) {
case HostResource:
case HostHoistable:
// eslint-disable-next-line no-fallthrough

case HostSingleton:
Expand Down Expand Up @@ -21106,7 +21105,7 @@ function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
return;
}

case HostResource:
case HostHoistable:
// eslint-disable-next-line-no-fallthrough

case HostSingleton:
Expand Down Expand Up @@ -21151,14 +21150,14 @@ function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
var oldProps = current !== null ? current.memoizedProps : newProps;
var type = finishedWork.type; // TODO: Type the updateQueue to be specific to host components.

var updatePayload = finishedWork.updateQueue;
var _updatePayload = finishedWork.updateQueue;
finishedWork.updateQueue = null;

if (updatePayload !== null) {
if (_updatePayload !== null) {
try {
commitUpdate(
_instance2,
updatePayload,
_updatePayload,
type,
oldProps,
newProps,
Expand Down Expand Up @@ -21211,15 +21210,19 @@ function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
}

case HostRoot: {
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);
{
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);
}

return;
}

case HostPortal: {
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);
{
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);
}

return;
}
Expand Down Expand Up @@ -21489,7 +21492,7 @@ function disappearLayoutEffects(finishedWork) {
break;
}

case HostResource:
case HostHoistable:
case HostSingleton:
case HostComponent: {
// TODO (Offscreen) Check: flags & RefStatic
Expand Down Expand Up @@ -21591,7 +21594,7 @@ function reappearLayoutEffects(
// ...
// }

case HostResource:
case HostHoistable:
case HostSingleton:
case HostComponent: {
recursivelyTraverseReappearLayoutEffects(
Expand Down Expand Up @@ -26748,7 +26751,7 @@ function findChildHostInstancesForFiberShallowly(fiber, hostInstances) {
var foundHostInstances = false;

while (true) {
if (node.tag === HostComponent || node.tag === HostResource || false) {
if (node.tag === HostComponent || node.tag === HostHoistable || false) {
// We got a match.
foundHostInstances = true;
hostInstances.add(node.stateNode); // There may still be more, so keep searching.
Expand Down
Loading

0 comments on commit a1c26b1

Please sign in to comment.