diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 322e35db80b6a21c6de3cf92824c37858a01ff74..2db8df155fa5751b3cae4d165ceb06d2e4b848f7 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -736,30 +736,82 @@ var policies = { "hasOffenders": true, "unit": 'bytes' }, - "imageOptimization": { - "tool": "redownload", + "imagesNotOptimized": { + "tool": "phantomas", "label": "Image optimization", "message": "

This metric measures the number of bytes that could be saved by optimizing images.

Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as Kraken.io or the excellent ImageOptim on Mac. For SVG images, you can use SVGOMG.

The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!

", - "isOkThreshold": 20480, + "isOkThreshold": 2048, "isBadThreshold": 204800, "isAbnormalThreshold": 307200, "hasOffenders": true, - "unit": 'bytes' + "unit": 'bytes', + "valueTransformFn": function(offenders) { + let totalGain = 0; + offenders.forEach((offender) => { + offender.gain = offender.fileSize - offender.newFileSize; + totalGain += offender.gain; + }); + return totalGain; + }, + "offendersTransformFn": function(offenders) { + return offenders; + } }, - "oldImageFormats": { + "imagesOldFormat": { "tool": "phantomas", "label": "Old image formats", - "message": "

Measures the number of bytes that could be saved by converting images to newer and more efficient formats. The best image format is generally AVIF and the second best is WebP.

Be careful, you need to provide fallback images for old browsers and search engine bots.

", - "isOkThreshold": 30720, + "message": "

This metric goes further than \"Image optimization\". Measures the number of bytes that could be saved by converting images to newer and more efficient formats. The best image format is generally AVIF and the second best is WebP.

Be careful, you need to provide fallback images for old browsers and search engine bots.

", + "isOkThreshold": 2048, "isBadThreshold": 307200, "isAbnormalThreshold": 512000, "hasOffenders": true, - "unit": 'bytes' + "unit": 'bytes', + "valueTransformFn": function(offenders) { + let totalGain = 0; + offenders.forEach((offender) => { + offender.gain = offender.fileSize - offender.newFileSize; + totalGain += offender.gain; + }); + return totalGain; + }, + "offendersTransformFn": function(offenders) { + return offenders; + } }, - "imagesTooLarge": { - "tool": "redownload", + "imagesScaledDown": { + "tool": "phantomas", "label": "Oversized images", - "message": "

This is the number of images with a width >1200px on mobile, >1800px on tablet, >2400 on desktop, >3200px on HD desktop. Try reducing their size.

Please ignore if the file is used as a sprite.

", + "message": "

This rule compares the number of pixels in a loaded images to the number of physical pixels it is displayed on. Then it estimates the number of KB that could be saved by serving it with the correct dimensions.

Of course, it is hard to serve perfect images for all screens. For this reason, this rule is quite permissive.

", + "isOkThreshold": 2048, + "isBadThreshold": 307200, + "isAbnormalThreshold": 512000, + "hasOffenders": true, + "unit": 'bytes', + "valueTransformFn": function(offenders) { + let totalGain = 0; + offenders.forEach((offender) => { + offender.gain = offender.fileSize - offender.newFileSize; + totalGain += offender.gain; + }); + return totalGain; + }, + "offendersTransformFn": function(offenders) { + return offenders; + } + }, + "imagesExcessiveDensity": { + "tool": "phantomas", + "label": "Excessive image density", + "message": "

Devices with very high pixel density screen (such as 3x or 4x) are programmed to load high density images. This is the normal behavior, however the human eye barely sees the difference over 2x. This metric alerts you if an image density is > 2.2x.

There is currently no browser functionnality to prevent the issue (for this reason its impact on global score is low). But you can build your own clever solution!

", + "isOkThreshold": 0, + "isBadThreshold": 10, + "isAbnormalThreshold": 20, + "hasOffenders": true + }, + "imagesWithIncorrectSizesParam": { + "tool": "phantomas", + "label": "Incorrect sizes parameter", + "message": "

When using an adaptative image with a srcset attribute and w values, it is important to correctly set the sizes attribute. Otherwise, the browser might pick the wrong image in the srcset.

The Responsive Image Linter extension for Chrome can help you further.

", "isOkThreshold": 0, "isBadThreshold": 5, "isAbnormalThreshold": 10, diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index 4ecdc42f39a2a4e1c2f33101adfc86b95a178797..3fa9add74b537febf54343d2f5b92adffbfe28c9 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -1,24 +1,37 @@ { + "globalScore": { + "pageWeight": 2, + "images": 2, + "domComplexity": 1, + "javascriptComplexity": 2, + "badJavascript": 2, + "jQuery": 0.5, + "cssComplexity": 0.5, + "badCSS": 1, + "fonts": 1, + "serverConfig": 1 + }, "categories": { "pageWeight": { - "label": "Page weight", + "label": "Network", "policies": { "totalWeight": 5, - "imageOptimization": 2, - "oldImageFormats": 2, - "imagesTooLarge": 1, "compression": 2, - "fileMinification": 2 + "fileMinification": 2, + "identicalFiles": 2, + "emptyRequests": 3, + "notFound": 2, + "domains": 3 } }, - "requests": { - "label": "Requests", + "images": { + "label": "Images", "policies": { - "totalRequests": 2, - "domains": 3, - "notFound": 2, - "identicalFiles": 2, - "emptyRequests": 3, + "imagesNotOptimized": 2, + "imagesOldFormat": 2, + "imagesScaledDown": 2, + "imagesExcessiveDensity": 0.25, + "imagesWithIncorrectSizesParam": 1, "lazyLoadableImagesBelowTheFold": 2, "hiddenImages": 1 } @@ -102,17 +115,5 @@ "cachingTooShort": 1 } } - }, - "globalScore": { - "pageWeight": 3, - "requests": 2, - "domComplexity": 2, - "javascriptComplexity": 2, - "badJavascript": 2, - "jQuery": 0.5, - "cssComplexity": 0.5, - "badCSS": 1, - "fonts": 1, - "serverConfig": 1 } } \ No newline at end of file diff --git a/lib/rulesChecker.js b/lib/rulesChecker.js index eaffd6e8ad64a659af16e018a2b5e74adb020389..f5f78240879ab86711b427201c7a161442428f3d 100644 --- a/lib/rulesChecker.js +++ b/lib/rulesChecker.js @@ -57,9 +57,16 @@ var RulesChecker = function() { data.toolsResults[policy.tool].offenders[metricName]) { offenders = data.toolsResults[policy.tool].offenders[metricName]; } + + // It is possible to declare a transformation function for the main metric value. + // The function should + if (policy.valueTransformFn) { + rule.value = policy.valueTransformFn(offenders); + } + var offendersObj = {}; - + // It is possible to declare a transformation function for the offenders. // The function should take an array of strings as single parameter and return a string. if (policy.offendersTransformFn) { diff --git a/lib/tools/phantomas/phantomasWrapper.js b/lib/tools/phantomas/phantomasWrapper.js index e71039e7dad45d31bd564a4589bada54a16c136f..ac2ac311565638be39b42b052541371e23acfa83 100644 --- a/lib/tools/phantomas/phantomasWrapper.js +++ b/lib/tools/phantomas/phantomasWrapper.js @@ -47,6 +47,7 @@ var PhantomasWrapper = function() { // Mandatory 'analyze-css': true, + 'analyze-images': true, 'ignoreSslErrors': true, // until Phantomas 2.1 'ignore-ssl-errors': true // for Phantomas >= 2.2 }; @@ -80,6 +81,12 @@ var PhantomasWrapper = function() { offenders: results.getAllOffenders() }; + // Special rules here + if (task.options.device !== 'phone') { + delete json.metrics.imagesExcessiveDensity; + delete json.offenders.imagesExcessiveDensity; + } + deferred.resolve(json); }). catch(res => { diff --git a/lib/tools/redownload/redownload.js b/lib/tools/redownload/redownload.js index 58fce88511a1e8495ec08d5b99dfadc0404113f4..dc5030068cd44ad18474f1229b771e0834106334 100644 --- a/lib/tools/redownload/redownload.js +++ b/lib/tools/redownload/redownload.js @@ -14,13 +14,13 @@ var async = require('async'); var request = require('request'); var md5 = require('md5'); -var imageOptimizer = require('./imageOptimizer'); +//var imageOptimizer = require('./imageOptimizer'); var fileMinifier = require('./fileMinifier'); var gzipCompressor = require('./gzipCompressor'); var brotliCompressor = require('./brotliCompressor'); var contentTypeChecker = require('./contentTypeChecker'); var fontAnalyzer = require('./fontAnalyzer'); -var imageDimensions = require('./imageDimensions'); +//var imageDimensions = require('./imageDimensions'); var Redownload = function() { @@ -76,9 +76,9 @@ var Redownload = function() { .then(contentTypeChecker.checkContentType) - .then(imageOptimizer.optimizeImage) + //.then(imageOptimizer.optimizeImage) - .then(imageDimensions.getDimensions) + //.then(imageDimensions.getDimensions) .then(fileMinifier.minifyFile) @@ -151,12 +151,12 @@ var Redownload = function() { }); // Image compression - offenders.imageOptimization = listImagesNotOptimized(results); - metrics.imageOptimization = offenders.imageOptimization.totalGain; + //offenders.imageOptimization = listImagesNotOptimized(results); + //metrics.imageOptimization = offenders.imageOptimization.totalGain; // Image width - offenders.imagesTooLarge = listImagesTooLarge(results, data.params.options.device); - metrics.imagesTooLarge = offenders.imagesTooLarge.length; + //offenders.imagesTooLarge = listImagesTooLarge(results, data.params.options.device); + //metrics.imagesTooLarge = offenders.imagesTooLarge.length; // File minification offenders.fileMinification = listFilesNotMinified(results); @@ -341,7 +341,7 @@ var Redownload = function() { } - function listImagesNotOptimized(requests) { + /*function listImagesNotOptimized(requests) { var results = { totalGain: 0, images: [] @@ -396,9 +396,9 @@ var Redownload = function() { } }); return results; - } + }*/ - function listImagesTooLarge(requests, device) { + /*function listImagesTooLarge(requests, device) { var results = []; requests.forEach(function(req) { @@ -423,7 +423,7 @@ var Redownload = function() { }); return results; - } + }*/ function listFilesNotMinified(requests) { diff --git a/package.json b/package.json index ef327b94fbfb701682911029afb34843aeb8da22..7192a98f5685f8731166ea046d3d6d0c1745e257 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "md5": "2.3.0", "meow": "5.0.0", "parse-color": "1.0.0", - "phantomas": "2.8.0", + "phantomas": "gmetais/phantomas#analyze-image", "q": "1.5.1", "request": "2.88.2", "sharp": "0.32.3", diff --git a/test/core/redownloadTest.js b/test/core/redownloadTest.js index ada3e22ce131efd37b1747fe5597340e684fb2d9..4da5f347ff0b2a61b11eba581e45765217926b91 100644 --- a/test/core/redownloadTest.js +++ b/test/core/redownloadTest.js @@ -82,17 +82,6 @@ describe('redownload', function() { data.toolsResults.redownload.offenders.totalWeight.byType.image.requests.length.should.equal(2); data.toolsResults.redownload.offenders.totalWeight.byType.other.requests.length.should.equal(1); - data.toolsResults.redownload.offenders.should.have.a.property('imageOptimization'); - data.toolsResults.redownload.offenders.imageOptimization.totalGain.should.be.above(0); - data.toolsResults.redownload.offenders.imageOptimization.images.length.should.equal(2); - - data.toolsResults.redownload.offenders.should.have.a.property('oldImageFormats'); - data.toolsResults.redownload.offenders.oldImageFormats.totalGain.should.be.above(0); - data.toolsResults.redownload.offenders.oldImageFormats.images.length.should.equal(1); - - data.toolsResults.redownload.offenders.should.have.a.property('imagesTooLarge'); - data.toolsResults.redownload.offenders.imagesTooLarge.length.should.equal(0); - data.toolsResults.redownload.offenders.should.have.a.property('compression'); data.toolsResults.redownload.offenders.compression.totalGain.should.be.above(0); data.toolsResults.redownload.offenders.compression.files.length.should.equal(5);