WebP Image Detection server-side con NodeJS e Sharp

WebP Image Detection server-side con NodeJS e Sharp

Capita molto spesso, a chi si occupa di informatica, di imbattersi nella realizzazione di una SEO efficace per il sito web di un cliente. E capita altrettanto spesso, di ritrovarsi abbastanza in difficoltà nel dover eseguire dei passaggi fondamentali che ne prescindono la buona riuscita.

Uno dei più importanti credo sia quello di restituire immagini correttamente scalate, leggere e del formato giusto.

Ebbene sì, non bastano solo dei buoni contenuti per salire nel Google PageRank. Bisogna regalare all’utente l’esperienza più veloce possibile.
Le immagini ricoprono un ruolo molto importante, essendo le principali responsabili dei rallentamenti nel caricamento di una pagina web.

Ecco il mio metodo rapido e veloce senza l’utilizzo di framework, direttamente con NodeJS e Sharp!

Nell’ultimo periodo mi sono imbattuto nel problema di dover migrare circa 200 mila immagini di un sito web su un server CDN creato ad HOC, al fine di “decentralizzare” endpoints / interfaccia / media.

Non mi voglio dilungare sul come ho realizzato il server o trasferito in prima istanza le immagini. Voglio parlare dello script che genera immagini perfettamente scalate, e nel formato più leggero del web: il webP.

Immaginiamo di avere il nostro server (linux) settato a dovere, con tutti i contenuti multimediali nelle varie cartelle. Ci spostiamo nella nostra “htdocs” e qui lanciamo i comando:

//In questa guida si predilige una conoscenza minima di npm e NodeJS
npm init -y 
npm install sharp

Crea un file denominato cdn.js (o come preferisci). Puoi farlo nel tuo editor preferito e poi caricarlo tramite ftp.

Inizialmente includeremo le librerie necessarie:

var http = require('http');
const fs = require('fs');
var sharp = require('sharp');

http

Crea il server e lo mette costantemente in ascolto di tutte le chiamate.

var server = http.createServer(function (req, res) {
// il nostro andrà qui
}).listen(3000, () => {
  console.log('Server in ascolto su http://localhost:3000.')
});

fs

Estrapoliamo il path del file direttamente prendendolo dall’url:

var filepath = __dirname + req.url;
if (fs.existsSync(filepath)) {
  // esegui le operazioni sul file
} else {
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify({ error: 'Immagine non trovata' }));
}

sharp

Utilizziamo sharp per convertire il file e restituire l’immagine nel formato più compatibile e con gli headers corretti.

sharp(filepath).webp({ lossless: true })
   .toBuffer()
   .then(data => {
      res.setHeader('Content-Type', 'image/webp');
      res.setHeader("Cache-Control", "public,max-age=31536000");
      res.setHeader("Last-Modified", mtime.toUTCString());
      res.end(data);
   })
   .catch(err => {
      res.setHeader('Content-Type', 'application/json');
      res.end(JSON.stringify({ error: 'Errore nel caricamento dell\'immagine.' 
   }));
});

Come scoprire se il browser che sta effettuando la richiesta supporta webP

Il webP è un formato immagine inventato da google che riesce a mantenere una qualità estremamente alta, pur riducendosi considerevolmente in peso.
(E come sappiamo, più un’immagine è leggera, più velocemente viene scaricata). Se vuoi approfondire ti rimando a https://developers.google.com/speed/webp/.

Tuttavia, ancora oggi, non tutti i browser supportano questo formato.
Molti programmatori si ritrovano nella situazione scomoda di dover scegliere fra un sito veloce o uno totalmente responsive.

Ci sono diverse soluzioni online che spiegano come ovviare a questo problema. La maggior parte di queste predilige un approccio client-side che a mio modo di vedere è sempre rischioso da utilizzare. Non si riesce quasi mai a coprire tutte le casistiche e quindi la totale responsività risulta difficilmente raggiungibile.

Per questo motivo, ho deciso di attuare un approccio server-side che mi permetterà di avere la più totale sicurezza sul servire o meno il formato.

Indagando nell’header ho scoperto che è proprio il browser stesso a dircelo nella sua request.
Basta controllare l’attributo accept e verificare che la stringa contenga il valore: image/webp.

Ecco come lo faccio io nel mio script:

var webp = false;
if (req.headers !== undefined && req.headers['accept'] !== undefined
    && req.headers['accept'].includes('image/webp')) {
   webp = true;
}

Tramite questo metodo, riusciremo a capire se è il caso di restituire immagini processate in webP o mantenere il formato originale (solitamente JPG o PNG).

Fatto ciò, verifico se effettuare una compressione basandomi solo sulla larghezza passata in input nell’url baseurl + /{width}, altrimenti lascio le dimensioni originali.

Ecco il codice completo, cdn.js:

