Skip to content

Commit

Permalink
core(redirects): surface client-side redirects (#11027)
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickhulce committed Jul 13, 2020
1 parent 259e222 commit 2a02410
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,18 @@ const expectations = [
requestedUrl: `http://localhost:10200/online-only.html?delay=2000&redirect=%2Fredirects-final.html`,
finalUrl: 'http://localhost:10200/redirects-final.html',
audits: {
'first-contentful-paint': {
numericValue: '>=2000',
},
'interactive': {
numericValue: '>=2000',
},
'speed-index': {
numericValue: '>=2000',
},
'redirects': {
score: 1,
numericValue: '>=2000',
details: {
items: {
length: 2,
Expand All @@ -36,6 +46,15 @@ const expectations = [
requestedUrl: `http://localhost:10200/online-only.html?delay=1000&redirect_count=3&redirect=%2Fredirects-final.html`,
finalUrl: 'http://localhost:10200/redirects-final.html',
audits: {
'first-contentful-paint': {
numericValue: '>=3000',
},
'interactive': {
numericValue: '>=3000',
},
'speed-index': {
numericValue: '>=3000',
},
'redirects': {
score: '<1',
details: {
Expand All @@ -50,6 +69,38 @@ const expectations = [
],
},
},
{
// Client-side redirect (2s + 5s), paints at 2s, server-side redirect (1s)
// TODO: Assert performance metrics on client-side redirects, see https://github.com/GoogleChrome/lighthouse/pull/10325
lhr: {
requestedUrl: `http://localhost:10200/js-redirect.html?delay=2000&jsDelay=5000&jsRedirect=%2Fonline-only.html%3Fdelay%3D1000%26redirect%3D%2Fredirects-final.html`,
finalUrl: 'http://localhost:10200/redirects-final.html',
audits: {
// Just captures the server-side at the moment, should be 8s in the future
'first-contentful-paint': {
numericValue: '>=1000',
},
'interactive': {
numericValue: '>=1000',
},
'speed-index': {
numericValue: '>=1000',
},
'redirects': {
score: '<1',
numericValue: '>=8000',
details: {
items: {
length: 3,
},
},
},
},
runWarnings: [
/The page may not be loading as expected because your test URL \(.*js-redirect.html.*\) was redirected to .*redirects-final.html. Try testing the second URL directly./,
],
},
},
{
// Client-side redirect (2s + 5s), no paint
// TODO: Assert performance metrics on client-side redirects, see https://github.com/GoogleChrome/lighthouse/pull/10325
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ module.exports = {
extends: 'lighthouse:default',
settings: {
onlyAudits: [
'first-contentful-paint',
'interactive',
'speed-index',
'redirects',
],
// Use provided throttling method to test usage of correct navStart.
throttlingMethod: 'provided',
},
};
82 changes: 61 additions & 21 deletions lighthouse-core/audits/redirects.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,47 @@ class Redirects extends Audit {
};
}

/**
* This method generates the document request chain including client-side and server-side redirects.
*
* Example:
* GET /initialUrl => 302 /firstRedirect
* GET /firstRedirect => 200 /firstRedirect, window.location = '/secondRedirect'
* GET /secondRedirect => 302 /finalUrl
* GET /finalUrl => 200 /finalUrl
*
* Returns network records [/initialUrl, /firstRedirect, /secondRedirect, /thirdRedirect, /finalUrl]
*
* @param {LH.Artifacts.NetworkRequest} mainResource
* @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
* @param {LH.Artifacts.TraceOfTab} traceOfTab
* @return {Array<LH.Artifacts.NetworkRequest>}
*/
static getDocumentRequestChain(mainResource, networkRecords, traceOfTab) {
/** @type {Array<LH.Artifacts.NetworkRequest>} */
const documentRequests = [];

// Find all the document requests by examining navigation events and their redirects
for (const event of traceOfTab.processEvents) {
if (event.name !== 'navigationStart') continue;

const data = event.args.data || {};
if (!data.documentLoaderURL || !data.isLoadingMainFrame) continue;

let networkRecord = networkRecords.find(record => record.url === data.documentLoaderURL);
while (networkRecord) {
documentRequests.push(networkRecord);
networkRecord = networkRecord.redirectDestination;
}
}

// If we found documents in the trace, just use this directly.
if (documentRequests.length) return documentRequests;

// Use the main resource as a backup if we didn't find any modern navigationStart events
return (mainResource.redirects || []).concat(mainResource);
}

/**
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
Expand All @@ -62,38 +103,35 @@ class Redirects extends Audit {
}
}

// 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
redirectRequests.push(mainResource);
const documentRequests = Redirects.getDocumentRequestChain(
mainResource, networkRecords, traceOfTab);

let totalWastedMs = 0;
const pageRedirects = [];
const tableRows = [];

// 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,
});
}
// Iterate through all the document requests and report how much time was wasted until the
// next document request was issued. The final document request will have a `wastedMs` of 0.
for (let i = 0; i < documentRequests.length; i++) {
// If we didn't have enough documents for at least 1 redirect, just skip this loop.
if (documentRequests.length < 2) break;

for (let i = 1; i < redirectRequests.length; i++) {
const initialRequest = redirectRequests[i - 1];
const redirectedRequest = redirectRequests[i];
const initialRequest = documentRequests[i];
const redirectedRequest = documentRequests[i + 1] || initialRequest;

const initialTiming = nodeTimingsByUrl.get(initialRequest.url);
const redirectedTiming = nodeTimingsByUrl.get(redirectedRequest.url);
if (!initialTiming || !redirectedTiming) {
throw new Error('Could not find redirects in graph');
}

const wastedMs = redirectedTiming.startTime - initialTiming.startTime;
const lanternTimingDeltaMs = redirectedTiming.startTime - initialTiming.startTime;
const observedTimingDeltaS = redirectedRequest.startTime - initialRequest.startTime;
const wastedMs = settings.throttlingMethod === 'simulate' ?
lanternTimingDeltaMs : observedTimingDeltaS * 1000;
totalWastedMs += wastedMs;

pageRedirects.push({
url: redirectedRequest.url,
tableRows.push({
url: initialRequest.url,
wastedMs,
});
}
Expand All @@ -103,11 +141,13 @@ class Redirects extends Audit {
{key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
{key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnTimeSpent)},
];
const details = Audit.makeOpportunityDetails(headings, pageRedirects, totalWastedMs);
const details = Audit.makeOpportunityDetails(headings, tableRows, totalWastedMs);

return {
// We award a passing grade if you only have 1 redirect
score: redirectRequests.length <= 2 ? 1 : UnusedBytes.scoreForWastedMs(totalWastedMs),
// TODO(phulce): reconsider if cases like the example in https://github.com/GoogleChrome/lighthouse/issues/8984
// should fail this audit.
score: documentRequests.length <= 2 ? 1 : UnusedBytes.scoreForWastedMs(totalWastedMs),
numericValue: totalWastedMs,
numericUnit: 'millisecond',
displayValue: totalWastedMs ?
Expand Down
96 changes: 84 additions & 12 deletions lighthouse-core/test/audits/redirects-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ const FAILING_THREE_REDIRECTS = [{
timing: {receiveHeadersEnd: 11},
}, {
requestId: '1:redirect',
startTime: 11,
startTime: 1,
priority: 'VeryHigh',
url: 'https://example.com/',
timing: {receiveHeadersEnd: 12},
}, {
requestId: '1:redirect:redirect',
startTime: 12,
startTime: 2,
priority: 'VeryHigh',
url: 'https://m.example.com/',
timing: {receiveHeadersEnd: 17},
}, {
requestId: '1:redirect:redirect:redirect',
startTime: 17,
startTime: 3,
priority: 'VeryHigh',
url: 'https://m.example.com/final',
timing: {receiveHeadersEnd: 19},
Expand Down Expand Up @@ -80,6 +80,30 @@ const SUCCESS_NOREDIRECT = [{
timing: {receiveHeadersEnd: 140},
}];

const FAILING_CLIENTSIDE = [
{
requestId: '1',
startTime: 445,
priority: 'VeryHigh',
url: 'http://lisairish.com/',
timing: {receiveHeadersEnd: 446},
},
{
requestId: '1:redirect',
startTime: 446,
priority: 'VeryHigh',
url: 'https://lisairish.com/',
timing: {receiveHeadersEnd: 447},
},
{
requestId: '2',
startTime: 447,
priority: 'VeryHigh',
url: 'https://www.lisairish.com/',
timing: {receiveHeadersEnd: 448},
},
];

describe('Performance: Redirects audit', () => {
const mockArtifacts = (networkRecords, finalUrl) => {
const devtoolsLog = networkRecordsToDevtoolsLog(networkRecords);
Expand All @@ -91,23 +115,71 @@ describe('Performance: Redirects audit', () => {
};
};

it('fails when client-side redirects detected', async () => {
const context = {settings: {}, computedCache: new Map()};
const artifacts = mockArtifacts(FAILING_CLIENTSIDE, 'https://www.lisairish.com/');

const traceEvents = artifacts.traces.defaultPass.traceEvents;
const navStart = traceEvents.find(e => e.name === 'navigationStart');
const secondNavStart = JSON.parse(JSON.stringify(navStart));
traceEvents.push(secondNavStart);
navStart.args.data.isLoadingMainFrame = true;
navStart.args.data.documentLoaderURL = 'http://lisairish.com/';
secondNavStart.ts++;
secondNavStart.args.data.isLoadingMainFrame = true;
secondNavStart.args.data.documentLoaderURL = 'https://www.lisairish.com/';

const output = await RedirectsAudit.audit(artifacts, context);
expect(output.details.items).toHaveLength(3);
expect(Math.round(output.score * 100) / 100).toMatchInlineSnapshot(`0.35`);
expect(output.numericValue).toMatchInlineSnapshot(`2000`);
});

it('uses lantern timings when throttlingMethod is simulate', async () => {
const artifacts = mockArtifacts(FAILING_THREE_REDIRECTS, 'https://m.example.com/final');
const context = {settings: {throttlingMethod: 'simulate'}, computedCache: new Map()};
const output = await RedirectsAudit.audit(artifacts, context);
expect(output.details.items).toHaveLength(4);
expect(output.details.items.map(item => [item.url, item.wastedMs])).toMatchInlineSnapshot(`
Array [
Array [
"http://example.com/",
630,
],
Array [
"https://example.com/",
480,
],
Array [
"https://m.example.com/",
780,
],
Array [
"https://m.example.com/final",
0,
],
]
`);
expect(output.numericValue).toMatchInlineSnapshot(`1890`);
});

it('fails when 3 redirects detected', () => {
const artifacts = mockArtifacts(FAILING_THREE_REDIRECTS, 'https://m.example.com/final');
const context = {settings: {}, computedCache: new Map()};
return RedirectsAudit.audit(artifacts, context).then(output => {
assert.equal(Math.round(output.score * 100) / 100, 0.37);
assert.equal(output.details.items.length, 4);
assert.equal(output.numericValue, 1890);
expect(output.details.items).toHaveLength(4);
expect(Math.round(output.score * 100) / 100).toMatchInlineSnapshot(`0.24`);
expect(output.numericValue).toMatchInlineSnapshot(`3000`);
});
});

it('fails when 2 redirects detected', () => {
const artifacts = mockArtifacts(FAILING_TWO_REDIRECTS, 'https://www.lisairish.com/');
const context = {settings: {}, computedCache: new Map()};
return RedirectsAudit.audit(artifacts, context).then(output => {
assert.equal(Math.round(output.score * 100) / 100, 0.46);
assert.equal(output.details.items.length, 3);
assert.equal(Math.round(output.numericValue), 1110);
expect(output.details.items).toHaveLength(3);
expect(Math.round(output.score * 100) / 100).toMatchInlineSnapshot(`0.35`);
expect(output.numericValue).toMatchInlineSnapshot(`2000`);
});
});

Expand All @@ -116,10 +188,10 @@ describe('Performance: Redirects audit', () => {
const context = {settings: {}, computedCache: new Map()};
return RedirectsAudit.audit(artifacts, context).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
assert.equal(output.details.items.length, 2);
assert.equal(Math.round(output.numericValue), 780);
expect(output.details.items).toHaveLength(2);
expect(output.score).toEqual(1);
expect(output.numericValue).toMatchInlineSnapshot(`1000`);
});
});

Expand Down

0 comments on commit 2a02410

Please sign in to comment.