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 { class Redirects extends Audit {
/** /**
* @return {!AuditMeta} * @return {LH.Audit.Meta}
*/ */
static get meta() { static get meta() {
return { return {
@ -20,68 +20,92 @@ class Redirects extends Audit {
failureDescription: 'Has multiple page redirects', failureDescription: 'Has multiple page redirects',
scoreDisplayMode: Audit.SCORING_MODES.NUMERIC, 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).', 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 * @param {LH.Artifacts} artifacts
* @return {!AuditResult} * @param {LH.Audit.Context} context
* @return {Promise<LH.Audit.Product>}
*/ */
static audit(artifacts) { static async audit(artifacts, context) {
const data = {URL: artifacts.URL, devtoolsLog: artifacts.devtoolsLogs[Audit.DEFAULT_PASS]}; const settings = context.settings;
return artifacts.requestMainResource(data) const trace = artifacts.traces[Audit.DEFAULT_PASS];
.then(mainResource => { const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
// redirects is only available when redirects happens
const redirectRequests = Array.from(mainResource.redirects || []);
// add main resource to redirectRequests so we can use it to calculate wastedMs const traceOfTab = await artifacts.requestTraceOfTab(trace);
redirectRequests.push(mainResource); const networkRecords = await artifacts.requestNetworkRecords(devtoolsLog);
const mainResource = await artifacts.requestMainResource({URL: artifacts.URL, devtoolsLog});
let totalWastedMs = 0; const metricComputationData = {trace, devtoolsLog, traceOfTab, networkRecords, settings};
const pageRedirects = []; const metricResult = await artifacts.requestLanternInteractive(metricComputationData);
// Kickoff the results table (with the initial request) if there are > 1 redirects /** @type {Map<string, LH.Gatherer.Simulation.NodeTiming>} */
if (redirectRequests.length > 1) { const nodeTimingsByUrl = new Map();
pageRedirects.push({ for (const [node, timing] of metricResult.pessimisticEstimate.nodeTimings.entries()) {
url: `(Initial: ${redirectRequests[0].url})`, if (node.type === 'network') {
wastedMs: 0, const networkNode = /** @type {LH.Gatherer.Simulation.GraphNetworkNode} */ (node);
}); nodeTimingsByUrl.set(networkNode.record.url, timing);
} }
}
for (let i = 1; i < redirectRequests.length; i++) { // redirects is only available when redirects happens
const initialRequest = redirectRequests[i - 1]; const redirectRequests = Array.from(mainResource.redirects || []);
const redirectedRequest = redirectRequests[i];
const wastedMs = (redirectedRequest.startTime - initialRequest.startTime) * 1000; // add main resource to redirectRequests so we can use it to calculate wastedMs
totalWastedMs += wastedMs; redirectRequests.push(mainResource);
pageRedirects.push({ let totalWastedMs = 0;
url: redirectedRequest.url, const pageRedirects = [];
wastedMs,
});
}
const headings = [ // Kickoff the results table (with the initial request) if there are > 1 redirects
{key: 'url', itemType: 'text', text: 'Redirected URL'}, if (redirectRequests.length > 1) {
{key: 'wastedMs', itemType: 'ms', text: 'Time for Redirect', granularity: 1}, pageRedirects.push({
]; url: `(Initial: ${redirectRequests[0].url})`,
const summary = {wastedMs: totalWastedMs}; wastedMs: 0,
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,
};
}); });
}
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 */ /* eslint-env mocha */
const FAILING_THREE_REDIRECTS = { const FAILING_THREE_REDIRECTS = {
startTime: 17, startTime: 17,
url: 'http://exampel.com/', url: 'https://m.example.com/final',
redirects: [ redirects: [
{ {
startTime: 0, startTime: 0,
@ -30,25 +30,25 @@ const FAILING_THREE_REDIRECTS = {
const FAILING_TWO_REDIRECTS = { const FAILING_TWO_REDIRECTS = {
startTime: 446.286, startTime: 446.286,
url: 'http://lisairish.com/', url: 'https://www.lisairish.com/',
redirects: [ redirects: [
{ {
startTime: 445.648, startTime: 445.648,
url: 'https://lisairish.com/', url: 'http://lisairish.com/',
}, },
{ {
startTime: 445.757, startTime: 445.757,
url: 'https://www.lisairish.com/', url: 'https://lisairish.com/',
}, },
], ],
}; };
const SUCCESS_ONE_REDIRECT = { const SUCCESS_ONE_REDIRECT = {
startTime: 136.383, startTime: 136.383,
url: 'https://lisairish.com/', url: 'https://www.lisairish.com/',
redirects: [{ redirects: [{
startTime: 135.873, startTime: 135.873,
url: 'https://www.lisairish.com/', url: 'https://lisairish.com/',
}], }],
}; };
@ -58,30 +58,45 @@ const SUCCESS_NOREDIRECT = {
redirects: [], redirects: [],
}; };
const mockArtifacts = (mockChain) => {
return {
devtoolsLogs: {
[Audit.DEFAULT_PASS]: [],
},
requestNetworkRecords: () => {
return Promise.resolve([]);
},
requestMainResource: function() {
return Promise.resolve(mockChain);
},
};
};
describe('Performance: Redirects audit', () => { 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', () => { 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.score, 0);
assert.equal(output.details.items.length, 4); assert.equal(output.details.items.length, 4);
assert.equal(output.rawValue, 17000); assert.equal(output.rawValue, 17000);
}); });
}); });
it('fails when 2 redirects detected', () => { 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.score, 0.65);
assert.equal(output.details.items.length, 3); assert.equal(output.details.items.length, 3);
assert.equal(Math.round(output.rawValue), 638); assert.equal(Math.round(output.rawValue), 638);
@ -89,7 +104,12 @@ describe('Performance: Redirects audit', () => {
}); });
it('passes when one redirect detected', () => { 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 // If === 1 redirect, perfect score is expected, regardless of latency
assert.equal(output.score, 1); assert.equal(output.score, 1);
// We will still generate a table and show wasted time // We will still generate a table and show wasted time
@ -99,7 +119,7 @@ describe('Performance: Redirects audit', () => {
}); });
it('passes when no redirect detected', () => { 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.score, 1);
assert.equal(output.details.items.length, 0); assert.equal(output.details.items.length, 0);
assert.equal(output.rawValue, 0); assert.equal(output.rawValue, 0);

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

@ -17,6 +17,7 @@
"include": [ "include": [
"lighthouse-cli/**/*.js", "lighthouse-cli/**/*.js",
"lighthouse-core/audits/audit.js", "lighthouse-core/audits/audit.js",
"lighthouse-core/audits/redirects.js",
"lighthouse-core/audits/accessibility/**/*.js", "lighthouse-core/audits/accessibility/**/*.js",
"lighthouse-core/audits/dobetterweb/**/*.js", "lighthouse-core/audits/dobetterweb/**/*.js",
"lighthouse-core/audits/byte-efficiency/**/*.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>; requestSpeedline(trace: Trace): Promise<LH.Artifacts.Speedline>;
// Metrics. // 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>; requestEstimatedInputLatency(data: LH.Artifacts.MetricComputationDataInput): Promise<Artifacts.LanternMetric|Artifacts.Metric>;
requestFirstContentfulPaint(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>; 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>; requestSpeedIndex(data: LH.Artifacts.MetricComputationDataInput): Promise<Artifacts.LanternMetric|Artifacts.Metric>;
// Lantern metrics. // 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>; requestLanternEstimatedInputLatency(data: LH.Artifacts.MetricComputationData): Promise<Artifacts.LanternMetric>;
requestLanternFirstContentfulPaint(data: LH.Artifacts.MetricComputationData): Promise<Artifacts.LanternMetric>; requestLanternFirstContentfulPaint(data: LH.Artifacts.MetricComputationData): Promise<Artifacts.LanternMetric>;
requestLanternFirstCPUIdle(data: LH.Artifacts.MetricComputationData): Promise<Artifacts.LanternMetric>; requestLanternFirstCPUIdle(data: LH.Artifacts.MetricComputationData): Promise<Artifacts.LanternMetric>;