Skip to content

Commit 1506685

Browse files
authored
Suspensey Fonts for View Transition (#32031)
Fonts flickering in while loading can be disturbing to any transition but especially View Transitions. Even if they don't cause layout thrash - the paint thrash is bad enough. We might add Suspensey fonts to all Transitions in the future but it's especially a no-brainer for View Transitions. We need to apply mutations to the DOM first to know whether that will trigger new fonts to load. For general Suspensey fonts, we'd have to revert the commit by applying mutations in reverse to return to the previous state. For View Transitions, since a snapshot is already frozen, we can freeze the screen while we're waiting for the font at no extra cost. It does mean that the page isn't responsive during this time but we should only block this for a short period anyway. The timeout needs to be short enough that it doesn't cause too much of an issue when it's a new load and slow, yet long enough that you have a chance to load it. Otherwise we wait for no reason. The assumption here is that you likely have either cached the font or preloaded it earlier - or you're on an extremely fast connection. This case is for optimizing the high end experience. Before: https://github.com/user-attachments/assets/e0acfffe-fa49-40d6-82c3-5b08760175fb After: https://github.com/user-attachments/assets/615a03d3-9d6b-4eb1-8bd5-182c4c37a628 Note that since the Navigation is blocked on the font now the browser spinner shows up while the font is loading.
1 parent fd9cfa4 commit 1506685

File tree

4 files changed

+53
-2
lines changed

4 files changed

+53
-2
lines changed

fixtures/view-transition/src/components/Chrome.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ export default class Chrome extends Component {
1212
<meta name="viewport" content="width=device-width, initial-scale=1" />
1313
<link rel="shortcut icon" href="favicon.ico" />
1414
<link rel="stylesheet" href={assets['main.css']} />
15+
<link rel="preconnect" href="https://fonts.googleapis.com" />
16+
<link
17+
rel="preconnect"
18+
href="https://fonts.gstatic.com"
19+
crossOrigin=""
20+
/>
21+
<link
22+
href="https://fonts.googleapis.com/css2?family=Roboto:wght@100&display=swap"
23+
rel="stylesheet"
24+
/>
1525
<title>{this.props.title}</title>
1626
</head>
1727
<body>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.roboto-font {
2+
font-family: "Roboto", serif;
3+
font-optical-sizing: auto;
4+
font-weight: 100;
5+
font-style: normal;
6+
font-variation-settings:
7+
"wdth" 100;
8+
}

fixtures/view-transition/src/components/Page.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function Component() {
2929
className={
3030
transitions['enter-slide-right'] + ' ' + transitions['exit-slide-left']
3131
}>
32-
<p>Slide In from Left, Slide Out to Right</p>
32+
<p className="roboto-font">Slide In from Left, Slide Out to Right</p>
3333
</ViewTransition>
3434
);
3535
}

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,13 @@ export function hasInstanceAffectedParent(
11981198
return oldRect.height !== newRect.height || oldRect.width !== newRect.width;
11991199
}
12001200

1201+
// How long to wait for new fonts to load before just committing anyway.
1202+
// This freezes the screen. It needs to be short enough that it doesn't cause too much of
1203+
// an issue when it's a new load and slow, yet long enough that you have a chance to load
1204+
// it. Otherwise we wait for no reason. The assumption here is that you likely have
1205+
// either cached the font or preloaded it earlier.
1206+
const SUSPENSEY_FONT_TIMEOUT = 500;
1207+
12011208
export function startViewTransition(
12021209
rootContainer: Container,
12031210
mutationCallback: () => void,
@@ -1220,8 +1227,34 @@ export function startViewTransition(
12201227
const ownerWindow = ownerDocument.defaultView;
12211228
const pendingNavigation =
12221229
ownerWindow.navigation && ownerWindow.navigation.transition;
1230+
// $FlowFixMe[prop-missing]
1231+
const previousFontLoadingStatus = ownerDocument.fonts.status;
12231232
mutationCallback();
1224-
// TODO: Wait for fonts.
1233+
if (previousFontLoadingStatus === 'loaded') {
1234+
// Force layout calculation to trigger font loading.
1235+
// eslint-disable-next-line ft-flow/no-unused-expressions
1236+
(ownerDocument.documentElement: any).clientHeight;
1237+
if (
1238+
// $FlowFixMe[prop-missing]
1239+
ownerDocument.fonts.status === 'loading'
1240+
) {
1241+
// The mutation lead to new fonts being loaded. We should wait on them before continuing.
1242+
// This avoids waiting for potentially unrelated fonts that were already loading before.
1243+
// Either in an earlier transition or as part of a sync optimistic state. This doesn't
1244+
// include preloads that happened earlier.
1245+
const fontsReady = Promise.race([
1246+
// $FlowFixMe[prop-missing]
1247+
ownerDocument.fonts.ready,
1248+
new Promise(resolve =>
1249+
setTimeout(resolve, SUSPENSEY_FONT_TIMEOUT),
1250+
),
1251+
]).then(layoutCallback, layoutCallback);
1252+
const allReady = pendingNavigation
1253+
? Promise.allSettled([pendingNavigation.finished, fontsReady])
1254+
: fontsReady;
1255+
return allReady.then(afterMutationCallback, afterMutationCallback);
1256+
}
1257+
}
12251258
layoutCallback();
12261259
if (pendingNavigation) {
12271260
return pendingNavigation.finished.then(

0 commit comments

Comments
 (0)