|
| 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 ResourceSummary = require('../computed/resource-summary.js'); |
| 10 | +const i18n = require('../lib/i18n/i18n.js'); |
| 11 | + |
| 12 | +const UIStrings = { |
| 13 | + /** Title of a Lighthouse audit that compares the size and quantity of page resources against targets set by the user. These targets are thought of as "performance budgets" because these metrics impact page performance (i.e. how quickly a page loads). */ |
| 14 | + title: 'Performance budget', |
| 15 | + /** Description of a Lighthouse audit where a user sets budgets for the quantity and size of page resources. No character length limits. */ |
| 16 | + description: 'Keep the quantity and size of network requests under the targets ' + |
| 17 | + 'set by the provided performance budget.', |
| 18 | + /** [ICU Syntax] Entry in a data table identifying the number of network requests of a particular type. Count will be a whole number. String should be as short as possible to be able to fit well into the table. */ |
| 19 | + requestCountOverBudget: `{count, plural, |
| 20 | + =1 {1 request} |
| 21 | + other {# requests} |
| 22 | + }`, |
| 23 | +}; |
| 24 | + |
| 25 | +const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); |
| 26 | + |
| 27 | +/** @typedef {{count: number, size: number}} ResourceEntry */ |
| 28 | +/** @typedef {{resourceType: LH.Budget.ResourceType, label: string, requestCount: number, size: number, sizeOverBudget: number | undefined, countOverBudget: string | undefined}} BudgetItem */ |
| 29 | + |
| 30 | +class ResourceBudget extends Audit { |
| 31 | + /** |
| 32 | + * @return {LH.Audit.Meta} |
| 33 | + */ |
| 34 | + static get meta() { |
| 35 | + return { |
| 36 | + id: 'performance-budget', |
| 37 | + title: str_(UIStrings.title), |
| 38 | + description: str_(UIStrings.description), |
| 39 | + scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE, |
| 40 | + requiredArtifacts: ['devtoolsLogs', 'URL'], |
| 41 | + }; |
| 42 | + } |
| 43 | + |
| 44 | + /** |
| 45 | + * @param {LH.Budget.ResourceType} resourceType |
| 46 | + * @return {string} |
| 47 | + */ |
| 48 | + static getRowLabel(resourceType) { |
| 49 | + /** @type {Record<LH.Budget.ResourceType,string>} */ |
| 50 | + const strMappings = { |
| 51 | + 'total': i18n.UIStrings.totalResourceType, |
| 52 | + 'document': i18n.UIStrings.documentResourceType, |
| 53 | + 'script': i18n.UIStrings.scriptResourceType, |
| 54 | + 'stylesheet': i18n.UIStrings.stylesheetResourceType, |
| 55 | + 'image': i18n.UIStrings.imageResourceType, |
| 56 | + 'media': i18n.UIStrings.mediaResourceType, |
| 57 | + 'font': i18n.UIStrings.fontResourceType, |
| 58 | + 'other': i18n.UIStrings.otherResourceType, |
| 59 | + 'third-party': i18n.UIStrings.thirdPartyResourceType, |
| 60 | + }; |
| 61 | + return strMappings[resourceType]; |
| 62 | + } |
| 63 | + |
| 64 | + /** |
| 65 | + * @param {LH.Budget} budget |
| 66 | + * @param {Record<LH.Budget.ResourceType,ResourceEntry>} summary |
| 67 | + * @return {Array<BudgetItem>} |
| 68 | + */ |
| 69 | + static tableItems(budget, summary) { |
| 70 | + const resourceTypes = /** @type {Array<LH.Budget.ResourceType>} */ (Object.keys(summary)); |
| 71 | + return resourceTypes.map((resourceType) => { |
| 72 | + const label = str_(this.getRowLabel(resourceType)); |
| 73 | + const requestCount = summary[resourceType].count; |
| 74 | + const size = summary[resourceType].size; |
| 75 | + |
| 76 | + let sizeOverBudget; |
| 77 | + let countOverBudget; |
| 78 | + |
| 79 | + if (budget.resourceSizes) { |
| 80 | + const sizeBudget = budget.resourceSizes.find(b => b.resourceType === resourceType); |
| 81 | + if (sizeBudget && (size > (sizeBudget.budget * 1024))) { |
| 82 | + sizeOverBudget = size - (sizeBudget.budget * 1024); |
| 83 | + } |
| 84 | + } |
| 85 | + if (budget.resourceCounts) { |
| 86 | + const countBudget = budget.resourceCounts.find(b => b.resourceType === resourceType); |
| 87 | + if (countBudget && (requestCount > countBudget.budget)) { |
| 88 | + const requestDifference = requestCount - countBudget.budget; |
| 89 | + countOverBudget = str_(UIStrings.requestCountOverBudget, {count: requestDifference}); |
| 90 | + } |
| 91 | + } |
| 92 | + return { |
| 93 | + resourceType, |
| 94 | + label, |
| 95 | + requestCount, |
| 96 | + size, |
| 97 | + countOverBudget, |
| 98 | + sizeOverBudget, |
| 99 | + }; |
| 100 | + }).filter((row) => { |
| 101 | + // Only resources with budgets should be included in the table |
| 102 | + if (budget.resourceSizes) { |
| 103 | + if (budget.resourceSizes.some(b => b.resourceType === row.resourceType)) return true; |
| 104 | + } |
| 105 | + if (budget.resourceCounts) { |
| 106 | + if (budget.resourceCounts.some(b => b.resourceType === row.resourceType)) return true; |
| 107 | + } |
| 108 | + return false; |
| 109 | + }).sort((a, b) => { |
| 110 | + return (b.sizeOverBudget || 0) - (a.sizeOverBudget || 0); |
| 111 | + }); |
| 112 | + } |
| 113 | + |
| 114 | + /** |
| 115 | + * @param {LH.Artifacts} artifacts |
| 116 | + * @param {LH.Audit.Context} context |
| 117 | + * @return {Promise<LH.Audit.Product>} |
| 118 | + */ |
| 119 | + static async audit(artifacts, context) { |
| 120 | + const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS]; |
| 121 | + const summary = await ResourceSummary.request({devtoolsLog, URL: artifacts.URL}, context); |
| 122 | + const budget = context.settings.budgets ? context.settings.budgets[0] : undefined; |
| 123 | + |
| 124 | + if (!budget) { |
| 125 | + return { |
| 126 | + score: 0, |
| 127 | + notApplicable: true, |
| 128 | + }; |
| 129 | + } |
| 130 | + |
| 131 | + /** @type { LH.Audit.Details.Table['headings'] } */ |
| 132 | + const headers = [ |
| 133 | + {key: 'label', itemType: 'text', text: 'Resource Type'}, |
| 134 | + {key: 'requestCount', itemType: 'numeric', text: 'Requests'}, |
| 135 | + {key: 'size', itemType: 'bytes', text: 'Transfer Size'}, |
| 136 | + {key: 'countOverBudget', itemType: 'text', text: ''}, |
| 137 | + {key: 'sizeOverBudget', itemType: 'bytes', text: 'Over Budget'}, |
| 138 | + ]; |
| 139 | + |
| 140 | + return { |
| 141 | + details: Audit.makeTableDetails(headers, this.tableItems(budget, summary)), |
| 142 | + score: 1, |
| 143 | + }; |
| 144 | + } |
| 145 | +} |
| 146 | + |
| 147 | +module.exports = ResourceBudget; |
| 148 | +module.exports.UIStrings = UIStrings; |
0 commit comments