diff --git a/lighthouse-core/audits/time-to-first-byte.js b/lighthouse-core/audits/time-to-first-byte.js new file mode 100644 index 000000000000..fc3cc7fd4754 --- /dev/null +++ b/lighthouse-core/audits/time-to-first-byte.js @@ -0,0 +1,71 @@ +/** + * @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 Util = require('../report/v2/renderer/util'); + +const TTFB_THRESHOLD = 600; + +class TTFBMetric extends Audit { + /** + * @return {!AuditMeta} + */ + static get meta() { + return { + category: 'Performance', + name: 'time-to-first-byte', + description: 'Keep server response times low (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: ['devtoolsLogs', 'URL'] + }; + } + + static caclulateTTFB(record) { + const timing = record._timing; + + return timing.receiveHeadersEnd - timing.sendEnd; + } + + /** + * @param {!Artifacts} artifacts + * @return {!AuditResult} + */ + static audit(artifacts) { + const devtoolsLogs = artifacts.devtoolsLogs[Audit.DEFAULT_PASS]; + + return artifacts.requestNetworkRecords(devtoolsLogs) + .then((networkRecords) => { + let debugString = ''; + + const finalUrl = artifacts.URL.finalUrl; + const finalUrlRequest = networkRecords.find(record => record._url === finalUrl); + const ttfb = TTFBMetric.caclulateTTFB(finalUrlRequest); + const passed = ttfb < TTFB_THRESHOLD; + + if (!passed) { + debugString = `Root document took ${Util.formatMilliseconds(ttfb, 1)} ms` + + 'to get the first byte.'; + } + + return { + rawValue: ttfb, + score: passed, + displayValue: Util.formatMilliseconds(ttfb), + extendedInfo: { + value: { + wastedMs: ttfb - TTFB_THRESHOLD, + }, + }, + debugString, + }; + }); + } +} + +module.exports = TTFBMetric; diff --git a/lighthouse-core/audits/time-to-firstbyte.js b/lighthouse-core/audits/time-to-firstbyte.js deleted file mode 100644 index 5dab4d6ade22..000000000000 --- a/lighthouse-core/audits/time-to-firstbyte.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * @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 Util = require('../report/v2/renderer/util'); -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 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'] - }; - } - - 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 => record._requestId === id); - - if (networkRecord) { - const ttfb = TTFBMetric.caclulateTTFB(networkRecord); - results.push({ - url: URL.getURLDisplayName(networkRecord._url), - ttfb: Util.formatMilliseconds(Math.round(ttfb), 1), - 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) { - const thresholdDisplay = Util.formatMiliseconds(TTFB_THRESHOLD, 1); - const recordsOverBudgetDisplay = Util.formatNumber(recordsOverBudget.length); - displayValue = `${recordsOverBudgetDisplay} critical request(s) went over` + - ` the ${thresholdDisplay} threshold`; - } - - return { - rawValue: recordsOverBudget.length === 0, - displayValue, - extendedInfo: { - value: { - results, - tableHeadings: { - url: 'Request URL', - ttfb: 'Time To First Byte', - }, - }, - }, - }; - }); - } -} - -module.exports = TTFBMetric; diff --git a/lighthouse-core/config/default.js b/lighthouse-core/config/default.js index d273c674d8b1..b291716c258d 100644 --- a/lighthouse-core/config/default.js +++ b/lighthouse-core/config/default.js @@ -72,7 +72,7 @@ module.exports = { 'speed-index-metric', 'screenshot-thumbnails', 'estimated-input-latency', - // 'time-to-firstbyte', + 'time-to-first-byte', 'first-interactive', 'consistently-interactive', 'user-timings', @@ -232,7 +232,7 @@ module.exports = { {id: 'uses-optimized-images', weight: 0, group: 'perf-hint'}, {id: 'uses-webp-images', weight: 0, group: 'perf-hint'}, {id: 'uses-request-compression', weight: 0, group: 'perf-hint'}, - // {id: 'time-to-firstbyte', weight: 0, group: 'perf-hint'}, + {id: 'time-to-first-byte', weight: 0, group: 'perf-hint'}, {id: 'total-byte-weight', weight: 0, group: 'perf-info'}, {id: 'dom-size', weight: 0, group: 'perf-info'}, {id: 'critical-request-chains', weight: 0, group: 'perf-info'}, diff --git a/lighthouse-core/test/audits/time-to-first-byte-test.js b/lighthouse-core/test/audits/time-to-first-byte-test.js new file mode 100644 index 000000000000..a55ea2bf16c7 --- /dev/null +++ b/lighthouse-core/test/audits/time-to-first-byte-test.js @@ -0,0 +1,48 @@ +/** + * @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 TimeToFirstByte = require('../../audits/time-to-first-byte.js'); +const assert = require('assert'); + +/* eslint-env mocha */ +describe('Performance: time-to-first-byte audit', () => { + it('fails when ttfb of root document is higher than 600ms', () => { + const networkRecords = [ + {_url: 'https://example.com/', _requestId: '0', _timing: {receiveHeadersEnd: 830, sendEnd: 200}}, + {_url: 'https://google.com/styles.css', _requestId: '1', _timing: {receiveHeadersEnd: 450, sendEnd: 200}}, + {_url: 'https://google.com/image.jpg', _requestId: '2', _timing: {receiveHeadersEnd: 600, sendEnd: 400}}, + ]; + const artifacts = { + devtoolsLogs: {[TimeToFirstByte.DEFAULT_PASS]: []}, + requestNetworkRecords: () => Promise.resolve(networkRecords), + URL: {finalUrl: 'https://example.com/'}, + }; + + return TimeToFirstByte.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, 630); + assert.strictEqual(result.score, false); + }); + }); + + it('succeeds when ttfb of root document is lower than 600ms', () => { + const networkRecords = [ + {_url: 'https://example.com/', _requestId: '0', _timing: {receiveHeadersEnd: 400, sendEnd: 200}}, + {_url: 'https://google.com/styles.css', _requestId: '1', _timing: {receiveHeadersEnd: 850, sendEnd: 200}}, + {_url: 'https://google.com/image.jpg', _requestId: '2', _timing: {receiveHeadersEnd: 1000, sendEnd: 400}}, + ]; + const artifacts = { + devtoolsLogs: {[TimeToFirstByte.DEFAULT_PASS]: []}, + requestNetworkRecords: () => Promise.resolve(networkRecords), + URL: {finalUrl: 'https://example.com/'}, + }; + + return TimeToFirstByte.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, 200); + assert.strictEqual(result.score, true); + }); + }); +}); diff --git a/lighthouse-core/test/audits/time-to-firstbyte-test.js b/lighthouse-core/test/audits/time-to-firstbyte-test.js deleted file mode 100644 index 828cc9514c99..000000000000 --- a/lighthouse-core/test/audits/time-to-firstbyte-test.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @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 TimeToFirstByte = require('../../audits/time-to-firstbyte.js'); -const assert = require('assert'); - -/* eslint-env mocha */ -describe('Performance: time-to-firstbyte audit', () => { - it.skip('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: {}, - } - } - ); - }; - - return TimeToFirstByte.audit(artifacts).then(result => { - assert.strictEqual(result.rawValue, false); - assert.ok(result.displayValue.includes('1 request(s)')); - }); - }); - - it.skip('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: {}, - } - } - ); - }; - - return TimeToFirstByte.audit(artifacts).then(result => { - assert.strictEqual(result.rawValue, true); - }); - }); -});