forked from GoogleChrome/lighthouse
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfont-display.js
193 lines (172 loc) · 8.7 KB
/
font-display.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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/**
* @license Copyright 2017 The Lighthouse 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 {Audit} from './audit.js';
import UrlUtils from '../lib/url-utils.js';
import * as i18n from '../lib/i18n/i18n.js';
import {Sentry} from '../lib/sentry.js';
import {NetworkRecords} from '../computed/network-records.js';
const PASSING_FONT_DISPLAY_REGEX = /^(block|fallback|optional|swap)$/;
const CSS_URL_REGEX = /url\((.*?)\)/;
const CSS_URL_GLOBAL_REGEX = new RegExp(CSS_URL_REGEX, 'g');
const UIStrings = {
/** Title of a diagnostic audit that provides detail on if all the text on a webpage was visible while the page was loading its webfonts. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
title: 'All text remains visible during webfont loads',
/** Title of a diagnostic audit that provides detail on the load of the page's webfonts. Often the text is invisible for seconds before the webfont resource is loaded. This imperative title is shown to users when there is a significant amount of execution time that could be reduced. */
failureTitle: 'Ensure text remains visible during webfont load',
/** Description of a Lighthouse audit that tells the user *why* they should use the font-display CSS feature. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description:
'Leverage the `font-display` CSS feature to ensure text is user-visible while ' +
'webfonts are loading. ' +
'[Learn more about `font-display`](https://developer.chrome.com/docs/lighthouse/performance/font-display/).',
/**
* @description [ICU Syntax] A warning message that is shown when Lighthouse couldn't automatically check some of the page's fonts, telling the user that they will need to manually check the fonts coming from a certain URL origin.
* @example {https://font.cdn.com/} fontOrigin
*/
undeclaredFontOriginWarning:
'{fontCountForOrigin, plural, ' +
// eslint-disable-next-line max-len
'=1 {Lighthouse was unable to automatically check the `font-display` value for the origin {fontOrigin}.} ' +
// eslint-disable-next-line max-len
'other {Lighthouse was unable to automatically check the `font-display` values for the origin {fontOrigin}.}}',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class FontDisplay extends Audit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'font-display',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
supportedModes: ['navigation'],
requiredArtifacts: ['devtoolsLogs', 'CSSUsage', 'URL'],
};
}
/**
* @param {LH.Artifacts} artifacts
* @param {RegExp} passingFontDisplayRegex
* @return {{passingURLs: Set<string>, failingURLs: Set<string>}}
*/
static findFontDisplayDeclarations(artifacts, passingFontDisplayRegex) {
/** @type {Set<string>} */
const passingURLs = new Set();
/** @type {Set<string>} */
const failingURLs = new Set();
// Go through all the stylesheets to find all @font-face declarations
for (const stylesheet of artifacts.CSSUsage.stylesheets) {
// Eliminate newlines so we can more easily scan through with a regex
const newlinesStripped = stylesheet.content.replace(/(\r|\n)+/g, ' ');
// Find the @font-faces
const fontFaceDeclarations = newlinesStripped.match(/@font-face\s*{(.*?)}/g) || [];
// Go through all the @font-face declarations to find a declared `font-display: ` property
for (const declaration of fontFaceDeclarations) {
// We'll try to find the URL it's referencing.
const rawFontURLs = declaration.match(CSS_URL_GLOBAL_REGEX);
// If no URLs, we can't really do anything; bail
if (!rawFontURLs) continue;
// Find the font-display value by matching a single token, optionally surrounded by whitespace,
// followed either by a semicolon or the end of a block.
const fontDisplayMatch = declaration.match(/font-display\s*:\s*(\w+)\s*(;|\})/);
const rawFontDisplay = fontDisplayMatch?.[1] || '';
const hasPassingFontDisplay = passingFontDisplayRegex.test(rawFontDisplay);
const targetURLSet = hasPassingFontDisplay ? passingURLs : failingURLs;
// Finally convert the raw font URLs to the absolute URLs and add them to the set.
const relativeURLs = rawFontURLs
// @ts-expect-error - guaranteed to match from previous regex, pull URL group out
.map(s => s.match(CSS_URL_REGEX)[1].trim())
.map(s => {
// remove any quotes surrounding the URL
if (/^('|").*\1$/.test(s)) {
return s.substr(1, s.length - 2);
}
return s;
});
// Convert the relative CSS URL to an absolute URL and add it to the target set.
for (const relativeURL of relativeURLs) {
try {
const relativeRoot = UrlUtils.isValid(stylesheet.header.sourceURL) ?
stylesheet.header.sourceURL : artifacts.URL.finalDisplayedUrl;
const absoluteURL = new URL(relativeURL, relativeRoot);
targetURLSet.add(absoluteURL.href);
} catch (err) {
Sentry.captureException(err, {tags: {audit: this.meta.id}});
}
}
}
}
return {passingURLs, failingURLs};
}
/**
* Some pages load many fonts we can't check, so dedupe on origin.
* @param {Array<string>} warningUrls
* @return {Array<LH.IcuMessage>}
*/
static getWarningsForFontUrls(warningUrls) {
/** @type {Map<string, number>} */
const warningCountByOrigin = new Map();
for (const warningUrl of warningUrls) {
const origin = UrlUtils.getOrigin(warningUrl);
if (!origin) continue;
const count = warningCountByOrigin.get(origin) || 0;
warningCountByOrigin.set(origin, count + 1);
}
const warnings = [...warningCountByOrigin].map(([fontOrigin, fontCountForOrigin]) => {
return str_(UIStrings.undeclaredFontOriginWarning, {fontCountForOrigin, fontOrigin});
});
return warnings;
}
/**
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {Promise<LH.Audit.Product>}
*/
static async audit(artifacts, context) {
const devtoolsLogs = artifacts.devtoolsLogs[this.DEFAULT_PASS];
const networkRecords = await NetworkRecords.request(devtoolsLogs, context);
const {passingURLs, failingURLs} =
FontDisplay.findFontDisplayDeclarations(artifacts, PASSING_FONT_DISPLAY_REGEX);
/** @type {Array<string>} */
const warningURLs = [];
const results = networkRecords
// Find all fonts...
.filter(record => record.resourceType === 'Font')
// ...and that aren't data URLs, the blocking concern doesn't really apply
.filter(record => !/^data:/.test(record.url))
.filter(record => !/^blob:/.test(record.url))
// ...that have a failing font-display value
.filter(record => {
// Failing URLs should be considered.
if (failingURLs.has(record.url)) return true;
// Everything else shouldn't be, but we should warn if we don't recognize the URL at all.
if (!passingURLs.has(record.url)) warningURLs.push(record.url);
return false;
})
.map(record => {
// In reality the end time should be calculated with paint time included
// all browsers wait 3000ms to block text so we make sure 3000 is our max wasted time
const wastedMs = Math.min(record.networkEndTime - record.networkRequestTime, 3000);
return {
url: record.url,
wastedMs,
};
});
/** @type {LH.Audit.Details.Table['headings']} */
const headings = [
{key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
{key: 'wastedMs', valueType: 'ms', label: str_(i18n.UIStrings.columnWastedMs)},
];
const details = Audit.makeTableDetails(headings, results);
return {
score: Number(results.length === 0),
details,
warnings: FontDisplay.getWarningsForFontUrls(warningURLs),
};
}
}
export default FontDisplay;
export {UIStrings};