forked from ampproject/amphtml
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfont-stylesheet-timeout.js
150 lines (144 loc) · 5.36 KB
/
font-stylesheet-timeout.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
* Copyright 2016 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {urls} from './config';
import {onDocumentReady} from './core/document-ready';
import {escapeCssSelectorIdent} from './core/dom/css-selectors';
/**
* While browsers put a timeout on font downloads (3s by default,
* some less on slow connections), there is no such timeout for style
* sheets. In the case of AMP external stylesheets are ONLY used to
* download fonts, but browsers have no reasonable timeout for
* stylesheets. Users may thus wait a long time for these to download
* even though all they do is reference fonts.
*
* For that reasons this function identifies (or rather infers) font
* stylesheets that have not downloaded within timeout period of the page
* response starting and reinserts equivalent link tags dynamically. This
* removes their page-render-blocking nature and lets the doc render.
*
* @param {!Window} win
*/
export function fontStylesheetTimeout(win) {
onDocumentReady(win.document, () => maybeTimeoutFonts(win));
}
/**
* @param {!Window} win
*/
function maybeTimeoutFonts(win) {
// Educated guess 😅, but we're calculating the correct value further down
// if available.
let timeSinceNavigationStart = 1500;
// If available, we start counting from the time the HTTP request
// for the page started. The preload scanner should then quickly
// start the CSS download.
const perf = win.performance;
if (perf && perf.timing && perf.timing.navigationStart) {
timeSinceNavigationStart = Date.now() - perf.timing.navigationStart;
}
// Set timeout such that we have some time to paint fonts in time for
// the desired goal of a 2500ms for LCP.
const timeout = Math.max(
1,
2500 - 400 /* Estimated max time to paint */ - timeSinceNavigationStart
);
// Avoid timer dependency since this runs very early in execution.
win.setTimeout(() => {
// Try again, more fonts might have loaded.
timeoutFontFaces(win);
const {styleSheets} = win.document;
if (!styleSheets) {
return;
}
// Find all stylesheets that aren't loaded from the AMP CDN (those are
// critical if they are present).
const styleLinkElements = win.document.querySelectorAll(
`link[rel~="stylesheet"]:not([href^="${escapeCssSelectorIdent(
urls.cdn
)}"])`
);
// Compare external sheets against elements of document.styleSheets.
// They do not appear in this list until they have been loaded.
const timedoutStyleSheets = [];
for (let i = 0; i < styleLinkElements.length; i++) {
const link = styleLinkElements[i];
let found = false;
for (let n = 0; n < styleSheets.length; n++) {
if (styleSheets[n].ownerNode == link) {
found = true;
break;
}
}
if (!found) {
timedoutStyleSheets.push(link);
}
}
for (let i = 0; i < timedoutStyleSheets.length; i++) {
const link = timedoutStyleSheets[i];
// To avoid blocking the render, we assign a non-matching media
// attribute first…
const media = link.media || 'all';
link.media = 'print';
// And then switch it back to the original after the stylesheet
// loaded.
link.onload = () => {
link.media = media;
timeoutFontFaces(win);
};
link.setAttribute('i-amphtml-timeout', timeout);
// Pop/insert the same link. This causes Chrome to unblock, and doesn't
// blank out Safari. #12521
link.parentNode.insertBefore(link, link.nextSibling);
}
}, timeout);
}
/**
* Sets font faces that haven't been loaded by the time this was called to
* `font-display: swap` in supported browsers.
* See https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display
* for details on behavior.
* Swap effectively leads to immediate display of the fallback font with
* the custom font being displayed when possible.
* While this is not the most desirable setting, it is compatible with the
* default (which does that but only waiting for 3 seconds).
* Ideally websites would opt into `font-display: optional` which provides
* nicer UX for non-icon fonts.
* If fonts set a non default display mode, this does nothing.
* @param {!Window} win
*/
function timeoutFontFaces(win) {
const doc = win.document;
// TODO(@cramforce) Switch to .values when FontFaceSet extern supports it.
if (!doc.fonts || !doc.fonts['values']) {
return;
}
const it = doc.fonts['values']();
let entry;
while ((entry = it.next())) {
const fontFace = entry.value;
if (!fontFace) {
return;
}
if (fontFace.status != 'loading') {
continue;
}
// Not supported or non-default value.
// If the publisher specified a non-default, we respect that, of course.
if (!('display' in fontFace) || fontFace.display != 'auto') {
continue;
}
fontFace.display = 'swap';
}
}