Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core(modern-images): update to include AVIF estimates #12682

Merged
merged 8 commits into from
Jun 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,15 @@ const expectations = [
},
'modern-image-formats': {
details: {
overallSavingsBytes: '>60000',
items: {
length: 6,
},
overallSavingsBytes: '137000 +/- 10000',
items: [
{url: /lighthouse-1024x680.jpg$/},
{url: /lighthouse-unoptimized.jpg$/},
{url: /lighthouse-480x320.jpg$/},
{url: /lighthouse-480x320.jpg\?attributesized/},
{url: /lighthouse-480x320.jpg\?css/},
{url: /lighthouse-480x320.jpg\?sprite/},
],
},
},
'uses-text-compression': {
Expand Down
67 changes: 53 additions & 14 deletions lighthouse-core/audits/byte-efficiency/modern-image-formats.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const UIStrings = {
/** Imperative title of a Lighthouse audit that tells the user to serve images in newer and more efficient image formats in order to enhance the performance of a page. A non-modern image format was designed 20+ years ago. This is displayed in a list of audit titles that Lighthouse generates. */
title: 'Serve images in next-gen formats',
/** Description of a Lighthouse audit that tells the user *why* they should use newer and more efficient image formats. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */
description: 'Image formats like JPEG 2000, JPEG XR, and WebP often provide better ' +
description: 'Image formats like WebP and AVIF often provide better ' +
'compression than PNG or JPEG, which means faster downloads and less data consumption. ' +
'[Learn more](https://web.dev/uses-webp-images/).',
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs updates here: GoogleChrome/web.dev#5696

};
Expand All @@ -40,16 +40,6 @@ class ModernImageFormats extends ByteEfficiencyAudit {
};
}

/**
* @param {{originalSize: number, webpSize: number}} image
* @return {{bytes: number, percent: number}}
*/
static computeSavings(image) {
const bytes = image.originalSize - image.webpSize;
const percent = 100 * bytes / image.originalSize;
return {bytes, percent};
}

/**
* @param {{naturalWidth: number, naturalHeight: number}} imageElement
* @return {number}
Expand All @@ -65,6 +55,38 @@ class ModernImageFormats extends ByteEfficiencyAudit {
return Math.round(totalPixels * expectedBytesPerPixel);
}

/**
* @param {{naturalWidth: number, naturalHeight: number}} imageElement
* @return {number}
*/
static estimateAvifSizeFromDimensions(imageElement) {
const totalPixels = imageElement.naturalWidth * imageElement.naturalHeight;
// See above for the rationale behind our 2 byte-per-pixel baseline and WebP ratio of 10:1.
// AVIF usually gives ~20% additional savings on top of that, so we will use 12:1.
// This is quite pessimistic as Netflix study shows a photographic compression ratio of ~40:1
// (0.4 *bits* per pixel at SSIM 0.97).
// https://netflixtechblog.com/avif-for-next-generation-image-coding-b1d75675fe4
const expectedBytesPerPixel = 2 * 1 / 12;
return Math.round(totalPixels * expectedBytesPerPixel);
}

/**
* @param {{jpegSize: number | undefined, webpSize: number | undefined}} otherFormatSizes
* @return {number|undefined}
*/
static estimateAvifSizeFromWebPAndJpegEstimates(otherFormatSizes) {
if (!otherFormatSizes.jpegSize || !otherFormatSizes.webpSize) return undefined;

// AVIF saves at least ~50% on JPEG, ~20% on WebP at low quality.
// http://downloads.aomedia.org/assets/pdf/symposium-2019/slides/CyrilConcolato_Netflix-AVIF-AOM-Research-Symposium-2019.pdf
// https://jakearchibald.com/2020/avif-has-landed/
// https://www.finally.agency/blog/what-is-avif-image-format
// See https://github.com/GoogleChrome/lighthouse/issues/12295#issue-840261460 for more.
const estimateFromJpeg = otherFormatSizes.jpegSize * 5 / 10;
patrickhulce marked this conversation as resolved.
Show resolved Hide resolved
const estimateFromWebp = otherFormatSizes.webpSize * 8 / 10;
return estimateFromJpeg / 2 + estimateFromWebp / 2;
}

