New rule that converts images to WebP and AVIF and checks the gain
This commit is contained in:
@@ -52,7 +52,7 @@ var ContentTypeChecker = function() {
|
||||
// If it's an image or a font, then rewrite.
|
||||
if (foundType !== null && (foundType.type === 'image' || foundType.type === 'webfont' || foundType.type === 'json')) {
|
||||
if (foundType.type !== entry.type) {
|
||||
debug('Content type %s is wrong for %s. It should be %s.', entry.type, entry.ulr, foundType.type);
|
||||
debug('Content type %s is wrong for %s. It should be %s.', entry.type, entry.url, foundType.type);
|
||||
}
|
||||
rewriteContentType(entry, foundType);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
var debug = require('debug')('ylt:imageReformater');
|
||||
var sharp = require('sharp');
|
||||
|
||||
// Disable sharp cache to reduce the "disk is full" error on Amazon Lambda
|
||||
sharp.cache(false);
|
||||
|
||||
var ImageOptimizer = function() {
|
||||
|
||||
// https://www.industrialempathy.com/posts/avif-webp-quality-settings
|
||||
const WEBP_QUALITY = 82;
|
||||
const AVIF_QUALITY = 64;
|
||||
|
||||
async function reformatImage(entry) {
|
||||
if (!entry.weightCheck || !entry.weightCheck.bodyBuffer) {
|
||||
// No valid file available
|
||||
return entry;
|
||||
}
|
||||
|
||||
var fileSize = entry.weightCheck.uncompressedSize;
|
||||
debug('Let\'s try to convert %s to other image formats', entry.url);
|
||||
debug('Current file size is %d', fileSize);
|
||||
|
||||
var animated = await isAnimated(entry);
|
||||
debug('Check if the file is animated: %s', animated);
|
||||
|
||||
|
||||
if (isJPEG(entry) || isPNG(entry)) {
|
||||
debug('File is %s, let\'s try to convert it to WebP', entry.contentType);
|
||||
|
||||
try {
|
||||
|
||||
const webpFile = await convertToWebp(entry.weightCheck.bodyBuffer, animated);
|
||||
|
||||
if (webpFile) {
|
||||
var webpFileSize = webpFile.length;
|
||||
|
||||
debug('WebP transformation complete for %s', entry.url);
|
||||
debug('WebP size is %d bytes', webpFileSize);
|
||||
|
||||
if (webpFile.length > 0 && gainIsEnough(fileSize, webpFileSize)) {
|
||||
entry.weightCheck.webpSize = webpFileSize;
|
||||
debug('WebP size is %d bytes smaller (-%d%)', fileSize - webpFileSize, Math.round((fileSize - webpFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
} else {
|
||||
debug('Convertion to WebP didn\'t work');
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
debug('Error while converting to WebP, ignoring');
|
||||
}
|
||||
}
|
||||
|
||||
if (!animated && (isJPEG(entry) || isPNG(entry) || isWebP(entry))) {
|
||||
debug('File is %s and is not animated, let\'s try to convert it to AVIF', entry.contentType);
|
||||
|
||||
try {
|
||||
|
||||
const avifFile = await convertToAvif(entry.weightCheck.bodyBuffer);
|
||||
|
||||
if (avifFile) {
|
||||
var avifFileSize = avifFile.length;
|
||||
|
||||
debug('AVIF transformation complete for %s', entry.url);
|
||||
debug('AVIF size is %d bytes', avifFileSize);
|
||||
|
||||
if (avifFile.length > 0 && gainIsEnough(fileSize, avifFileSize)) {
|
||||
entry.weightCheck.avifSize = avifFileSize;
|
||||
debug('AVIF size is %d bytes smaller (-%d%)', fileSize - avifFileSize, Math.round((fileSize - avifFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
} else {
|
||||
debug('Convertion to AVIF didn\'t work');
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
debug('Error while converting to AVIF, ignoring');
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
async function convertToWebp(bodyBuffer, isAnimated) {
|
||||
return sharp(bodyBuffer, {animated: isAnimated})
|
||||
.webp({quality: WEBP_QUALITY, alphaQuality: WEBP_QUALITY})
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
async function convertToAvif(bodyBuffer) {
|
||||
return sharp(bodyBuffer)
|
||||
.webp({quality: AVIF_QUALITY})
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
// The gain is estimated of enough value if it's over 2KB or over 20%,
|
||||
// but it's ignored if is below 100 bytes
|
||||
function gainIsEnough(oldWeight, newWeight) {
|
||||
var gain = oldWeight - newWeight;
|
||||
var ratio = gain / oldWeight;
|
||||
return (gain > 2048 || (ratio > 0.2 && gain > 100));
|
||||
}
|
||||
|
||||
function isJPEG(entry) {
|
||||
return entry.isImage && entry.contentType === 'image/jpeg';
|
||||
}
|
||||
|
||||
function isPNG(entry) {
|
||||
return entry.isImage && entry.contentType === 'image/png';
|
||||
}
|
||||
|
||||
function isWebP(entry) {
|
||||
return entry.isImage && entry.contentType === 'image/webp';
|
||||
}
|
||||
|
||||
function entryTypeCanBeReformated(entry) {
|
||||
return isJPEG(entry) || isPNG(entry) || isWebP(entry);
|
||||
}
|
||||
|
||||
async function isAnimated(entry) {
|
||||
if (isWebP(entry)) {
|
||||
const metadata = await sharp(entry.weightCheck.bodyBuffer).metadata();
|
||||
return metadata.pages > 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
reformatImage: reformatImage,
|
||||
convertToWebp: convertToWebp,
|
||||
convertToAvif: convertToAvif,
|
||||
gainIsEnough: gainIsEnough,
|
||||
entryTypeCanBeReformated: entryTypeCanBeReformated,
|
||||
isAnimated: isAnimated
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new ImageOptimizer();
|
||||
@@ -15,6 +15,7 @@ var request = require('request');
|
||||
var md5 = require('md5');
|
||||
|
||||
var imageOptimizer = require('./imageOptimizer');
|
||||
var imageReformater = require('./imageReformater');
|
||||
var fileMinifier = require('./fileMinifier');
|
||||
var gzipCompressor = require('./gzipCompressor');
|
||||
var brotliCompressor = require('./brotliCompressor');
|
||||
@@ -78,6 +79,10 @@ var Redownload = function() {
|
||||
|
||||
.then(imageOptimizer.optimizeImage)
|
||||
|
||||
.then(function(entry) {
|
||||
return Q(imageReformater.reformatImage(entry));
|
||||
})
|
||||
|
||||
.then(imageDimensions.getDimensions)
|
||||
|
||||
.then(fileMinifier.minifyFile)
|
||||
@@ -91,7 +96,7 @@ var Redownload = function() {
|
||||
})
|
||||
|
||||
.then(function(newEntry) {
|
||||
debug('File %s - Redownloaded, optimized, minified, compressed, analyzed: done', entry.url);
|
||||
debug('File %s - Redownloaded, optimized, reformated, minified, compressed, analyzed: done', entry.url);
|
||||
|
||||
// For the progress bar
|
||||
doneCount ++;
|
||||
@@ -154,6 +159,10 @@ var Redownload = function() {
|
||||
offenders.imageOptimization = listImagesNotOptimized(results);
|
||||
metrics.imageOptimization = offenders.imageOptimization.totalGain;
|
||||
|
||||
// Old image formats
|
||||
offenders.oldImageFormats = listImagesWithOldFormats(results);
|
||||
metrics.oldImageFormats = offenders.oldImageFormats.totalGain;
|
||||
|
||||
// Image width
|
||||
offenders.imagesTooLarge = listImagesTooLarge(results, data.params.options.device);
|
||||
metrics.imagesTooLarge = offenders.imagesTooLarge.length;
|
||||
@@ -398,6 +407,41 @@ var Redownload = function() {
|
||||
return results;
|
||||
}
|
||||
|
||||
function listImagesWithOldFormats(requests) {
|
||||
var results = {
|
||||
totalGain: 0,
|
||||
images: []
|
||||
};
|
||||
|
||||
requests.forEach(function(req) {
|
||||
|
||||
if (req.weightCheck.bodySize > 0 &&
|
||||
imageReformater.entryTypeCanBeReformated(req) &&
|
||||
(req.weightCheck.webpSize > 0 || req.weightCheck.avifSize > 0)) {
|
||||
|
||||
var image = {
|
||||
url: req.url,
|
||||
originalWeigth: req.weightCheck.bodySize
|
||||
};
|
||||
|
||||
if (req.weightCheck.webpSize) {
|
||||
image.webpSize = req.weightCheck.webpSize;
|
||||
}
|
||||
if (req.weightCheck.avifSize) {
|
||||
image.avifSize = req.weightCheck.avifSize;
|
||||
}
|
||||
|
||||
var smallest = Math.min(image.webpSize || Infinity, image.avifSize || Infinity);
|
||||
image.gain = req.weightCheck.bodySize - smallest;
|
||||
|
||||
results.totalGain += image.gain;
|
||||
results.images.push(image);
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function listImagesTooLarge(requests, device) {
|
||||
var results = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user