core(tsc): add type checking to most byte efficiency audits (#5072)

This commit is contained in:
Brendan Kenny 2018-05-01 12:04:40 -07:00 коммит произвёл GitHub
Родитель 9b15f70971
Коммит e8e024b6ca
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
21 изменённых файлов: 248 добавлений и 161 удалений

Просмотреть файл

@ -8,6 +8,9 @@
const Audit = require('../audit');
const Interactive = require('../../gather/computed/metrics/lantern-interactive'); // eslint-disable-line max-len
const Simulator = require('../../lib/dependency-graph/simulator/simulator'); // eslint-disable-line no-unused-vars
const Node = require('../../lib/dependency-graph/node.js'); // eslint-disable-line no-unused-vars
const NetworkNode = require('../../lib/dependency-graph/network-node.js'); // eslint-disable-line no-unused-vars
const KB_IN_BYTES = 1024;
@ -33,7 +36,7 @@ class UnusedBytes extends Audit {
/**
* @param {number} bytes
* @param {number} networkThroughput measured in bytes/second
* @return {string}
* @return {number}
*/
static bytesToMs(bytes, networkThroughput) {
const milliseconds = bytes / networkThroughput * 1000;
@ -42,9 +45,9 @@ class UnusedBytes extends Audit {
/**
* Estimates the number of bytes this network record would have consumed on the network based on the
* uncompressed size (totalBytes), uses the actual transfer size from the network record if applicable.
* uncompressed size (totalBytes). Uses the actual transfer size from the network record if applicable.
*
* @param {?LH.WebInspector.NetworkRequest} networkRecord
* @param {LH.WebInspector.NetworkRequest=} networkRecord
* @param {number} totalBytes Uncompressed size of the resource
* @param {string=} resourceType
* @param {number=} compressionRatio
@ -58,19 +61,21 @@ class UnusedBytes extends Audit {
return Math.round(totalBytes * compressionRatio);
} else if (networkRecord._resourceType && networkRecord._resourceType._name === resourceType) {
// This was a regular standalone asset, just use the transfer size.
return networkRecord._transferSize;
return networkRecord._transferSize || 0;
} else {
// This was an asset that was inlined in a different resource type (e.g. HTML document).
// Use the compression ratio of the resource to estimate the total transferred bytes.
const compressionRatio = networkRecord._transferSize / networkRecord._resourceSize || 1;
const transferSize = networkRecord._transferSize || 0;
const resourceSize = networkRecord._resourceSize;
const compressionRatio = resourceSize !== undefined ? (transferSize / resourceSize) : 1;
return Math.round(totalBytes * compressionRatio);
}
}
/**
* @param {Artifacts} artifacts
* @param {LH.Audit.Context=} context
* @return {Promise<AuditResult>}
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {Promise<LH.Audit.Product>}
*/
static audit(artifacts, context) {
const trace = artifacts.traces[Audit.DEFAULT_PASS];
@ -90,42 +95,51 @@ class UnusedBytes extends Audit {
artifacts.requestLoadSimulator(simulatorOptions),
])
)
.then(([result, graph, simulator]) => this.createAuditResult(result, graph, simulator));
.then(([result, graph, simulator]) => this.createAuditProduct(result, graph, simulator));
}
/**
* Computes the estimated effect of all the byte savings on the last long task
* in the provided graph.
*
* @param {Array<{url: string, wastedBytes: number}>} results The array of byte savings results per resource
* @param {Array<LH.Audit.ByteEfficiencyResult>} results The array of byte savings results per resource
* @param {Node} graph
* @param {Simulator} simulator
* @return {number}
*/
static computeWasteWithTTIGraph(results, graph, simulator) {
const simulationBeforeChanges = simulator.simulate(graph);
/** @type {Map<LH.Audit.ByteEfficiencyResult['url'], LH.Audit.ByteEfficiencyResult>} */
const resultsByUrl = new Map();
for (const result of results) {
resultsByUrl.set(result.url, result);
}
// Update all the transfer sizes to reflect implementing our recommendations
/** @type {Map<string, number>} */
const originalTransferSizes = new Map();
graph.traverse(node => {
if (node.type !== 'network') return;
if (!resultsByUrl.has(node.record.url)) return;
const original = node.record.transferSize;
const wastedBytes = resultsByUrl.get(node.record.url).wastedBytes;
const networkNode = /** @type {NetworkNode} */ (node);
const result = resultsByUrl.get(networkNode.record.url);
if (!result) return;
const original = networkNode.record.transferSize;
// cloning NetworkRequest objects is difficult, so just stash the original transfer size
node.record._originalTransferSize = original;
node.record._transferSize = Math.max(original - wastedBytes, 0);
originalTransferSizes.set(networkNode.record.requestId, original);
const wastedBytes = result.wastedBytes;
networkNode.record._transferSize = Math.max(original - wastedBytes, 0);
});
const simulationAfterChanges = simulator.simulate(graph);
// Restore the original transfer size after we've done our simulation
graph.traverse(node => {
if (node.type !== 'network') return;
if (!node.record._originalTransferSize) return;
node.record._transferSize = node.record._originalTransferSize;
const networkNode = /** @type {NetworkNode} */ (node);
const originalTransferSize = originalTransferSizes.get(networkNode.record.requestId);
if (originalTransferSize === undefined) return;
networkNode.record._transferSize = originalTransferSize;
});
const savingsOnTTI = Math.max(
@ -139,12 +153,12 @@ class UnusedBytes extends Audit {
}
/**
* @param {Audit.HeadingsResult} result
* @param {LH.Audit.ByteEfficiencyProduct} result
* @param {Node} graph
* @param {Simulator} simulator
* @return {AuditResult}
* @return {LH.Audit.Product}
*/
static createAuditResult(result, graph, simulator) {
static createAuditProduct(result, graph, simulator) {
const debugString = result.debugString;
const results = result.results.sort((itemA, itemB) => itemB.wastedBytes - itemA.wastedBytes);
@ -161,6 +175,8 @@ class UnusedBytes extends Audit {
wastedMs,
wastedBytes,
};
// @ts-ignore - TODO(bckenny): unify details types. items shouldn't be an indexed type.
const details = Audit.makeTableDetails(result.headings, results, summary);
return {
@ -179,13 +195,19 @@ class UnusedBytes extends Audit {
};
}
/* eslint-disable no-unused-vars */
/**
* @param {!Artifacts} artifacts
* @return {!Audit.HeadingsResult}
* @param {LH.Artifacts} artifacts
* @param {Array<LH.WebInspector.NetworkRequest>} networkRecords
* @param {LH.Audit.Context} context
* @return {LH.Audit.ByteEfficiencyProduct|Promise<LH.Audit.ByteEfficiencyProduct>}
*/
static audit_() {
static audit_(artifacts, networkRecords, context) {
throw new Error('audit_ unimplemented');
}
/* eslint-enable no-unused-vars */
}
module.exports = UnusedBytes;

Просмотреть файл

@ -42,26 +42,25 @@ class EfficientAnimatedContent extends ByteEfficiencyAudit {
}
/**
* @param {!LH.Artifacts} artifacts
* @return {Promise<LH.Audit.Product>}
* @param {LH.Artifacts} artifacts
* @param {Array<LH.WebInspector.NetworkRequest>} networkRecords
* @return {LH.Audit.ByteEfficiencyProduct}
*/
static async audit_(artifacts) {
const devtoolsLogs = artifacts.devtoolsLogs[EfficientAnimatedContent.DEFAULT_PASS];
const networkRecords = await artifacts.requestNetworkRecords(devtoolsLogs);
static audit_(artifacts, networkRecords) {
const unoptimizedContent = networkRecords.filter(
record => record.mimeType === 'image/gif' &&
record => record._mimeType === 'image/gif' &&
record._resourceType === WebInspector.resourceTypes.Image &&
record.resourceSize > GIF_BYTE_THRESHOLD
(record._resourceSize || 0) > GIF_BYTE_THRESHOLD
);
/** @type {Array<{url: string, totalBytes: number, wastedBytes: number}>}*/
const results = unoptimizedContent.map(record => {
const resourceSize = record._resourceSize || 0;
return {
url: record.url,
totalBytes: record.resourceSize,
wastedBytes: Math.round(record.resourceSize *
EfficientAnimatedContent.getPercentSavings(record.resourceSize)),
totalBytes: resourceSize,
wastedBytes: Math.round(resourceSize *
EfficientAnimatedContent.getPercentSavings(resourceSize)),
};
});

Просмотреть файл

@ -21,7 +21,7 @@ const IGNORE_THRESHOLD_IN_PERCENT = 75;
class OffscreenImages extends ByteEfficiencyAudit {
/**
* @return {!AuditMeta}
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
@ -38,7 +38,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
}
/**
* @param {!ClientRect} imageRect
* @param {ClientRect} imageRect
* @param {{innerWidth: number, innerHeight: number}} viewportDimensions
* @return {number}
*/
@ -82,8 +82,10 @@ class OffscreenImages extends ByteEfficiencyAudit {
}
/**
* @param {!Artifacts} artifacts
* @return {!Audit.HeadingsResult}
* @param {LH.Artifacts} artifacts
* @param {Array<LH.WebInspector.NetworkRequest>} networkRecords
* @param {LH.Audit.Context} context
* @return {Promise<LH.Audit.ByteEfficiencyProduct>}
*/
static audit_(artifacts, networkRecords, context) {
const images = artifacts.ImageUsage;
@ -91,6 +93,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
const trace = artifacts.traces[ByteEfficiencyAudit.DEFAULT_PASS];
const devtoolsLog = artifacts.devtoolsLogs[ByteEfficiencyAudit.DEFAULT_PASS];
/** @type {string|undefined} */
let debugString;
const resultsMap = images.reduce((results, image) => {
if (!image.networkRecord) {
@ -100,6 +103,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
const processed = OffscreenImages.computeWaste(image, viewportDimensions);
if (processed instanceof Error) {
debugString = processed.message;
// @ts-ignore TODO(bckenny): Sentry type checking
Sentry.captureException(processed, {tags: {audit: this.meta.name}, level: 'warning'});
return results;
}
@ -116,6 +120,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
// TODO(phulce): move this to always use lantern
const settings = context.settings;
return artifacts.requestFirstCPUIdle({trace, devtoolsLog, settings}).then(firstInteractive => {
// @ts-ignore - see above TODO.
const ttiTimestamp = firstInteractive.timestamp / 1000000;
const results = Array.from(resultsMap.values()).filter(item => {
const isWasteful =

Просмотреть файл

@ -7,7 +7,6 @@
/**
* @fileoverview Audit a page to see if it does have resources that are blocking first paint
*/
'use strict';
const Audit = require('../audit');
@ -16,6 +15,9 @@ const ByteEfficiencyAudit = require('./byte-efficiency-audit');
const UnusedCSS = require('./unused-css-rules');
const WebInspector = require('../../lib/web-inspector');
const Simulator = require('../../lib/dependency-graph/simulator/simulator'); // eslint-disable-line no-unused-vars
const NetworkNode = require('../../lib/dependency-graph/network-node.js'); // eslint-disable-line no-unused-vars
// Because of the way we detect blocking stylesheets, asynchronously loaded
// CSS with link[rel=preload] and an onload handler (see https://github.com/filamentgroup/loadCSS)
// can be falsely flagged as blocking. Therefore, ignore stylesheets that loaded fast enough
@ -27,17 +29,25 @@ const MINIMUM_WASTED_MS = 50;
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
* @return {Object<string, {node: Node, nodeTiming: LH.Gatherer.Simulation.NodeTiming}>}
*/
const getNodesAndTimingByUrl = nodeTimings => {
function getNodesAndTimingByUrl(nodeTimings) {
/** @type {Object<string, {node: Node, nodeTiming: LH.Gatherer.Simulation.NodeTiming}>} */
const urlMap = {};
const nodes = Array.from(nodeTimings.keys());
return nodes.reduce((map, node) => {
map[node.record && node.record.url] = {node, nodeTiming: nodeTimings.get(node)};
return map;
}, {});
};
nodes.forEach(node => {
if (node.type !== 'network') return;
const networkNode = /** @type {NetworkNode} */ (node);
const nodeTiming = nodeTimings.get(node);
if (!nodeTiming) return;
urlMap[networkNode.record.url] = {node, nodeTiming};
});
return urlMap;
}
class RenderBlockingResources extends Audit {
/**
* @return {!AuditMeta}
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
@ -57,8 +67,9 @@ class RenderBlockingResources extends Audit {
}
/**
* @param {Artifacts} artifacts
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {Promise<{wastedMs: number, results: Array<{url: string, totalBytes: number, wastedMs: number}>}>}
*/
static async computeResults(artifacts, context) {
const trace = artifacts.traces[Audit.DEFAULT_PASS];
@ -70,6 +81,7 @@ class RenderBlockingResources extends Audit {
const metricSettings = {throttlingMethod: 'simulate'};
const metricComputationData = {trace, devtoolsLog, simulator, settings: metricSettings};
// @ts-ignore - TODO(bckenny): allow optional `throttling` settings
const fcpSimulation = await artifacts.requestFirstContentfulPaint(metricComputationData);
const fcpTsInMs = traceOfTab.timestamps.firstContentfulPaint / 1000;
@ -91,6 +103,7 @@ class RenderBlockingResources extends Audit {
node.traverse(node => deferredNodeIds.add(node.id));
// "wastedMs" is the download time of the network request, responseReceived - requestSent
// @ts-ignore - TODO(phulce): nodeTiming.startTime/endTime shouldn't be optional by this point?
const wastedMs = Math.round(nodeTiming.endTime - nodeTiming.startTime);
if (wastedMs < MINIMUM_WASTED_MS) continue;
@ -135,38 +148,43 @@ class RenderBlockingResources extends Audit {
const originalEstimate = simulator.simulate(fcpGraph).timeInMs;
let totalChildNetworkBytes = 0;
const minimalFCPGraph = fcpGraph.cloneWithRelationships(node => {
const minimalFCPGraph = /** @type {NetworkNode} */ (fcpGraph.cloneWithRelationships(node => {
// If a node can be deferred, exclude it from the new FCP graph
const canDeferRequest = deferredIds.has(node.id);
if (node.type !== Node.TYPES.NETWORK) return !canDeferRequest;
const networkNode = /** @type {NetworkNode} */ (node);
const isStylesheet =
node.type === Node.TYPES.NETWORK &&
node.record._resourceType === WebInspector.resourceTypes.Stylesheet;
networkNode.record._resourceType === WebInspector.resourceTypes.Stylesheet;
if (canDeferRequest && isStylesheet) {
// We'll inline the used bytes of the stylesheet and assume the rest can be deferred
const wastedBytes = wastedCssBytesByUrl.get(node.record.url) || 0;
totalChildNetworkBytes += node.record._transferSize - wastedBytes;
const wastedBytes = wastedCssBytesByUrl.get(networkNode.record.url) || 0;
totalChildNetworkBytes += (networkNode.record._transferSize || 0) - wastedBytes;
}
// If a node can be deferred, exclude it from the new FCP graph
return !canDeferRequest;
});
}));
// Add the inlined bytes to the HTML response
minimalFCPGraph.record._transferSize += totalChildNetworkBytes;
const originalTransferSize = minimalFCPGraph.record._transferSize;
const safeTransferSize = originalTransferSize || 0;
minimalFCPGraph.record._transferSize = safeTransferSize + totalChildNetworkBytes;
const estimateAfterInline = simulator.simulate(minimalFCPGraph).timeInMs;
minimalFCPGraph.record._transferSize -= totalChildNetworkBytes;
minimalFCPGraph.record._transferSize = originalTransferSize;
return Math.round(Math.max(originalEstimate - estimateAfterInline, 0));
}
/**
* @param {!Artifacts} artifacts
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {Map<string, number>}
* @return {Promise<Map<string, number>>}
*/
static async computeWastedCSSBytes(artifacts, context) {
const wastedBytesByUrl = new Map();
try {
// TODO(phulce): pull this out into computed artifact
const results = await UnusedCSS.audit(artifacts, context);
// @ts-ignore - TODO(bckenny): details types.
for (const item of results.details.items) {
wastedBytesByUrl.set(item.url, item.wastedBytes);
}
@ -176,9 +194,9 @@ class RenderBlockingResources extends Audit {
}
/**
* @param {!Artifacts} artifacts
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {AuditResult}
* @return {Promise<LH.Audit.Product>}
*/
static async audit(artifacts, context) {
const {results, wastedMs} = await RenderBlockingResources.computeResults(artifacts, context);

Просмотреть файл

@ -10,7 +10,7 @@ const Util = require('../../report/html/renderer/util');
class TotalByteWeight extends ByteEfficiencyAudit {
/**
* @return {!AuditMeta}
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
@ -39,9 +39,9 @@ class TotalByteWeight extends ByteEfficiencyAudit {
}
/**
* @param {!Artifacts} artifacts
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {!Promise<!AuditResult>}
* @return {Promise<LH.Audit.Product>}
*/
static audit(artifacts, context) {
const devtoolsLogs = artifacts.devtoolsLogs[ByteEfficiencyAudit.DEFAULT_PASS];
@ -50,11 +50,12 @@ class TotalByteWeight extends ByteEfficiencyAudit {
artifacts.requestNetworkThroughput(devtoolsLogs),
]).then(([networkRecords, networkThroughput]) => {
let totalBytes = 0;
/** @type {Array<{url: string, totalBytes: number, totalMs: number}>} */
let results = [];
networkRecords.forEach(record => {
// exclude data URIs since their size is reflected in other resources
// exclude unfinished requests since they won't have transfer size information
if (record.scheme === 'data' || !record.finished) return;
if (record.parsedURL.scheme === 'data' || !record.finished) return;
const result = {
url: record.url,

Просмотреть файл

@ -16,7 +16,7 @@ const IGNORE_THRESHOLD_IN_BYTES = 2048;
*/
class UnminifiedCSS extends ByteEfficiencyAudit {
/**
* @return {!AuditMeta}
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
@ -92,15 +92,16 @@ class UnminifiedCSS extends ByteEfficiencyAudit {
}
/**
* @param {{content: string, header: {sourceURL: string}}} stylesheet
* @param {?LH.WebInspector.NetworkRequest} networkRecord
* @param {LH.Artifacts.CSSStyleSheetInfo} stylesheet
* @param {LH.WebInspector.NetworkRequest=} networkRecord
* @param {string} pageUrl
* @return {{minifiedLength: number, contentLength: number}}
* @return {{url: string|LH.Audit.DetailsRendererCodeDetailJSON, totalBytes: number, wastedBytes: number, wastedPercent: number}}
*/
static computeWaste(stylesheet, networkRecord, pageUrl) {
const content = stylesheet.content;
const totalTokenLength = UnminifiedCSS.computeTokenLength(content);
/** @type {LH.Audit.ByteEfficiencyResult['url']} */
let url = stylesheet.header.sourceURL;
if (!url || url === pageUrl) {
const contentPreview = UnusedCSSRules.determineContentPreview(stylesheet.content);
@ -121,8 +122,9 @@ class UnminifiedCSS extends ByteEfficiencyAudit {
}
/**
* @param {!Artifacts} artifacts
* @return {!Audit.HeadingsResult}
* @param {LH.Artifacts} artifacts
* @param {Array<LH.WebInspector.NetworkRequest>} networkRecords
* @return {LH.Audit.ByteEfficiencyProduct}
*/
static audit_(artifacts, networkRecords) {
const pageUrl = artifacts.URL.finalUrl;

Просмотреть файл

@ -6,6 +6,7 @@
'use strict';
const ByteEfficiencyAudit = require('./byte-efficiency-audit');
// @ts-ignore - TODO: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/25410
const esprima = require('esprima');
const IGNORE_THRESHOLD_IN_PERCENT = 10;
@ -23,7 +24,7 @@ const IGNORE_THRESHOLD_IN_BYTES = 2048;
*/
class UnminifiedJavaScript extends ByteEfficiencyAudit {
/**
* @return {!AuditMeta}
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
@ -39,7 +40,8 @@ class UnminifiedJavaScript extends ByteEfficiencyAudit {
/**
* @param {string} scriptContent
* @return {{minifiedLength: number, contentLength: number}}
* @param {LH.WebInspector.NetworkRequest} networkRecord
* @return {{url: string, totalBytes: number, wastedBytes: number, wastedPercent: number}}
*/
static computeWaste(scriptContent, networkRecord) {
const contentLength = scriptContent.length;
@ -68,10 +70,12 @@ class UnminifiedJavaScript extends ByteEfficiencyAudit {
}
/**
* @param {!Artifacts} artifacts
* @return {!Audit.HeadingsResult}
* @param {LH.Artifacts} artifacts
* @param {Array<LH.WebInspector.NetworkRequest>} networkRecords
* @return {LH.Audit.ByteEfficiencyProduct}
*/
static audit_(artifacts, networkRecords) {
/** @type {Array<LH.Audit.ByteEfficiencyResult>} */
const results = [];
let debugString;
for (const requestId of Object.keys(artifacts.Scripts)) {

Просмотреть файл

@ -10,9 +10,11 @@ const ByteEfficiencyAudit = require('./byte-efficiency-audit');
const IGNORE_THRESHOLD_IN_BYTES = 2048;
const PREVIEW_LENGTH = 100;
/** @typedef {LH.Artifacts.CSSStyleSheetInfo & {networkRecord: LH.WebInspector.NetworkRequest, usedRules: Array<LH.Crdp.CSS.RuleUsage>}} StyleSheetInfo */
class UnusedCSSRules extends ByteEfficiencyAudit {
/**
* @return {!AuditMeta}
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
@ -28,16 +30,16 @@ class UnusedCSSRules extends ByteEfficiencyAudit {
}
/**
* @param {!Array.<{header: {styleSheetId: string}}>} styles The output of the Styles gatherer.
* @param {Array<LH.Artifacts.CSSStyleSheetInfo>} styles The output of the Styles gatherer.
* @param {Array<LH.WebInspector.NetworkRequest>} networkRecords
* @return {!Object} A map of styleSheetId to stylesheet information.
* @return {Object<string, StyleSheetInfo>} A map of styleSheetId to stylesheet information.
*/
static indexStylesheetsById(styles, networkRecords) {
const indexedNetworkRecords = networkRecords
.reduce((indexed, record) => {
indexed[record.url] = record;
return indexed;
}, {});
}, /** @type {Object<string, LH.WebInspector.NetworkRequest>} */ ({}));
return styles.reduce((indexed, stylesheet) => {
indexed[stylesheet.header.styleSheetId] = Object.assign({
@ -45,13 +47,13 @@ class UnusedCSSRules extends ByteEfficiencyAudit {
networkRecord: indexedNetworkRecords[stylesheet.header.sourceURL],
}, stylesheet);
return indexed;
}, {});
}, /** @type {Object<string, StyleSheetInfo>} */ ({}));
}
/**
* Adds used rules to their corresponding stylesheet.
* @param {!Array.<{styleSheetId: string, used: boolean}>} rules The output of the CSSUsage gatherer.
* @param {!Object} indexedStylesheets Stylesheet information indexed by id.
* @param {Array<LH.Crdp.CSS.RuleUsage>} rules The output of the CSSUsage gatherer.
* @param {Object<string, StyleSheetInfo>} indexedStylesheets Stylesheet information indexed by id.
*/
static indexUsedRules(rules, indexedStylesheets) {
rules.forEach(rule => {
@ -68,7 +70,7 @@ class UnusedCSSRules extends ByteEfficiencyAudit {
}
/**
* @param {!Object} stylesheetInfo
* @param {StyleSheetInfo} stylesheetInfo
* @return {{wastedBytes: number, totalBytes: number, wastedPercent: number}}
*/
static computeUsage(stylesheetInfo) {
@ -93,7 +95,7 @@ class UnusedCSSRules extends ByteEfficiencyAudit {
/**
* Trims stylesheet content down to the first rule-set definition.
* @param {?string} content
* @param {string=} content
* @return {string}
*/
static determineContentPreview(content) {
@ -128,15 +130,12 @@ class UnusedCSSRules extends ByteEfficiencyAudit {
}
/**
* @param {!Object} stylesheetInfo The stylesheetInfo object.
* @param {StyleSheetInfo} stylesheetInfo The stylesheetInfo object.
* @param {string} pageUrl The URL of the page, used to identify inline styles.
* @return {?{url: string, wastedBytes: number, totalBytes: number}}
* @return {LH.Audit.ByteEfficiencyResult}
*/
static mapSheetToResult(stylesheetInfo, pageUrl) {
if (stylesheetInfo.isDuplicate) {
return null;
}
/** @type {LH.Audit.ByteEfficiencyResult['url']} */
let url = stylesheetInfo.header.sourceURL;
if (!url || url === pageUrl) {
const contentPreview = UnusedCSSRules.determineContentPreview(stylesheetInfo.content);
@ -148,8 +147,8 @@ class UnusedCSSRules extends ByteEfficiencyAudit {
}
/**
* @param {!Artifacts} artifacts
* @return {!Audit.HeadingsResult}
* @param {LH.Artifacts} artifacts
* @return {Promise<LH.Audit.ByteEfficiencyProduct>}
*/
static audit_(artifacts) {
const styles = artifacts.CSSUsage.stylesheets;

Просмотреть файл

@ -11,7 +11,7 @@ const IGNORE_THRESHOLD_IN_BYTES = 2048;
class UnusedJavaScript extends ByteEfficiencyAudit {
/**
* @return {!AuditMeta}
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
@ -25,7 +25,7 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
}
/**
* @param {!JsUsageArtifact} script
* @param {LH.Crdp.Profiler.ScriptCoverage} script
* @return {{unusedLength: number, contentLength: number}}
*/
static computeWaste(script) {
@ -61,9 +61,9 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
}
/**
* @param {!Array<{unusedLength: number, contentLength: number}>} wasteData
* @param {Array<{unusedLength: number, contentLength: number}>} wasteData
* @param {LH.WebInspector.NetworkRequest} networkRecord
* @return {{url: string, totalBytes: number, wastedBytes: number, wastedPercent: number}}
* @return {LH.Audit.ByteEfficiencyResult}
*/
static mergeWaste(wasteData, networkRecord) {
let unusedLength = 0;
@ -87,10 +87,12 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
}
/**
* @param {!Artifacts} artifacts
* @return {!Audit.HeadingsResult}
* @param {LH.Artifacts} artifacts
* @param {Array<LH.WebInspector.NetworkRequest>} networkRecords
* @return {LH.Audit.ByteEfficiencyProduct}
*/
static audit_(artifacts, networkRecords) {
/** @type {Map<string, Array<LH.Crdp.Profiler.ScriptCoverage>>} */
const scriptsByUrl = new Map();
for (const script of artifacts.JsUsage) {
const scripts = scriptsByUrl.get(script.url) || [];
@ -98,6 +100,7 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
scriptsByUrl.set(script.url, scripts);
}
/** @type {Array<LH.Audit.ByteEfficiencyResult>} */
const results = [];
for (const [url, scripts] of scriptsByUrl.entries()) {
const networkRecord = networkRecords.find(record => record.url === url);

Просмотреть файл

@ -6,6 +6,7 @@
'use strict';
const assert = require('assert');
// @ts-ignore - typed where used.
const parseCacheControl = require('parse-cache-control');
const Audit = require('../audit');
const WebInspector = require('../../lib/web-inspector');
@ -16,11 +17,10 @@ const IGNORE_THRESHOLD_IN_PERCENT = 0.925;
class CacheHeaders extends Audit {
/**
* @return {!AuditMeta}
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
category: 'Caching',
name: 'uses-long-cache-ttl',
description: 'Uses efficient cache policy on static assets',
failureDescription: 'Uses inefficient cache policy on static assets',
@ -103,22 +103,24 @@ class CacheHeaders extends Audit {
* Computes the user-specified cache lifetime, 0 if explicit no-cache policy is in effect, and null if not
* user-specified. See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
*
* @param {!Map<string,string>} headers
* @param {!Object} cacheControl Follows the potential settings of cache-control, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
* @param {Map<string, string>} headers
* @param {{'no-cache'?: boolean,'no-store'?: boolean, 'max-age'?: number}} cacheControl Follows the potential settings of cache-control, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
* @return {?number}
*/
static computeCacheLifetimeInSeconds(headers, cacheControl) {
if (cacheControl) {
// Cache-Control takes precendence over expires
if (cacheControl['no-cache'] || cacheControl['no-store']) return 0;
if (Number.isFinite(cacheControl['max-age'])) return Math.max(cacheControl['max-age'], 0);
const maxAge = cacheControl['max-age'];
if (maxAge !== undefined && Number.isFinite(maxAge)) return Math.max(maxAge, 0);
} else if ((headers.get('pragma') || '').includes('no-cache')) {
// The HTTP/1.0 Pragma header can disable caching if cache-control is not set, see https://tools.ietf.org/html/rfc7234#section-5.4
return 0;
}
if (headers.has('expires')) {
const expires = new Date(headers.get('expires')).getTime();
const expiresHeaders = headers.get('expires');
if (expiresHeaders) {
const expires = new Date(expiresHeaders).getTime();
// Invalid expires values MUST be treated as already expired
if (!expires) return 0;
return Math.max(0, Math.ceil((expires - Date.now()) / 1000));
@ -162,9 +164,9 @@ class CacheHeaders extends Audit {
}
/**
* @param {!Artifacts} artifacts
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {!AuditResult}
* @return {Promise<LH.Audit.Product>}
*/
static audit(artifacts, context) {
const devtoolsLogs = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
@ -176,8 +178,9 @@ class CacheHeaders extends Audit {
for (const record of records) {
if (!CacheHeaders.isCacheableAsset(record)) continue;
/** @type {Map<string, string>} */
const headers = new Map();
for (const header of record._responseHeaders) {
for (const header of record._responseHeaders || []) {
headers.set(header.name.toLowerCase(), header.value);
}
@ -195,7 +198,7 @@ class CacheHeaders extends Audit {
if (cacheHitProbability > IGNORE_THRESHOLD_IN_PERCENT) continue;
const url = URL.elideDataURI(record._url);
const totalBytes = record._transferSize;
const totalBytes = record._transferSize || 0;
const wastedBytes = (1 - cacheHitProbability) * totalBytes;
totalWastedBytes += wastedBytes;

Просмотреть файл

@ -7,6 +7,7 @@
* @fileoverview This audit determines if the images used are sufficiently larger
* than JPEG compressed images without metadata at quality 85.
*/
// @ts-nocheck - TODO(bckenny)
'use strict';
const ByteEfficiencyAudit = require('./byte-efficiency-audit');
@ -16,7 +17,7 @@ const IGNORE_THRESHOLD_IN_BYTES = 4096;
class UsesOptimizedImages extends ByteEfficiencyAudit {
/**
* @return {!AuditMeta}
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
@ -31,19 +32,18 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
}
/**
* @param {{originalSize: number, webpSize: number, jpegSize: number}} image
* @param {string} type
* @param {{originalSize: number, jpegSize: number}} image
* @return {{bytes: number, percent: number}}
*/
static computeSavings(image, type) {
const bytes = image.originalSize - image[type + 'Size'];
static computeSavings(image) {
const bytes = image.originalSize - image.jpegSize;
const percent = 100 * bytes / image.originalSize;
return {bytes, percent};
}
/**
* @param {!Artifacts} artifacts
* @return {!Audit.HeadingsResult}
* @param {LH.Artifacts} artifacts
* @return {LH.Audit.ByteEfficiencyProduct}
*/
static audit_(artifacts) {
const images = artifacts.OptimizedImages;
@ -60,7 +60,7 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
}
const url = URL.elideDataURI(image.url);
const jpegSavings = UsesOptimizedImages.computeSavings(image, 'jpeg');
const jpegSavings = UsesOptimizedImages.computeSavings(image);
results.push({
url,

Просмотреть файл

@ -18,11 +18,10 @@ const Sentry = require('../../lib/sentry');
const URL = require('../../lib/url-shim');
const IGNORE_THRESHOLD_IN_BYTES = 2048;
const WASTEFUL_THRESHOLD_IN_BYTES = 25 * 1024;
class UsesResponsiveImages extends ByteEfficiencyAudit {
/**
* @return {!AuditMeta}
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
@ -39,9 +38,9 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
}
/**
* @param {!Object} image
* @param {LH.Artifacts.SingleImageUsage} image
* @param {number} DPR devicePixelRatio
* @return {?Object}
* @return {null|Error|LH.Audit.ByteEfficiencyResult};
*/
static computeWaste(image, DPR) {
const url = URL.elideDataURI(image.src);
@ -66,19 +65,19 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
totalBytes,
wastedBytes,
wastedPercent: 100 * wastedRatio,
isWasteful: wastedBytes > WASTEFUL_THRESHOLD_IN_BYTES,
};
}
/**
* @param {!Artifacts} artifacts
* @return {!Audit.HeadingsResult}
* @param {LH.Artifacts} artifacts
* @return {LH.Audit.ByteEfficiencyProduct}
*/
static audit_(artifacts) {
const images = artifacts.ImageUsage;
const DPR = artifacts.ViewportDimensions.devicePixelRatio;
let debugString;
/** @type {Map<LH.Audit.ByteEfficiencyResult['url'], LH.Audit.ByteEfficiencyResult>} */
const resultsMap = new Map();
images.forEach(image => {
// TODO: give SVG a free pass until a detail per pixel metric is available
@ -91,6 +90,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
if (processed instanceof Error) {
debugString = processed.message;
// @ts-ignore TODO(bckenny): Sentry type checking
Sentry.captureException(processed, {tags: {audit: this.meta.name}, level: 'warning'});
return;
}

Просмотреть файл

@ -17,7 +17,7 @@ const IGNORE_THRESHOLD_IN_PERCENT = 0.1;
class ResponsesAreCompressed extends ByteEfficiencyAudit {
/**
* @return {!AuditMeta}
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
@ -33,13 +33,13 @@ class ResponsesAreCompressed extends ByteEfficiencyAudit {
}
/**
* @param {!Artifacts} artifacts
* @param {number} networkThroughput
* @return {!Audit.HeadingsResult}
* @param {LH.Artifacts} artifacts
* @return {LH.Audit.ByteEfficiencyProduct}
*/
static audit_(artifacts) {
const uncompressedResponses = artifacts.ResponseCompression;
/** @type {Array<LH.Audit.ByteEfficiencyResult>} */
const results = [];
uncompressedResponses.forEach(record => {
const originalSize = record.resourceSize;

Просмотреть файл

@ -6,17 +6,17 @@
/*
* @fileoverview This audit determines if the images could be smaller when compressed with WebP.
*/
// @ts-nocheck - TODO(bckenny)
'use strict';
const ByteEfficiencyAudit = require('./byte-efficiency-audit');
const OptimizedImages = require('./uses-optimized-images');
const URL = require('../../lib/url-shim');
const IGNORE_THRESHOLD_IN_BYTES = 8192;
class UsesWebPImages extends ByteEfficiencyAudit {
/**
* @return {!AuditMeta}
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
@ -32,8 +32,18 @@ class UsesWebPImages extends ByteEfficiencyAudit {
}
/**
* @param {!Artifacts} artifacts
* @return {!Audit.HeadingsResult}
* @param {{originalSize: number, webpSize: number}} image
* @return {{bytes: number, percent: number}}
*/
static computeSavings(image) {
const bytes = image.originalSize - image.webpSize;
const percent = 100 * bytes / image.originalSize;
return {bytes, percent};
}
/**
* @param {LH.Artifacts} artifacts
* @return {LH.Audit.ByteEfficiencyProduct}
*/
static audit_(artifacts) {
const images = artifacts.OptimizedImages;
@ -49,7 +59,7 @@ class UsesWebPImages extends ByteEfficiencyAudit {
}
const url = URL.elideDataURI(image.url);
const webpSavings = OptimizedImages.computeSavings(image, 'webp');
const webpSavings = UsesWebPImages.computeSavings(image);
results.push({
url,

Просмотреть файл

@ -19,7 +19,7 @@
*/
class Node {
/**
* @param {string|number} id
* @param {string} id
*/
constructor(id) {
this._id = id;
@ -30,7 +30,7 @@ class Node {
}
/**
* @return {string|number}
* @return {string}
*/
get id() {
return this._id;

Просмотреть файл

@ -76,7 +76,7 @@ describe('Byte efficiency base audit', () => {
});
it('should format details', () => {
const result = ByteEfficiencyAudit.createAuditResult({
const result = ByteEfficiencyAudit.createAuditProduct({
headings: baseHeadings,
results: [],
}, graph, simulator);
@ -85,7 +85,7 @@ describe('Byte efficiency base audit', () => {
});
it('should set the rawValue', () => {
const result = ByteEfficiencyAudit.createAuditResult(
const result = ByteEfficiencyAudit.createAuditProduct(
{
headings: baseHeadings,
results: [
@ -101,22 +101,22 @@ describe('Byte efficiency base audit', () => {
});
it('should score the wastedMs', () => {
const perfectResult = ByteEfficiencyAudit.createAuditResult({
const perfectResult = ByteEfficiencyAudit.createAuditProduct({
headings: baseHeadings,
results: [{url: 'http://example.com/', wastedBytes: 1 * 1000}],
}, graph, simulator);
const goodResult = ByteEfficiencyAudit.createAuditResult({
const goodResult = ByteEfficiencyAudit.createAuditProduct({
headings: baseHeadings,
results: [{url: 'http://example.com/', wastedBytes: 20 * 1000}],
}, graph, simulator);
const averageResult = ByteEfficiencyAudit.createAuditResult({
const averageResult = ByteEfficiencyAudit.createAuditProduct({
headings: baseHeadings,
results: [{url: 'http://example.com/', wastedBytes: 100 * 1000}],
}, graph, simulator);
const failingResult = ByteEfficiencyAudit.createAuditResult({
const failingResult = ByteEfficiencyAudit.createAuditProduct({
headings: baseHeadings,
results: [{url: 'http://example.com/', wastedBytes: 400 * 1000}],
}, graph, simulator);
@ -129,7 +129,7 @@ describe('Byte efficiency base audit', () => {
it('should throw on invalid graph', () => {
assert.throws(() => {
ByteEfficiencyAudit.createAuditResult({
ByteEfficiencyAudit.createAuditProduct({
headings: baseHeadings,
results: [{wastedBytes: 350, totalBytes: 700, wastedPercent: 50}],
}, null);
@ -137,7 +137,7 @@ describe('Byte efficiency base audit', () => {
});
it('should populate KB', () => {
const result = ByteEfficiencyAudit.createAuditResult({
const result = ByteEfficiencyAudit.createAuditProduct({
headings: baseHeadings,
results: [
{wastedBytes: 2048, totalBytes: 4096, wastedPercent: 50},
@ -152,7 +152,7 @@ describe('Byte efficiency base audit', () => {
});
it('should sort on wastedBytes', () => {
const result = ByteEfficiencyAudit.createAuditResult({
const result = ByteEfficiencyAudit.createAuditProduct({
headings: baseHeadings,
results: [
{wastedBytes: 350, totalBytes: 700, wastedPercent: 50},
@ -167,7 +167,7 @@ describe('Byte efficiency base audit', () => {
});
it('should create a display value', () => {
const result = ByteEfficiencyAudit.createAuditResult({
const result = ByteEfficiencyAudit.createAuditProduct({
headings: baseHeadings,
results: [
{wastedBytes: 512, totalBytes: 700, wastedPercent: 50},
@ -185,7 +185,7 @@ describe('Byte efficiency base audit', () => {
const artifacts = Runner.instantiateComputedArtifacts();
const graph = await artifacts.requestPageDependencyGraph({trace, devtoolsLog});
const simulator = await artifacts.requestLoadSimulator({devtoolsLog, settings});
const result = ByteEfficiencyAudit.createAuditResult(
const result = ByteEfficiencyAudit.createAuditProduct(
{
headings: [{key: 'value', text: 'Label'}],
results: [

Просмотреть файл

@ -16,23 +16,22 @@ describe('Page uses videos for animated GIFs', () => {
const networkRecords = [
{
_resourceType: WebInspector.resourceTypes.Image,
mimeType: 'image/gif',
resourceSize: 100240,
_mimeType: 'image/gif',
_resourceSize: 100240,
url: 'https://example.com/example.gif',
},
{
_resourceType: WebInspector.resourceTypes.Image,
mimeType: 'image/gif',
resourceSize: 110000,
_mimeType: 'image/gif',
_resourceSize: 110000,
url: 'https://example.com/example2.gif',
},
];
const artifacts = {
devtoolsLogs: {[EfficientAnimatedContent.DEFAULT_PASS]: []},
requestNetworkRecords: () => Promise.resolve(networkRecords),
};
const {results} = await EfficientAnimatedContent.audit_(artifacts);
const {results} = await EfficientAnimatedContent.audit_(artifacts, networkRecords);
assert.equal(results.length, 1);
assert.equal(results[0].url, 'https://example.com/example2.gif');
assert.equal(results[0].totalBytes, 110000);
@ -49,10 +48,9 @@ describe('Page uses videos for animated GIFs', () => {
];
const artifacts = {
devtoolsLogs: {[EfficientAnimatedContent.DEFAULT_PASS]: []},
requestNetworkRecords: () => Promise.resolve(networkRecords),
};
const {results} = await EfficientAnimatedContent.audit_(artifacts);
const {results} = await EfficientAnimatedContent.audit_(artifacts, networkRecords);
assert.equal(results.length, 0);
});
@ -71,10 +69,9 @@ describe('Page uses videos for animated GIFs', () => {
];
const artifacts = {
devtoolsLogs: {[EfficientAnimatedContent.DEFAULT_PASS]: []},
requestNetworkRecords: () => Promise.resolve(networkRecords),
};
const {results} = await EfficientAnimatedContent.audit_(artifacts);
const {results} = await EfficientAnimatedContent.audit_(artifacts, networkRecords);
assert.equal(results.length, 0);
});
});

Просмотреть файл

@ -7,17 +7,23 @@
const TotalByteWeight = require('../../../audits/byte-efficiency/total-byte-weight.js');
const assert = require('assert');
const URL = require('url').URL;
const options = TotalByteWeight.defaultOptions;
/* eslint-env mocha */
function generateRequest(url, size, baseUrl = 'http://google.com/') {
const parsedUrl = new URL(url, baseUrl);
const scheme = parsedUrl.protocol.slice(0, -1);
return {
url: `${baseUrl}${url}`,
url: parsedUrl.href,
finished: true,
transferSize: size * 1024,
responseReceivedTime: 1000,
endTime: 2000,
parsedURL: {
scheme,
},
};
}
@ -25,6 +31,7 @@ function generateArtifacts(records) {
if (records[0] && records[0].length > 1) {
records = records.map(args => generateRequest(...args));
}
return {
devtoolsLogs: {defaultPass: []},
requestNetworkRecords: () => Promise.resolve(records),

Просмотреть файл

@ -19,6 +19,7 @@
"lighthouse-core/audits/audit.js",
"lighthouse-core/audits/accessibility/**/*.js",
"lighthouse-core/audits/dobetterweb/**/*.js",
"lighthouse-core/audits/byte-efficiency/**/*.js",
"lighthouse-core/audits/manual/**/*.js",
"lighthouse-core/audits/seo/manual/*.js",
"lighthouse-core/audits/seo/robots-txt.js",

22
typings/audit.d.ts поставляемый
Просмотреть файл

@ -38,15 +38,24 @@ declare global {
key: string;
itemType: string;
text: string;
displayUnit?: string;
granularity?: number;
}
export interface HeadingsResult {
results: number;
export interface ByteEfficiencyProduct {
results: Array<ByteEfficiencyResult>;
headings: Array<Audit.Heading>;
passes: boolean;
displayValue?: string;
debugString?: string;
}
export interface ByteEfficiencyResult {
url: string | DetailsRendererCodeDetailJSON;
wastedBytes: number;
totalBytes: number;
wastedPercent?: number;
}
// TODO: placeholder typedefs until Details are typed
export interface DetailsRendererDetailsSummary {
wastedMs?: number;
@ -61,8 +70,13 @@ declare global {
summary?: DetailsRendererDetailsSummary;
}
export interface DetailsRendererCodeDetailJSON {
type: 'code',
value: string;
}
export type DetailsItem = string | number | DetailsRendererNodeDetailsJSON |
DetailsRendererLinkDetailsJSON;
DetailsRendererLinkDetailsJSON | DetailsRendererCodeDetailJSON;
export interface DetailsRendererNodeDetailsJSON {
type: 'node';

2
typings/web-inspector.d.ts поставляемый
Просмотреть файл

@ -26,7 +26,9 @@ declare global {
_responseReceivedTime: number;
transferSize: number;
/** Should use a default of 0 if not defined */
_transferSize?: number;
/** Should use a default of 0 if not defined */
_resourceSize?: number;
finished: boolean;