|
| 1 | +/** |
| 2 | + * @license Copyright 2019 Google Inc. All Rights Reserved. |
| 3 | + * 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 |
| 4 | + * 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. |
| 5 | + */ |
| 6 | +'use strict'; |
| 7 | + |
| 8 | +const Audit = require('./audit.js'); |
| 9 | +const TimingSummary = require('../computed/metrics/timing-summary.js'); |
| 10 | +const MainResource = require('../computed/main-resource.js'); |
| 11 | +const Budget = require('../config/budget.js'); |
| 12 | +const i18n = require('../lib/i18n/i18n.js'); |
| 13 | + |
| 14 | +const UIStrings = { |
| 15 | + /** Title of a Lighthouse audit that compares how quickly the page loads against targets set by the user. Timing budgets are a type of performance budget. */ |
| 16 | + title: 'Timing budget', |
| 17 | + /** Description of a Lighthouse audit where a user sets budgets for how quickly the page loads. No character length limits. 'Learn More' becomes link text to additional documentation. */ |
| 18 | + description: 'Set a timing budget to help you keep an eye on the performance of your site. Performant sites load fast and respond to user input events quickly. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/budgets).', |
| 19 | + /** Label for a column in a data table; entries will be the names of different timing metrics, e.g. "Time to Interactive", "First Contentful Paint", etc. */ |
| 20 | + columnTimingMetric: 'Metric', |
| 21 | + /** Label for a column in a data table; entries will be the measured value of a particular timing metric. Most entries will have a unit of milliseconds, but units could be other things as well. */ |
| 22 | + columnMeasurement: 'Measurement', |
| 23 | +}; |
| 24 | + |
| 25 | +const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); |
| 26 | + |
| 27 | +/** @typedef {{metric: LH.Budget.TimingMetric, label: string, measurement?: number, overBudget?: number}} BudgetItem */ |
| 28 | + |
| 29 | +class TimingBudget extends Audit { |
| 30 | + /** |
| 31 | + * @return {LH.Audit.Meta} |
| 32 | + */ |
| 33 | + static get meta() { |
| 34 | + return { |
| 35 | + id: 'timing-budget', |
| 36 | + title: str_(UIStrings.title), |
| 37 | + description: str_(UIStrings.description), |
| 38 | + scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE, |
| 39 | + requiredArtifacts: ['devtoolsLogs', 'traces', 'URL'], |
| 40 | + }; |
| 41 | + } |
| 42 | + |
| 43 | + /** |
| 44 | + * @param {LH.Budget.TimingMetric} timingMetric |
| 45 | + * @return {string} |
| 46 | + */ |
| 47 | + static getRowLabel(timingMetric) { |
| 48 | + /** @type {Record<LH.Budget.TimingMetric, string>} */ |
| 49 | + const strMappings = { |
| 50 | + 'first-contentful-paint': i18n.UIStrings.firstContentfulPaintMetric, |
| 51 | + 'first-cpu-idle': i18n.UIStrings.firstCPUIdleMetric, |
| 52 | + 'interactive': i18n.UIStrings.interactiveMetric, |
| 53 | + 'first-meaningful-paint': i18n.UIStrings.firstMeaningfulPaintMetric, |
| 54 | + 'max-potential-fid': i18n.UIStrings.maxPotentialFIDMetric, |
| 55 | + 'estimated-input-latency': i18n.UIStrings.estimatedInputLatencyMetric, |
| 56 | + 'total-blocking-time': i18n.UIStrings.totalBlockingTimeMetric, |
| 57 | + 'speed-index': i18n.UIStrings.speedIndexMetric, |
| 58 | + }; |
| 59 | + return str_(strMappings[timingMetric]); |
| 60 | + } |
| 61 | + |
| 62 | + /** |
| 63 | + * @param {LH.Budget.TimingMetric} timingMetric |
| 64 | + * @param {LH.Artifacts.TimingSummary} summary |
| 65 | + * @return {number|undefined} |
| 66 | + */ |
| 67 | + static getMeasurement(timingMetric, summary) { |
| 68 | + /** @type {Record<LH.Budget.TimingMetric, number|undefined>} */ |
| 69 | + const measurements = { |
| 70 | + 'first-contentful-paint': summary.firstContentfulPaint, |
| 71 | + 'first-cpu-idle': summary.firstCPUIdle, |
| 72 | + 'interactive': summary.interactive, |
| 73 | + 'first-meaningful-paint': summary.firstMeaningfulPaint, |
| 74 | + 'max-potential-fid': summary.maxPotentialFID, |
| 75 | + 'estimated-input-latency': summary.estimatedInputLatency, |
| 76 | + 'total-blocking-time': summary.totalBlockingTime, |
| 77 | + 'speed-index': summary.speedIndex, |
| 78 | + }; |
| 79 | + return measurements[timingMetric]; |
| 80 | + } |
| 81 | + |
| 82 | + /** |
| 83 | + * @param {LH.Budget} budget |
| 84 | + * @param {LH.Artifacts.TimingSummary} summary |
| 85 | + * @return {Array<BudgetItem>} |
| 86 | + */ |
| 87 | + static tableItems(budget, summary) { |
| 88 | + if (!budget.timings) { |
| 89 | + return []; |
| 90 | + } |
| 91 | + return budget.timings.map((timingBudget) => { |
| 92 | + const metricName = timingBudget.metric; |
| 93 | + const label = this.getRowLabel(metricName); |
| 94 | + const measurement = this.getMeasurement(metricName, summary); |
| 95 | + const overBudget = measurement && (measurement > timingBudget.budget) |
| 96 | + ? (measurement - timingBudget.budget) : undefined; |
| 97 | + return { |
| 98 | + metric: metricName, |
| 99 | + label, |
| 100 | + measurement, |
| 101 | + overBudget, |
| 102 | + }; |
| 103 | + }).sort((a, b) => { |
| 104 | + return (b.overBudget || 0) - (a.overBudget || 0); |
| 105 | + }); |
| 106 | + } |
| 107 | + |
| 108 | + /** |
| 109 | + * @param {LH.Artifacts} artifacts |
| 110 | + * @param {LH.Audit.Context} context |
| 111 | + * @return {Promise<LH.Audit.Product>} |
| 112 | + */ |
| 113 | + static async audit(artifacts, context) { |
| 114 | + const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS]; |
| 115 | + const trace = artifacts.traces[Audit.DEFAULT_PASS]; |
| 116 | + const mainResource = await MainResource.request({URL: artifacts.URL, devtoolsLog}, context); |
| 117 | + const summary = (await TimingSummary.request({trace, devtoolsLog}, context)).metrics; |
| 118 | + const budget = Budget.getMatchingBudget(context.settings.budgets, mainResource.url); |
| 119 | + |
| 120 | + if (!budget) { |
| 121 | + return { |
| 122 | + score: 0, |
| 123 | + notApplicable: true, |
| 124 | + }; |
| 125 | + } |
| 126 | + |
| 127 | + /** @type {LH.Audit.Details.Table['headings']} */ |
| 128 | + const headers = [ |
| 129 | + {key: 'label', itemType: 'text', text: str_(UIStrings.columnTimingMetric)}, |
| 130 | + /** |
| 131 | + * Note: SpeedIndex, unlike other timing metrics, is not measured in milliseconds. |
| 132 | + * The renderer applies the correct units to the 'measurement' and 'overBudget' columns for SpeedIndex. |
| 133 | + */ |
| 134 | + {key: 'measurement', itemType: 'ms', text: str_(UIStrings.columnMeasurement)}, |
| 135 | + {key: 'overBudget', itemType: 'ms', text: str_(i18n.UIStrings.columnOverBudget)}, |
| 136 | + ]; |
| 137 | + |
| 138 | + return { |
| 139 | + details: Audit.makeTableDetails(headers, this.tableItems(budget, summary)), |
| 140 | + score: 1, |
| 141 | + }; |
| 142 | + } |
| 143 | +} |
| 144 | + |
| 145 | +module.exports = TimingBudget; |
| 146 | +module.exports.UIStrings = UIStrings; |
0 commit comments