/**
* @param {LH.Artifacts} artifacts
* @return {ByteEfficiencyAudit.ByteEfficiencyProduct}
Expand All @@ -86,7 +108,15 @@ class ModernImageFormats extends ByteEfficiencyAudit {
continue;
}

// Skip if the image was already using a modern format.
if (image.mimeType === 'image/webp' || image.mimeType === 'image/avif') continue;

const jpegSize = image.jpegSize;
let webpSize = image.webpSize;
let avifSize = ModernImageFormats.estimateAvifSizeFromWebPAndJpegEstimates({
jpegSize,
webpSize,
});
let fromProtocol = true;

if (typeof webpSize === 'undefined') {
Expand All @@ -107,21 +137,30 @@ class ModernImageFormats extends ByteEfficiencyAudit {
naturalHeight,
naturalWidth,
});
avifSize = ModernImageFormats.estimateAvifSizeFromDimensions({
naturalHeight,
naturalWidth,
});
fromProtocol = false;
}

if (image.originalSize < webpSize + IGNORE_THRESHOLD_IN_BYTES) continue;
if (webpSize === undefined || avifSize === undefined) continue;

// Visible wasted bytes uses AVIF, but we still include the WebP savings in the LHR.
const wastedWebpBytes = image.originalSize - webpSize;
patrickhulce marked this conversation as resolved.
Show resolved Hide resolved
const wastedBytes = image.originalSize - avifSize;
if (wastedBytes < IGNORE_THRESHOLD_IN_BYTES) continue;

const url = URL.elideDataURI(image.url);
const isCrossOrigin = !URL.originsMatch(pageURL, image.url);
const webpSavings = ModernImageFormats.computeSavings({...image, webpSize: webpSize});

items.push({
url,
fromProtocol,
isCrossOrigin,
totalBytes: image.originalSize,
wastedBytes: webpSavings.bytes,
wastedBytes,
wastedWebpBytes,
});
}

Expand Down
2 changes: 1 addition & 1 deletion lighthouse-core/lib/i18n/locales/en-US.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lighthouse-core/lib/i18n/locales/en-XL.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,23 @@ function generateArtifacts(images) {

describe('Page uses optimized images', () => {
it('ignores files when there is only insignificant savings', () => {
const artifacts = generateArtifacts([{originalSize: 5000, webpSize: 4500}]);
const artifacts = generateArtifacts([{originalSize: 5000, jpegSize: 10000, webpSize: 4500}]);
const auditResult = ModernImageFormats.audit_(artifacts);

expect(auditResult.items).toEqual([]);
});

it('flags files when there is only small savings', () => {
const artifacts = generateArtifacts([{originalSize: 15000, webpSize: 4500}]);
it('flags files using AVIF savings', () => {
const artifacts = generateArtifacts([{originalSize: 15000, jpegSize: 8000, webpSize: 5000}]);
const auditResult = ModernImageFormats.audit_(artifacts);

expect(auditResult.items).toEqual([
{
fromProtocol: true,
isCrossOrigin: false,
totalBytes: 15000,
wastedBytes: 15000 - 4500,
wastedBytes: 15000 - (5000 * 0.8),
wastedWebpBytes: 15000 - 5000,
url: 'http://google.com/image.jpeg',
},
]);
Expand All @@ -78,32 +79,44 @@ describe('Page uses optimized images', () => {
fromProtocol: false,
isCrossOrigin: false,
totalBytes: 1e6,
wastedBytes: 1e6 - 1000 * 1000 * 2 / 10,
wastedBytes: Math.round(1e6 - 1000 * 1000 * 2 / 12),
wastedWebpBytes: Math.round(1e6 - 1000 * 1000 * 2 / 10),
url: 'http://google.com/image.jpeg',
},
]);
});

it('estimates savings on cross-origin files', () => {
const artifacts = generateArtifacts([{
url: 'http://localhost:1234/image.jpeg', originalSize: 50000, webpSize: 20000,
url: 'http://localhost:1234/image.jpg', originalSize: 50000, jpegSize: 50000, webpSize: 20000,
}]);
const auditResult = ModernImageFormats.audit_(artifacts);

expect(auditResult.items).toMatchObject([
{
fromProtocol: true,
isCrossOrigin: true,
url: 'http://localhost:1234/image.jpeg',
url: 'http://localhost:1234/image.jpg',
},
]);
});

it('passes when all images are sufficiently optimized', () => {
const artifacts = generateArtifacts([
{type: 'png', originalSize: 50000, webpSize: 50001},
{type: 'jpeg', originalSize: 50000, webpSize: 50001},
{type: 'bmp', originalSize: 4000, webpSize: 2000},
{type: 'png', originalSize: 50000, jpegSize: 100001, webpSize: 60001},
{type: 'jpeg', originalSize: 50000, jpegSize: 100001, webpSize: 60001},
{type: 'bmp', originalSize: 4000, webpSize: 5001},
]);

const auditResult = ModernImageFormats.audit_(artifacts);

expect(auditResult.items).toEqual([]);
});

it('passes when all images are already using a modern format', () => {
const artifacts = generateArtifacts([
{type: 'webp', originalSize: 50000, jpegSize: 100000, webpSize: 100000},
{type: 'avif', originalSize: 50000, jpegSize: 100000, webpSize: 100000},
]);

const auditResult = ModernImageFormats.audit_(artifacts);
Expand All @@ -113,7 +126,7 @@ describe('Page uses optimized images', () => {

it('elides data URIs', () => {
const artifacts = generateArtifacts([
{type: 'data:webp', originalSize: 15000, webpSize: 4500},
{type: 'data:jpeg', originalSize: 15000, jpegSize: 10000, webpSize: 4500},
]);

const auditResult = ModernImageFormats.audit_(artifacts);
Expand Down
11 changes: 6 additions & 5 deletions lighthouse-core/test/results/sample_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -3823,12 +3823,12 @@
"modern-image-formats": {
"id": "modern-image-formats",
"title": "Serve images in next-gen formats",
"description": "Image formats like JPEG 2000, JPEG XR, and WebP often provide better compression than PNG or JPEG, which means faster downloads and less data consumption. [Learn more](https://web.dev/uses-webp-images/).",
"description": "Image formats like WebP and AVIF often provide better compression than PNG or JPEG, which means faster downloads and less data consumption. [Learn more](https://web.dev/uses-webp-images/).",
"score": 0.88,
"scoreDisplayMode": "numeric",
"numericValue": 150,
"numericUnit": "millisecond",
"displayValue": "Potential savings of 9 KiB",
"displayValue": "Potential savings of 13 KiB",
"warnings": [],
"details": {
"type": "opportunity",
Expand Down Expand Up @@ -3860,11 +3860,12 @@
"fromProtocol": true,
"isCrossOrigin": false,
"totalBytes": 24620,
"wastedBytes": 9028
"wastedBytes": 12989.45,
"wastedWebpBytes": 9028
}
],
"overallSavingsMs": 150,
"overallSavingsBytes": 9028
"overallSavingsBytes": 12989.45
}
},
"uses-optimized-images": {
Expand Down Expand Up @@ -8172,7 +8173,7 @@
},
{
"values": {
"wastedBytes": 9028
"wastedBytes": 12989.45
},
"path": "audits[modern-image-formats].displayValue"
},
Expand Down
5 changes: 4 additions & 1 deletion report/test/renderer/report-ui-features-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ describe('ReportUIFeatures', () => {
beforeAll(() => {
const lhr = JSON.parse(JSON.stringify(sampleResults));
lhr.requestedUrl = lhr.finalUrl = 'http://www.example.com';
const webpAuditItemTemplate = sampleResults.audits['modern-image-formats'].details.items[0];
const webpAuditItemTemplate = {
...sampleResults.audits['modern-image-formats'].details.items[0],
wastedBytes: 8.8 * 1024,
};
const renderBlockingAuditItemTemplate =
sampleResults.audits['render-blocking-resources'].details.items[0];
const textCompressionAuditItemTemplate =
Expand Down