Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions lighthouse-core/audits/resource-budget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* @license Copyright 2019 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 NetworkRecords = require('../computed/network-records.js');
const ComputedResourceSummary = require('../computed/resource-summary.js');
const i18n = require('../lib/i18n/i18n.js');
const MainResource = require('../computed/main-resource.js');
const Budget = require('../config/budget.js');

const UIStrings = {
/** Imperative title of a Lighthouse audit that tells the user to minimize the size and quantity of resources used to load the page. */
title: 'Keep request counts and file sizes small',
/** Description of a Lighthouse audit that tells the user that they can setup a budgets for the quantity and size of page resources. No character length limits. */
description: 'To set budgets for the quantity and size of page resources,' +
' add a budget.json file.',
/** [ICU Syntax] Label identifying the number of requests*/
requestCount: `{count, plural,
=1 {1 request}
other {# requests}
}`,
totalResourceType: 'Total',
/** Label for the 'Document' resource type. */
documentResourceType: 'Document',
/** Label for the 'script' resource type. */
scriptResourceType: 'Script',
/** Label for the 'stylesheet' resource type. */
stylesheetResourceType: 'Stylesheet',
/** Label for the 'image' resource type. */
imageResourceType: 'Image',
/** Label for the 'media' resource type. */
mediaResourceType: 'Media',
/** Label for the 'font' resource type. */
fontResourceType: 'Font',
/** Label for the 'other' resource type. */
otherResourceType: 'Other',
/** Label for the 'third-party' resource type. */
thirdPartyResourceType: 'Third-party',
};

const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);

class ResourceBudget extends Audit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'resource-budget',
title: str_(UIStrings.title),
description: str_(UIStrings.description),
scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
requiredArtifacts: ['devtoolsLogs'],
};
}

/**
* @param {Array<LH.Budget.ResourceBudget>|undefined} budget
* @param {number} measurement
* @param {LH.Budget.ResourceType} resourceType
* @return {number|undefined}
*/
static overBudgetAmount(budget, measurement, resourceType) {
if (budget === undefined) {
return undefined;
}
const resourceBudget = budget.find((b) => b.resourceType === resourceType);
if (resourceBudget === undefined) {
return undefined;
}
const overBudget = measurement - resourceBudget.budget;
return overBudget > 0 ? overBudget : undefined;
}

/**
* @param {LH.Budget | undefined} budget
* @return {LH.Audit.Details.Table['headings']}
*/
static tableHeadings(budget) {
/** @type {LH.Audit.Details.Table['headings']} */
const headers = [
{key: 'label', itemType: 'text', text: 'Resource Type'},
{key: 'count', itemType: 'numeric', text: 'Requests'},
{key: 'size', itemType: 'bytes', text: 'File Size'},
];

/** @type {LH.Audit.Details.Table['headings']} */
const budgetHeaders = [
{key: 'countOverBudget', itemType: 'text', text: ''},
{key: 'sizeOverBudget', itemType: 'bytes', text: 'Over Budget'},
];
return budget ? headers.concat(budgetHeaders) : headers;
}

/**
* @param {{resourceType: LH.Budget.ResourceType, count: number, size: number}} row
* @param {LH.Budget | undefined} budget
* @return {boolean}
*/
static shouldIncludeRow(row, budget) {
if (budget === undefined) {
return true;
} else {
const budgets = (budget.resourceCounts || []).concat(budget.resourceSizes || []);
return !!budgets.find((b) => {
return b.resourceType === row.resourceType;
});
}
}