var http = require('http');
var sharp = require('sharp');
const fs = require('fs');

function isNumeric(num) {
  return !isNaN(num)
}

var server = http.createServer(function (req, res) {

  if (req.method === "GET") {
    var filepath = __dirname + req.url;
    var arr = req.url.split("/").filter(function (el) { return el.length != 0 });
    if (arr.length === 0) {
      res.writeHead(301,
        { Location: 'https://www.infooggi.it' }
      );
      res.end();
    }
    else {
      var nomefile = '';
      var extension = '';
      var resizeScale = 0;
      var webp = false;
      if (req.headers !== undefined && req.headers['accept'] !== undefined
        && req.headers['accept'].includes('image/webp')) {
        webp = true;
      }
      var newArr = arr;
      if (isNumeric(arr[arr.length - 1]) && arr.length > 1) {
        resizeScale = arr[arr.length - 1];
        nomefile = arr[arr.length - 2];
        newArr.pop();
        filepath = __dirname + '/' + newArr.join('/');
      } else {
        nomefile = arr[arr.length - 1];
      }
      if (fs.existsSync(filepath)) {
        var stats = fs.statSync(filepath);
        var mtime = stats.mtime;
        var size = stats.size;
        arrNome = nomefile.split('.').filter(function (el) { return el.length != 0 });

        if (arrNome.length > 1) {
          extension = arrNome[arrNome.length - 1];
        }
        if (extension !== '') {
          if (resizeScale === 0) {
            if (webp) {
              sharp(filepath).webp({ lossless: true })
                .toBuffer()
                .then(data => {
                  res.setHeader('Content-Type', 'image/webp');
                  res.setHeader("Cache-Control", "public,max-age=31536000");
                  res.setHeader("Last-Modified", mtime.toUTCString());
                  res.end(
                    data
                  );
                })
                .catch(err => {
                  res.setHeader('Content-Type', 'application/json');
                  res.end(JSON.stringify({ error: 'Errore nel ridimensionamento' }));
                });
            } else {
              sharp(filepath)
                .toBuffer()
                .then(data => {
                  res.setHeader('Content-Type', 'image/' + extension);
                  res.setHeader("Cache-Control", "public,max-age=31536000");
                  res.setHeader("Last-Modified", mtime.toUTCString());
                  res.end(
                    data
                  );
                })
                .catch(err => {
                  res.setHeader('Content-Type', 'application/json');
                  res.end(JSON.stringify({ error: 'Errore nel ridimensionamento' }));
                });
            }
          } else {
            if (webp) {
              sharp(filepath).webp({ lossless: true })
                .resize(parseInt(resizeScale))
                .toBuffer()
                .then(data => {
                  res.setHeader('Content-Type', 'image/webp');
                  res.setHeader("Cache-Control", "public,max-age=31536000");
                  res.setHeader("Last-Modified", mtime.toUTCString());
                  res.end(
                    data
                  );
                })
                .catch(err => {
                  res.setHeader('Content-Type', 'application/json');
                  res.end(JSON.stringify({ error: 'Errore nel ridimensionamento' }));
                });
            } else {
              sharp(filepath)
                .resize(parseInt(resizeScale))
                .toBuffer()
                .then(data => {
                  res.setHeader('Content-Type', 'image/' + extension);
                  res.setHeader("Cache-Control", "public,max-age=31536000");
                  res.setHeader("Last-Modified", mtime.toUTCString());
                  res.end(
                    data
                  );
                })
                .catch(err => {
                  res.setHeader('Content-Type', 'application/json');
                  res.end(JSON.stringify({ error: 'Errore nel ridimensionamento' }));
                });
            }
          }
        }
      } else {
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify({ error: 'Immagine non trovata' }));
      }
    }
  }

}).listen(555, () => {
  console.log('Server in ascolto su http://localhost:555 ')
});

Diamo il via alle danze! 😀

Ora che il codice è pronto per essere eseguito, possiamo uplodare il nostro script nella cartella dalla quale abbiamo lanciato il comando di inizializzazione npm.

Ci colleghiamo al nostro server e posizionandoci nella cartella dello script, lanciamo il comando:

node cdn.js

A questo punto avremo lanciato lo script costantemente in ascolto sulla porta 555.

Non ci resta che effettuare due prove per capire se funziona correttamente. Testare un’immagine di prova presente nella root del progetto sul server con 2 browser differenti. Io ho provato con:

  • Safari (NON supportato)
  • Chrome (Supportato)

Utilizzando i seguenti indirizzi di prova:

Conclusioni

Google tiene molto conto della velocità di caricamento di una pagina web nel suo algoritmo di ranking. Se si vuole veramente fare la differenza è fondamentale tenere conto di questi piccoli dettagli che fanno la differenza.

Alla prossima!

Leave a Comment

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *