Skip to content

Commit

Permalink
new-audit(font-display): enforce font-display optional (#3831)
Browse files Browse the repository at this point in the history
* webfonts wasted time

* Review changes

* fix lint

* Fix review changes

* Fix eslint

* Add gatherer test

* Add audit tests

* Added webfont smoketest

* Remove debugger statements

* Smokehouse: Convert save-assets-path to an array

* fix smokehouse

* Fix webfonts audit

* change eslint-env to browser

* nuke files

* Fix regexes for fonts

* comments and cleanup.

* Review changes

* Remove save-assets-path

* Final nits

* remove unnecessary parens

* add learn more link

* remove trailing space
  • Loading branch information
wardpeet authored and patrickhulce committed Feb 7, 2018
1 parent 3030b4f commit 7ce4334
Show file tree
Hide file tree
Showing 9 changed files with 483 additions and 0 deletions.
34 changes: 34 additions & 0 deletions lighthouse-cli/test/fixtures/perf/fonts.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<style>
@font-face {
font-family: 'Lobster';
font-style: normal;
font-weight: 400;
src: local('Lobster'), url('./lobster-v20-latin-regular.eot?#iefix') format('eot'), url('./lobster-v20-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Lobster Two';
font-style: normal;
font-weight: 700;
font-display: optional;
src: local("Lobster Two"), url("./lobster-two-v10-latin-700.woff2?delay=4000") format('woff2');
}
.webfont {
font-family: Lobster, sans-serif;
}
strong.webfont {
font-family: Lobster Two, sans-serif;
}
.nofont {
font-family: Unknown, sans-serif;
}
</style>
</head>
<body>
<p class="webfont">Let's load some sweet webfonts...</p>
<p><strong class="webfont">Let's load some sweet webfonts...</strong></p>
<p class"nofont">Some lovely text that uses the fallback font</p>
</body>
</html>
Binary file not shown.
Binary file not shown.
15 changes: 15 additions & 0 deletions lighthouse-cli/test/smokehouse/perf/expectations.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,19 @@ module.exports = [
},
},
},
{
initialUrl: 'http://localhost:10200/perf/fonts.html',
url: 'http://localhost:10200/perf/fonts.html',
audits: {
'font-display': {
score: false,
rawValue: false,
details: {
items: {
length: 1,
},
},
},
},
},
];
81 changes: 81 additions & 0 deletions lighthouse-core/audits/font-display.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* @license Copyright 2017 Google Inc. 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.
*/
'use strict';

const Audit = require('./audit');
const Util = require('../report/v2/renderer/util');
const WebInspector = require('../lib/web-inspector');
const allowedFontFaceDisplays = ['block', 'fallback', 'optional', 'swap'];

class FontDisplay extends Audit {
/**
* @return {!AuditMeta}
*/
static get meta() {
return {
name: 'font-display',
description: 'All text remains visible during webfont loads',
failureDescription: 'Avoid invisible text while webfonts are loading',
helpText: 'Leverage the font-display CSS feature to ensure text is user-visible while ' +
'webfonts are loading. ' +
'[Learn more](https://developers.google.com/web/updates/2016/02/font-display).',
requiredArtifacts: ['devtoolsLogs', 'Fonts'],
};
}

/**
* @param {!Artifacts} artifacts
* @return {!AuditResult}
*/
static audit(artifacts) {
const devtoolsLogs = artifacts.devtoolsLogs[this.DEFAULT_PASS];
const fontFaces = artifacts.Fonts;

// Filter font-faces that do not have a display tag with optional or swap
const fontsWithoutProperDisplay = fontFaces.filter(fontFace =>
!fontFace.display || !allowedFontFaceDisplays.includes(fontFace.display)
);

return artifacts.requestNetworkRecords(devtoolsLogs).then((networkRecords) => {
const results = networkRecords.filter(record => {
const isFont = record._resourceType === WebInspector.resourceTypes.Font;

return isFont;
})
.filter(fontRecord => {
// find the fontRecord of a font
return !!fontsWithoutProperDisplay.find(fontFace => {
return fontFace.src.find(src => fontRecord.url === src);
});
})
// calculate wasted time
.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 wastedTime = Math.min((record._endTime - record._startTime) * 1000, 3000);

return {
url: record.url,
wastedTime: Util.formatMilliseconds(wastedTime, 1),
};
});

const headings = [
{key: 'url', itemType: 'url', text: 'Font URL'},
{key: 'wastedTime', itemType: 'text', text: 'Font download time'},
];
const details = Audit.makeTableDetails(headings, results);

return {
score: results.length === 0,
rawValue: results.length === 0,
details,
};
});
}
}

module.exports = FontDisplay;
3 changes: 3 additions & 0 deletions lighthouse-core/config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ module.exports = {
'seo/hreflang',
'seo/embedded-content',
'seo/canonical',
'fonts',
],
},
{
Expand Down Expand Up @@ -98,6 +99,7 @@ module.exports = {
'deprecations',
'mainthread-work-breakdown',
'bootup-time',
'font-display',
'manual/pwa-cross-browser',
'manual/pwa-page-transitions',
'manual/pwa-each-page-has-url',
Expand Down Expand Up @@ -285,6 +287,7 @@ module.exports = {
{id: 'bootup-time', weight: 0, group: 'perf-info'},
{id: 'screenshot-thumbnails', weight: 0},
{id: 'mainthread-work-breakdown', weight: 0, group: 'perf-info'},
{id: 'font-display', weight: 0, group: 'perf-info'},
],
},
'pwa': {
Expand Down
157 changes: 157 additions & 0 deletions lighthouse-core/gather/gatherers/fonts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* @license Copyright 2017 Google Inc. 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.
*/
'use strict';

const Gatherer = require('./gatherer');
const fontFaceDescriptors = [
'display',
'family',
'featureSettings',
'stretch',
'style',
'unicodeRange',
'variant',
'weight',
];

/* eslint-env browser*/
/**
* Collect applied webfont data from `document.fonts`
* @param {string[]}
* @return {{}}
*/
/* istanbul ignore next */
function getAllLoadedFonts(descriptors) {
const getFont = fontFace => {
const fontRule = {};
descriptors.forEach(descriptor => {
fontRule[descriptor] = fontFace[descriptor];
});

return fontRule;
};

return document.fonts.ready.then(() => {
return Array.from(document.fonts).filter(fontFace => fontFace.status === 'loaded')
.map(getFont);
});
}

/**
* Collect authored webfont data from the `CSSFontFaceRule`s present in document.styleSheets
* @return {{}}
*/
/* istanbul ignore next */
function getFontFaceFromStylesheets() {
/**
* Get full data about each CSSFontFaceRule within a styleSheet object
* @param {StyleSheet} stylesheet
* @return {{}}
*/
function getSheetsFontFaces(stylesheet) {
const fontUrlRegex = 'url\\((?:")([^"]+)(?:"|\')\\)';
const fontFaceRules = [];
if (stylesheet.cssRules) {
for (const rule of stylesheet.cssRules) {
if (rule instanceof CSSFontFaceRule) {
const fontsObject = {
display: rule.style.fontDisplay || 'auto',
family: rule.style.fontFamily.replace(/"|'/g, ''),
stretch: rule.style.fontStretch || 'normal',
style: rule.style.fontStyle || 'normal',
weight: rule.style.fontWeight || 'normal',
variant: rule.style.fontVariant || 'normal',
unicodeRange: rule.style.unicodeRange || 'U+0-10FFFF',
featureSettings: rule.style.featureSettings || 'normal',
src: [],
};

if (rule.style.src) {
const matches = rule.style.src.match(new RegExp(fontUrlRegex, 'g'));
if (matches) {
fontsObject.src = matches.map(match => {
const res = new RegExp(fontUrlRegex).exec(match);
return new URL(res[1], location.href).href;
});
}
}

fontFaceRules.push(fontsObject);
}
}
}

return fontFaceRules;
}

/**
* Provided a <link rel=stylesheet> element, it attempts to reload the asset with CORS headers.
* Without CORS headers, a cross-origin stylesheet will have node.styleSheet.cssRules === null.
* @param {Element} oldNode
* @return {<!Promise>}
*/
function loadStylesheetWithCORS(oldNode) {
const newNode = oldNode.cloneNode(true);

return new Promise(resolve => {
newNode.addEventListener('load', function onload() {
newNode.removeEventListener('load', onload);
resolve(getFontFaceFromStylesheets());
});
newNode.crossOrigin = 'anonymous';
oldNode.parentNode.insertBefore(newNode, oldNode);
oldNode.remove();
});
}

const promises = [];
// Get all loaded stylesheets
for (const stylesheet of document.styleSheets) {
try {
// Cross-origin stylesheets don't expose cssRules by default. We reload them w/ CORS headers.
if (stylesheet.cssRules === null && stylesheet.href && stylesheet.ownerNode &&
!stylesheet.ownerNode.crossOrigin) {
promises.push(loadStylesheetWithCORS(stylesheet.ownerNode));
} else {
promises.push(Promise.resolve(getSheetsFontFaces(stylesheet)));
}
} catch (err) {
promises.push(loadStylesheetWithCORS(stylesheet.ownerNode));
}
}
// Flatten results
return Promise.all(promises).then(fontFaces => [].concat(...fontFaces));
}
/* eslint-env node */

class Fonts extends Gatherer {
_findSameFontFamily(fontFace, fontFacesList) {
return fontFacesList.find(fontItem => {
return !fontFaceDescriptors.find(descriptor => {
return fontFace[descriptor] !== fontItem[descriptor];
});
});
}

afterPass({driver}) {
const args = JSON.stringify(fontFaceDescriptors);
return Promise.all(
[
driver.evaluateAsync(`(${getAllLoadedFonts.toString()})(${args})`),
driver.evaluateAsync(`(${getFontFaceFromStylesheets.toString()})()`),
]
).then(([loadedFonts, fontFaces]) => {
return loadedFonts.map(fontFace => {
const fontFaceItem = this._findSameFontFamily(fontFace, fontFaces);
fontFace.src = fontFaceItem.src || [];

return fontFace;
});
});
}
}

module.exports = Fonts;
Loading

0 comments on commit 7ce4334

Please sign in to comment.