/**
* @param {LH.Budget|undefined} budget
* @param {Object<string,{resourceType: LH.Budget.ResourceType, count: number, size: number}>} summary
* @return {Array<{label: string, count: number, size: number, countOverBudget?: string|undefined, sizeOverBudget?: number|undefined}>}
*/
static tableItems(budget, summary) {
/** @type {Object<LH.Budget.ResourceType,string>} */
const strMappings = {
'total': str_(UIStrings.totalResourceType),
'document': str_(UIStrings.documentResourceType),
'script': str_(UIStrings.scriptResourceType),
'stylesheet': str_(UIStrings.stylesheetResourceType),
'image': str_(UIStrings.imageResourceType),
'media': str_(UIStrings.mediaResourceType),
'font': str_(UIStrings.fontResourceType),
'other': str_(UIStrings.otherResourceType),
'third-party': str_(UIStrings.thirdPartyResourceType),
};

/** @type {Array<{label: string, count: number, size: number, countOverBudget?: string|undefined, sizeOverBudget?: number|undefined}>}*/
return Object.values(summary).filter((row) => {
return this.shouldIncludeRow(row, budget);
}).map((row) => {
const type = row.resourceType;

const overCount = this.overBudgetAmount(budget && budget.resourceCounts, row.count, type);
const overSize = this.overBudgetAmount(budget && budget.resourceSizes, row.size, type);

return {
label: strMappings[type],
count: row.count,
size: row.size,
countOverBudget: overCount !== undefined ?
str_(UIStrings.requestCount, {count: overCount}) : undefined,
sizeOverBudget: overSize,
};
}).sort((a, b) => {
const overBudgetComparison = (b.sizeOverBudget || 0) - (a.sizeOverBudget || 0);
const sizeComparison = b.size - a.size;
return budget ? overBudgetComparison : sizeComparison;
});
}

/**
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {Promise<LH.Audit.Product>}
*/
static async audit(artifacts, context) {
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
const networkRecords = await NetworkRecords.request(devtoolsLog, context);
const mainResource = await (MainResource.request({devtoolsLog, URL: artifacts.URL}, context));

/** @type{LH.Budget | undefined} */
const budget = (context.settings.budgets || []).reverse().find((budget) => {
return Budget.urlMatchesPattern(mainResource.url, budget.path);
});
const resourceSummary = ComputedResourceSummary.summarize(networkRecords, mainResource.url);

const headings = ResourceBudget.tableHeadings(budget);
const tableItems = ResourceBudget.tableItems(budget, resourceSummary);

return {
details: Audit.makeTableDetails(headings, tableItems),
score: 1,
};
}
}

