core(redirects): use lantern to compute savings (#5081)

This commit is contained in:
Patrick Hulce 2018-05-01 13:37:01 -07:00 коммит произвёл Brendan Kenny
Родитель e8e024b6ca
Коммит 6159de61b6
4 изменённых файлов: 121 добавлений и 76 удалений

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

@ -11,7 +11,7 @@ const UnusedBytes = require('./byte-efficiency/byte-efficiency-audit');
class Redirects extends Audit {
/**
* @return {!AuditMeta}
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
@ -20,68 +20,92 @@ class Redirects extends Audit {
failureDescription: 'Has multiple page redirects',
scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
helpText: 'Redirects introduce additional delays before the page can be loaded. [Learn more](https://developers.google.com/speed/docs/insights/AvoidRedirects).',
requiredArtifacts: ['URL', 'devtoolsLogs'],
requiredArtifacts: ['URL', 'devtoolsLogs', 'traces'],
};
}
/**
* @param {!Artifacts} artifacts
* @return {!AuditResult}
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {Promise<LH.Audit.Product>}
*/
static audit(artifacts) {
const data = {URL: artifacts.URL, devtoolsLog: artifacts.devtoolsLogs[Audit.DEFAULT_PASS]};
return artifacts.requestMainResource(data)
.then(mainResource => {
// redirects is only available when redirects happens
const redirectRequests = Array.from(mainResource.redirects || []);
static async audit(artifacts, context) {
const settings = context.settings;
const trace = artifacts.traces[Audit.DEFAULT_PASS];
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
// add main resource to redirectRequests so we can use it to calculate wastedMs
redirectRequests.push(mainResource);
const traceOfTab = await artifacts.requestTraceOfTab(trace);
const networkRecords = await artifacts.requestNetworkRecords(devtoolsLog);
const mainResource = await artifacts.requestMainResource({URL: artifacts.URL, devtoolsLog});
let totalWastedMs = 0;
const pageRedirects = [];
const metricComputationData = {trace, devtoolsLog, traceOfTab, networkRecords, settings};
const metricResult = await artifacts.requestLanternInteractive(metricComputationData);
// Kickoff the results table (with the initial request) if there are > 1 redirects
if (redirectRequests.length > 1) {
pageRedirects.push({
url: `(Initial: ${redirectRequests[0].url})`,
wastedMs: 0,
});
}
/** @type {Map<string, LH.Gatherer.Simulation.NodeTiming>} */
const nodeTimingsByUrl = new Map();
for (const [node, timing] of metricResult.pessimisticEstimate.nodeTimings.entries()) {
if (node.type === 'network') {
const networkNode = /** @type {LH.Gatherer.Simulation.GraphNetworkNode} */ (node);
nodeTimingsByUrl.set(networkNode.record.url, timing);
}
}
for (let i = 1; i < redirectRequests.length; i++) {
const initialRequest = redirectRequests[i - 1];
const redirectedRequest = redirectRequests[i];
// redirects is only available when redirects happens
const redirectRequests = Array.from(mainResource.redirects || []);
const wastedMs = (redirectedRequest.startTime - initialRequest.startTime) * 1000;
totalWastedMs += wastedMs;
// add main resource to redirectRequests so we can use it to calculate wastedMs
redirectRequests.push(mainResource);
pageRedirects.push({
url: redirectedRequest.url,
wastedMs,
});
}
let totalWastedMs = 0;
const pageRedirects = [];
const headings = [
{key: 'url', itemType: 'text', text: 'Redirected URL'},
{key: 'wastedMs', itemType: 'ms', text: 'Time for Redirect', granularity: 1},
];
const summary = {wastedMs: totalWastedMs};
const details = Audit.makeTableDetails(headings, pageRedirects, summary);
return {
// We award a passing grade if you only have 1 redirect
score: redirectRequests.length <= 2 ? 1 : UnusedBytes.scoreForWastedMs(totalWastedMs),
rawValue: totalWastedMs,
displayValue: Util.formatMilliseconds(totalWastedMs, 1),
extendedInfo: {
value: {
wastedMs: totalWastedMs,
},
},
details,
};
// Kickoff the results table (with the initial request) if there are > 1 redirects
if (redirectRequests.length > 1) {
pageRedirects.push({
url: `(Initial: ${redirectRequests[0].url})`,
wastedMs: 0,
});
}
for (let i = 1; i < redirectRequests.length; i++) {
const initialRequest = redirectRequests[i - 1];
const redirectedRequest = redirectRequests[i];
const initialTiming = nodeTimingsByUrl.get(initialRequest.url);
const redirectedTiming = nodeTimingsByUrl.get(redirectedRequest.url);
if (!initialTiming || !redirectedTiming) {
throw new Error('Could not find redirects in graph');
}
// @ts-ignore TODO(phulce): split NodeTiming typedef, these are always defined
const wastedMs = redirectedTiming.startTime - initialTiming.startTime;
totalWastedMs += wastedMs;
pageRedirects.push({
url: redirectedRequest.url,
wastedMs,
});
}
const headings = [
{key: 'url', itemType: 'text', text: 'Redirected URL'},
{key: 'wastedMs', itemType: 'ms', text: 'Time for Redirect'},
];
const summary = {wastedMs: totalWastedMs};
const details = Audit.makeTableDetails(headings, pageRedirects, summary);
return {
// We award a passing grade if you only have 1 redirect
score: redirectRequests.length <= 2 ? 1 : UnusedBytes.scoreForWastedMs(totalWastedMs),
rawValue: totalWastedMs,
displayValue: Util.formatMilliseconds(totalWastedMs, 1),
extendedInfo: {
value: {
wastedMs: totalWastedMs,
},
},
details,
};
}
}

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

@ -11,7 +11,7 @@ const assert = require('assert');
/* eslint-env mocha */
const FAILING_THREE_REDIRECTS = {
startTime: 17,
url: 'http://exampel.com/',
url: 'https://m.example.com/final',
redirects: [
{
startTime: 0,
@ -30,25 +30,25 @@ const FAILING_THREE_REDIRECTS = {
const FAILING_TWO_REDIRECTS = {
startTime: 446.286,
url: 'http://lisairish.com/',
url: 'https://www.lisairish.com/',
redirects: [
{
startTime: 445.648,
url: 'https://lisairish.com/',
url: 'http://lisairish.com/',
},
{
startTime: 445.757,
url: 'https://www.lisairish.com/',
url: 'https://lisairish.com/',
},
],
};
const SUCCESS_ONE_REDIRECT = {
startTime: 136.383,
url: 'https://lisairish.com/',
url: 'https://www.lisairish.com/',
redirects: [{
startTime: 135.873,
url: 'https://www.lisairish.com/',
url: 'https://lisairish.com/',
}],
};
@ -58,30 +58,45 @@ const SUCCESS_NOREDIRECT = {
redirects: [],
};
const mockArtifacts = (mockChain) => {
return {
devtoolsLogs: {
[Audit.DEFAULT_PASS]: [],
},
requestNetworkRecords: () => {
return Promise.resolve([]);
},
requestMainResource: function() {
return Promise.resolve(mockChain);
},
};
};
describe('Performance: Redirects audit', () => {
let nodeTimings;
const mockArtifacts = (mockChain) => {
return {
traces: {},
devtoolsLogs: {},
requestLanternInteractive: () => ({pessimisticEstimate: {nodeTimings}}),
requestTraceOfTab: () => {},
requestNetworkRecords: () => [],
requestMainResource: function() {
return Promise.resolve(mockChain);
},
};
};
it('fails when 3 redirects detected', () => {
return Audit.audit(mockArtifacts(FAILING_THREE_REDIRECTS)).then(output => {
nodeTimings = new Map([
[{type: 'network', record: {url: 'http://example.com/'}}, {startTime: 0}],
[{type: 'network', record: {url: 'https://example.com/'}}, {startTime: 5000}],
[{type: 'network', record: {url: 'https://m.example.com/'}}, {startTime: 10000}],
[{type: 'network', record: {url: 'https://m.example.com/final'}}, {startTime: 17000}],
]);
return Audit.audit(mockArtifacts(FAILING_THREE_REDIRECTS), {}).then(output => {
assert.equal(output.score, 0);
assert.equal(output.details.items.length, 4);
assert.equal(output.rawValue, 17000);
});
});
it('fails when 2 redirects detected', () => {
return Audit.audit(mockArtifacts(FAILING_TWO_REDIRECTS)).then(output => {
nodeTimings = new Map([
[{type: 'network', record: {url: 'http://lisairish.com/'}}, {startTime: 0}],
[{type: 'network', record: {url: 'https://lisairish.com/'}}, {startTime: 300}],
[{type: 'network', record: {url: 'https://www.lisairish.com/'}}, {startTime: 638}],
]);
return Audit.audit(mockArtifacts(FAILING_TWO_REDIRECTS), {}).then(output => {
assert.equal(output.score, 0.65);
assert.equal(output.details.items.length, 3);
assert.equal(Math.round(output.rawValue), 638);
@ -89,7 +104,12 @@ describe('Performance: Redirects audit', () => {
});
it('passes when one redirect detected', () => {
return Audit.audit(mockArtifacts(SUCCESS_ONE_REDIRECT)).then(output => {
nodeTimings = new Map([
[{type: 'network', record: {url: 'https://lisairish.com/'}}, {startTime: 0}],
[{type: 'network', record: {url: 'https://www.lisairish.com/'}}, {startTime: 510}],
]);
return Audit.audit(mockArtifacts(SUCCESS_ONE_REDIRECT), {}).then(output => {
// If === 1 redirect, perfect score is expected, regardless of latency
assert.equal(output.score, 1);
// We will still generate a table and show wasted time
@ -99,7 +119,7 @@ describe('Performance: Redirects audit', () => {
});
it('passes when no redirect detected', () => {
return Audit.audit(mockArtifacts(SUCCESS_NOREDIRECT)).then(output => {
return Audit.audit(mockArtifacts(SUCCESS_NOREDIRECT), {}).then(output => {
assert.equal(output.score, 1);
assert.equal(output.details.items.length, 0);
assert.equal(output.rawValue, 0);

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

@ -17,6 +17,7 @@
"include": [
"lighthouse-cli/**/*.js",
"lighthouse-core/audits/audit.js",
"lighthouse-core/audits/redirects.js",
"lighthouse-core/audits/accessibility/**/*.js",
"lighthouse-core/audits/dobetterweb/**/*.js",
"lighthouse-core/audits/byte-efficiency/**/*.js",

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

@ -113,7 +113,7 @@ declare global {
requestSpeedline(trace: Trace): Promise<LH.Artifacts.Speedline>;
// Metrics.
requestConsistentlyInteractive(data: LH.Artifacts.MetricComputationDataInput): Promise<Artifacts.LanternMetric|Artifacts.Metric>;
requestInteractive(data: LH.Artifacts.MetricComputationDataInput): Promise<Artifacts.LanternMetric|Artifacts.Metric>;
requestEstimatedInputLatency(data: LH.Artifacts.MetricComputationDataInput): Promise<Artifacts.LanternMetric|Artifacts.Metric>;
requestFirstContentfulPaint(data: LH.Artifacts.MetricComputationDataInput): Promise<Artifacts.LanternMetric|Artifacts.Metric>;
requestFirstCPUIdle(data: LH.Artifacts.MetricComputationDataInput): Promise<Artifacts.LanternMetric|Artifacts.Metric>;
@ -121,7 +121,7 @@ declare global {
requestSpeedIndex(data: LH.Artifacts.MetricComputationDataInput): Promise<Artifacts.LanternMetric|Artifacts.Metric>;
// Lantern metrics.
requestLanternConsistentlyInteractive(data: LH.Artifacts.MetricComputationData): Promise<Artifacts.LanternMetric>;
requestLanternInteractive(data: LH.Artifacts.MetricComputationData): Promise<Artifacts.LanternMetric>;
requestLanternEstimatedInputLatency(data: LH.Artifacts.MetricComputationData): Promise<Artifacts.LanternMetric>;
requestLanternFirstContentfulPaint(data: LH.Artifacts.MetricComputationData): Promise<Artifacts.LanternMetric>;
requestLanternFirstCPUIdle(data: LH.Artifacts.MetricComputationData): Promise<Artifacts.LanternMetric>;