From f8ac5bc1c4aa0caed2922232151cdceacd82b878 Mon Sep 17 00:00:00 2001 From: Ward Peeters Date: Tue, 2 May 2017 23:15:31 +0200 Subject: [PATCH 1/3] Add time to first byte audit --- lighthouse-core/audits/time-to-firstbyte.js | 111 ++++++++++++++++++ .../test/audits/time-to-firstbyte-test.js | 89 ++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 lighthouse-core/audits/time-to-firstbyte.js create mode 100644 lighthouse-core/test/audits/time-to-firstbyte-test.js diff --git a/lighthouse-core/audits/time-to-firstbyte.js b/lighthouse-core/audits/time-to-firstbyte.js new file mode 100644 index 000000000000..04a15e1dc222 --- /dev/null +++ b/lighthouse-core/audits/time-to-firstbyte.js @@ -0,0 +1,111 @@ +/** + * @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 Formatter = require('../report/formatter'); +const URL = require('../lib/url-shim'); + +const TTFB_THRESHOLD = 200; +const TTFB_THRESHOLD_BUFFER = 15; + +class TTFBMetric extends Audit { + /** + * @return {!AuditMeta} + */ + static get meta() { + return { + category: 'Performance', + name: 'time-to-firstbyte', + description: 'Time To FirstByte', + helpText: 'Time To First Byte identifies the time at which your server sends a response.', + requiredArtifacts: ['URL', 'networkRecords'] + }; + } + + static caclulateTTFB(record) { + const timing = record._timing; + + return timing.receiveHeadersEnd - timing.sendEnd; + } + + /** + * @param {!Artifacts} artifacts + * @return {!AuditResult} + */ + static audit(artifacts) { + const networkRecords = artifacts.networkRecords[Audit.DEFAULT_PASS]; + const results = []; + const walk = (node) => { + const children = Object.keys(node); + + children.forEach(id => { + const child = node[id]; + + const networkRecord = networkRecords.find(record => { + return record._requestId === id; + }); + + if (networkRecord) { + const ttfb = TTFBMetric.caclulateTTFB(networkRecord); + results.push({ + url: URL.getDisplayName(networkRecord._url), + ttfb: `${Math.round(ttfb)}ms`, + rawTTFB: ttfb + }); + } + + if (child.children) { + walk(child.children); + } + }); + }; + + return artifacts.requestCriticalRequestChains(networkRecords).then(tree => { + walk(tree); + + const recordsOverBudget = results.filter(row => + row.rawTTFB > TTFB_THRESHOLD + TTFB_THRESHOLD_BUFFER); + let displayValue; + + if (recordsOverBudget.length) { + displayValue = recordsOverBudget.length + + ` critical request(s) went over the ${TTFB_THRESHOLD}ms threshold`; + } else { + displayValue = 'All critical server requests are fast.'; + } + + return { + rawValue: recordsOverBudget.length === 0, + displayValue, + extendedInfo: { + formatter: Formatter.SUPPORTED_FORMATS.TABLE, + value: { + results, + tableHeadings: { + url: 'Request URL', + ttfb: 'Time To First Byte', + }, + }, + }, + }; + }); + } +} + +module.exports = TTFBMetric; diff --git a/lighthouse-core/test/audits/time-to-firstbyte-test.js b/lighthouse-core/test/audits/time-to-firstbyte-test.js new file mode 100644 index 000000000000..9337231f336e --- /dev/null +++ b/lighthouse-core/test/audits/time-to-firstbyte-test.js @@ -0,0 +1,89 @@ +/** + * Copyright 2016 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 TimeToFirstByte = require('../../audits/time-to-firstbyte.js'); +const assert = require('assert'); + +/* eslint-env mocha */ +describe('Performance: time-to-firstbyte audit', () => { + it('fails when ttfb is higher than 215ms', () => { + const networkRecords = [ + {_url: 'https://google.com/', _requestId: '0', _timing: {receiveHeadersEnd: 500, sendEnd: 200}}, + {_url: 'https://google.com/styles.css', _requestId: '1', _timing: {receiveHeadersEnd: 414, sendEnd: 200}}, + {_url: 'https://google.com/image.jpg', _requestId: '2', _timing: {receiveHeadersEnd: 600, sendEnd: 400}}, + ]; + const artifacts = {networkRecords: {defaultPass: networkRecords}}; + + artifacts.requestCriticalRequestChains = () => { + return Promise.resolve( + { + '0': { + request: {url: networkRecords[0]._url}, + children: { + '1': { + request: {url: networkRecords[1]._url}, + children: {}, + } + }, + }, + '1': { + request: {url: networkRecords[2]._url}, + children: {}, + } + } + ); + }; + + TimeToFirstByte.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.displayValue.includes('1 request(s)')); + }); + }); + + it('succeeds when no request is under 215ms', () => { + const networkRecords = [ + {_url: 'https://google.com/', _requestId: '0', _timing: {receiveHeadersEnd: 300, sendEnd: 200}}, + {_url: 'https://google.com/styles.css', _requestId: '1', _timing: {receiveHeadersEnd: 414, sendEnd: 200}}, + {_url: 'https://google.com/image.jpg', _requestId: '2', _timing: {receiveHeadersEnd: 600, sendEnd: 400}}, + ]; + const artifacts = {networkRecords: {defaultPass: networkRecords}}; + + artifacts.requestCriticalRequestChains = () => { + return Promise.resolve( + { + '0': { + request: {url: networkRecords[0]._url}, + children: { + '1': { + request: {url: networkRecords[1]._url}, + children: {}, + } + }, + }, + '1': { + request: {url: networkRecords[2]._url}, + children: {}, + } + } + ); + }; + + TimeToFirstByte.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, true); + }); + }); +}); From 08c93ad61eb6941245e9633289e549d4baf62e1b Mon Sep 17 00:00:00 2001 From: Ward Peeters Date: Tue, 2 May 2017 23:15:51 +0200 Subject: [PATCH 2/3] Add time to first byte audit to the config --- lighthouse-core/config/default.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lighthouse-core/config/default.js b/lighthouse-core/config/default.js index 791084066d0f..725d1d606668 100644 --- a/lighthouse-core/config/default.js +++ b/lighthouse-core/config/default.js @@ -68,6 +68,7 @@ module.exports = { "load-fast-enough-for-pwa", "speed-index-metric", "estimated-input-latency", + "time-to-firstbyte", "time-to-interactive", "user-timings", "critical-request-chains", @@ -174,6 +175,10 @@ module.exports = { "expectedValue": 100, "weight": 1 }, + "time-to-firstbyte": { + "expectedValue": 100, + "weight": 1 + }, "time-to-interactive": { "expectedValue": 100, "weight": 1 @@ -615,6 +620,7 @@ module.exports = { {"id": "first-meaningful-paint", "weight": 5}, {"id": "speed-index-metric", "weight": 1}, {"id": "estimated-input-latency", "weight": 1}, + {"id": "time-to-firstbyte", "weight": 1}, {"id": "time-to-interactive", "weight": 5}, {"id": "link-blocking-first-paint", "weight": 0}, {"id": "script-blocking-first-paint", "weight": 0}, From 28fcba538afef8e80bf532a97dcb75d4ed25856e Mon Sep 17 00:00:00 2001 From: Ward Peeters Date: Wed, 3 May 2017 22:16:56 +0200 Subject: [PATCH 3/3] Review changes --- lighthouse-core/audits/time-to-firstbyte.js | 20 +++++++++---------- lighthouse-core/config/default.js | 4 ++-- .../test/audits/time-to-firstbyte-test.js | 2 +- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/lighthouse-core/audits/time-to-firstbyte.js b/lighthouse-core/audits/time-to-firstbyte.js index 04a15e1dc222..15e7ff089fba 100644 --- a/lighthouse-core/audits/time-to-firstbyte.js +++ b/lighthouse-core/audits/time-to-firstbyte.js @@ -32,9 +32,11 @@ class TTFBMetric extends Audit { return { category: 'Performance', name: 'time-to-firstbyte', - description: 'Time To FirstByte', - helpText: 'Time To First Byte identifies the time at which your server sends a response.', - requiredArtifacts: ['URL', 'networkRecords'] + description: 'Time To First Byte (TTFB)', + informative: true, + helpText: 'Time To First Byte identifies the time at which your server sends a response.' + + '[Learn more](https://developers.google.com/web/tools/chrome-devtools/network-performance/issues).', + requiredArtifacts: ['networkRecords'] }; } @@ -57,15 +59,13 @@ class TTFBMetric extends Audit { children.forEach(id => { const child = node[id]; - const networkRecord = networkRecords.find(record => { - return record._requestId === id; - }); + const networkRecord = networkRecords.find(record => record._requestId === id); if (networkRecord) { const ttfb = TTFBMetric.caclulateTTFB(networkRecord); results.push({ url: URL.getDisplayName(networkRecord._url), - ttfb: `${Math.round(ttfb)}ms`, + ttfb: `${Math.round(ttfb).toLocaleString()} ms`, rawTTFB: ttfb }); } @@ -80,14 +80,12 @@ class TTFBMetric extends Audit { walk(tree); const recordsOverBudget = results.filter(row => - row.rawTTFB > TTFB_THRESHOLD + TTFB_THRESHOLD_BUFFER); + row.rawTTFB > TTFB_THRESHOLD + TTFB_THRESHOLD_BUFFER); let displayValue; if (recordsOverBudget.length) { displayValue = recordsOverBudget.length + - ` critical request(s) went over the ${TTFB_THRESHOLD}ms threshold`; - } else { - displayValue = 'All critical server requests are fast.'; + ` critical request(s) went over the ${TTFB_THRESHOLD} ms threshold`; } return { diff --git a/lighthouse-core/config/default.js b/lighthouse-core/config/default.js index 725d1d606668..b0306a5d2168 100644 --- a/lighthouse-core/config/default.js +++ b/lighthouse-core/config/default.js @@ -176,7 +176,7 @@ module.exports = { "weight": 1 }, "time-to-firstbyte": { - "expectedValue": 100, + "expectedValue": true, "weight": 1 }, "time-to-interactive": { @@ -620,7 +620,6 @@ module.exports = { {"id": "first-meaningful-paint", "weight": 5}, {"id": "speed-index-metric", "weight": 1}, {"id": "estimated-input-latency", "weight": 1}, - {"id": "time-to-firstbyte", "weight": 1}, {"id": "time-to-interactive", "weight": 5}, {"id": "link-blocking-first-paint", "weight": 0}, {"id": "script-blocking-first-paint", "weight": 0}, @@ -628,6 +627,7 @@ module.exports = { {"id": "uses-optimized-images", "weight": 0}, {"id": "uses-request-compression", "weight": 0}, {"id": "uses-responsive-images", "weight": 0}, + {"id": "time-to-firstbyte", "weight": 0}, {"id": "total-byte-weight", "weight": 0}, {"id": "dom-size", "weight": 0}, {"id": "critical-request-chains", "weight": 0}, diff --git a/lighthouse-core/test/audits/time-to-firstbyte-test.js b/lighthouse-core/test/audits/time-to-firstbyte-test.js index 9337231f336e..1b50994b97ab 100644 --- a/lighthouse-core/test/audits/time-to-firstbyte-test.js +++ b/lighthouse-core/test/audits/time-to-firstbyte-test.js @@ -1,5 +1,5 @@ /** - * Copyright 2016 Google Inc. All rights reserved. + * 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.