module.exports = ResourceBudget;
module.exports.UIStrings = UIStrings;
2 changes: 2 additions & 0 deletions lighthouse-core/config/budget.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ class Budget {

if (b.resourceSizes !== undefined) {
budget.resourceSizes = b.resourceSizes.map((r) => {
// Users supply budgets in KB but Lighthouse uses bytes throughout
r.budget = r.budget * 1024;
return Budget.validateResourceBudget(r);
});
}
Expand Down
10 changes: 10 additions & 0 deletions lighthouse-core/config/default-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const i18n = require('../lib/i18n/i18n.js');
const UIStrings = {
/** Title of the Performance category of audits. Equivalent to 'Web performance', this term is inclusive of all web page speed and loading optimization topics. Also used as a label of a score gauge; try to limit to 20 characters. */
performanceCategoryTitle: 'Performance',
/** Title of the Budgets section of the Performance Category. 'Budgets' refers to a budget (like a financial budget), but applied to the amount of resources on a page, rather than money. */
budgetsGroupTitle: 'Budgets',
/** Description of the Budgets section of the Performance category. Within this section the budget results are displayed. */
budgetsGroupDescription: 'Performance budgets set standards for the performance of your site.',
/** Title of the speed metrics section of the Performance category. Within this section are various speed metrics which quantify the pageload performance into values presented in seconds and milliseconds. */
metricGroupTitle: 'Metrics',
/** Title of the opportunity section of the Performance category. Within this section are audits with imperative titles that suggest actions the user can take to improve the loading performance of their web page. 'Suggestion'/'Optimization'/'Recommendation' are reasonable synonyms for 'opportunity' in this case. */
Expand Down Expand Up @@ -192,6 +196,7 @@ const defaultConfig = {
'main-thread-tasks',
'metrics',
'offline-start-url',
'resource-budget',
'manual/pwa-cross-browser',
'manual/pwa-page-transitions',
'manual/pwa-each-page-has-url',
Expand Down Expand Up @@ -286,6 +291,10 @@ const defaultConfig = {
title: str_(UIStrings.loadOpportunitiesGroupTitle),
description: str_(UIStrings.loadOpportunitiesGroupDescription),
},
'budgets': {
title: str_(UIStrings.budgetsGroupTitle),
description: str_(UIStrings.budgetsGroupDescription),
},
'diagnostics': {
title: str_(UIStrings.diagnosticsGroupTitle),
description: str_(UIStrings.diagnosticsGroupDescription),
Expand Down Expand Up @@ -378,6 +387,7 @@ const defaultConfig = {
{id: 'bootup-time', weight: 0, group: 'diagnostics'},
{id: 'mainthread-work-breakdown', weight: 0, group: 'diagnostics'},
{id: 'font-display', weight: 0, group: 'diagnostics'},
{id: 'resource-budget', weight: 0, group: 'budgets'},
// Audits past this point don't belong to a group and will not be shown automatically
{id: 'network-requests', weight: 0},
{id: 'network-rtt', weight: 0},
Expand Down
56 changes: 56 additions & 0 deletions lighthouse-core/lib/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,54 @@
"message": "Avoid multiple page redirects",
"description": "Imperative title of a Lighthouse audit that tells the user to eliminate the redirects taken through multiple URLs to load the page. This is shown in a list of audits that Lighthouse generates."
},
"lighthouse-core/audits/resource-budget.js | description": {
"message": "To set budgets for the quantity and size of page resources, add a budget.json file.",
"description": "Description of a Lighthouse audit that tells the user that they can setup a budgets for the quantity and size of page resources. No character length limits."
},
"lighthouse-core/audits/resource-budget.js | documentResourceType": {
"message": "Document",
"description": "Label for the 'Document' resource type."
},
"lighthouse-core/audits/resource-budget.js | fontResourceType": {
"message": "Font",
"description": "Label for the 'font' resource type."
},
"lighthouse-core/audits/resource-budget.js | imageResourceType": {
"message": "Image",
"description": "Label for the 'image' resource type."
},
"lighthouse-core/audits/resource-budget.js | mediaResourceType": {
"message": "Media",
"description": "Label for the 'media' resource type."
},
"lighthouse-core/audits/resource-budget.js | otherResourceType": {
"message": "Other",
"description": "Label for the 'other' resource type."
},
"lighthouse-core/audits/resource-budget.js | requestCount": {
"message": "{count, plural,\n =1 {1 request}\n other {# requests}\n }",
"description": "[ICU Syntax] Label identifying the number of requests"
},
"lighthouse-core/audits/resource-budget.js | scriptResourceType": {
"message": "Script",
"description": "Label for the 'script' resource type."
},
"lighthouse-core/audits/resource-budget.js | stylesheetResourceType": {
"message": "Stylesheet",
"description": "Label for the 'stylesheet' resource type."
},
"lighthouse-core/audits/resource-budget.js | thirdPartyResourceType": {
"message": "Third-party",
"description": "Label for the 'third-party' resource type."
},
"lighthouse-core/audits/resource-budget.js | title": {
"message": "Keep request counts and file sizes small",
"description": "Imperative title of a Lighthouse audit that tells the user to minimize the size and quantity of resources used to load the page."
},
"lighthouse-core/audits/resource-budget.js | totalResourceType": {
"message": "Total",
"description": ""
},
"lighthouse-core/audits/seo/canonical.js | description": {
"message": "Canonical links suggest which URL to show in search results. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/canonical).",
"description": "Description of a Lighthouse audit that tells the user *why* they need to have a valid rel=canonical link. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation."
Expand Down Expand Up @@ -1103,6 +1151,14 @@
"message": "Tables and lists",
"description": "Title of the navigation section within the Accessibility category. Within this section are audits with descriptive titles that highlight opportunities to improve the experience of reading tabular or list data using assistive technology."
},
"lighthouse-core/config/default-config.js | budgetsGroupDescription": {
"message": "Performance budgets set standards for the performance of your site.",
"description": "Description of the Budgets section of the Performance category. Within this section the budget results are displayed."
},
"lighthouse-core/config/default-config.js | budgetsGroupTitle": {
"message": "Budgets",
"description": "Title of the Budgets section of the Performance Category. 'Budgets' refers to a budget (like a financial budget), but applied to the amount of resources on a page, rather than money."
},
"lighthouse-core/config/default-config.js | diagnosticsGroupDescription": {
"message": "More information about the performance of your application.",
"description": "Description of the diagnostics section of the Performance category. Within this section are audits with non-imperative titles that provide more detail on the page's page load performance characteristics. Whereas the 'Opportunities' suggest an action along with expected time savings, diagnostics do not. Within this section, the user may read the details and deduce additional actions they could take."
Expand Down
Loading