Skip to content

Commit

Permalink
core(modern-images): update to include AVIF estimates (#12682)
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickhulce authored Jun 30, 2021
1 parent d8d1d39 commit 557c527
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 37 deletions.
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/).',
};
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;
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;
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

0 comments on commit 557c527

Please sign in to comment.