Vai al contenuto

Migliorare le prestazioni del sito con i “data loading” pattern

Avere buone prestazioni per un sito web è un passaggio criciale per garantire un’esperienza utente positiva. I data loading patterns si riferiscono alle strategie utilizzate per caricare e visualizzare i dati in modo efficiente. In questo articolo, vengono forniti alcuni suggerimenti generali per migliorare le prestazioni di un sito web applicando per l’appunto un pattern di caricamento dati:

Lazy Loading

Il “Lazy Loading” (caricamento pigro) è una tecnica di caricamento dei contenuti solo quando sono necessari, piuttosto che caricarli tutti immediatamente al caricamento iniziale della pagina, migliorando così significativamente i tempi di caricamento iniziali e riducendo la quantità di risorse richieste all’utente all’avvio del sito.

Un caso comune in cui il Lazy Loading viene applicato è per le immagini. Supponiamo di avere una pagina web con molte immagini, alcune delle quali potrebbero non essere visibili all’utente finché non scorre verso il basso nella pagina. Invece di caricare tutte le immagini contemporaneamente, possiamo ritardare il caricamento di quelle che non sono immediatamente visibili.

Ecco un esempio semplice di Lazy Loading delle immagini utilizzando HTML e JavaScript:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Lazy Loading</title>
    <style>
        .placeholder {
            width: 300px;
            height: 200px;
            background-color: #eee;
        }
    </style>
</head>
<body>

<!-- Immagine con Lazy Loading -->
<img class="lazy-load" data-src="image.jpg" alt="Lazy Loaded Image" width="300" height="200">

<!-- Altre parti della tua pagina -->

<script>
    // Funzione per caricare l'immagine quando è visibile
    function lazyLoad() {
        const lazyImages = document.querySelectorAll('.lazy-load');

        lazyImages.forEach((image) => {
            if (isElementInViewport(image)) {
                image.src = image.getAttribute('data-src');
                image.classList.remove('lazy-load');
            }
        });
    }

    // Funzione per verificare se un elemento è visibile nel viewport
    function isElementInViewport(el) {
        const rect = el.getBoundingClientRect();
        return (
            rect.top >= 0 &&
            rect.left >= 0 &&
            rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
            rect.right <= (window.innerWidth || document.documentElement.clientWidth)
        );
    }

    // Aggiungi un listener per l'evento di scroll per chiamare lazyLoad
    window.addEventListener('scroll', lazyLoad);

    // Carica le immagini visibili all'avvio della pagina
    document.addEventListener('DOMContentLoaded', lazyLoad);
</script>

</body>
</html>

In questo esempio:

  • La classe lazy-load viene assegnata a ogni immagine che vuoi caricare in modo pigro.
  • L’attributo data-src contiene il percorso dell’immagine da caricare.
  • Una classe di stile .placeholder può essere utilizzata per creare un’area vuota con le dimensioni dell’immagine, in modo che la pagina non cambi di dimensioni quando l’immagine viene finalmente caricata.

Quando l’utente scorre la pagina o carica la pagina iniziale, le immagini saranno caricate solo quando diventano visibili nel viewport.

Paginazione

La paginazione è una tecnica che suddivide grandi set di dati in pagine più piccole, consentendo agli utenti di navigare tra di esse senza dover caricare tutto il contenuto in una sola volta. Questo è particolarmente utile quando si gestiscono grandi elenchi di elementi, come risultati di ricerca, elenchi di prodotti o post in un blog.

Ecco un esempio di paginazione utilizzando HTML, JavaScript e un po’ di CSS:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pagination</title>
    <style>
        .pagination {
            display: flex;
            list-style: none;
            padding: 0;
        }

        .pagination li {
            margin: 5px;
            padding: 8px;
            border: 1px solid #ddd;
            cursor: pointer;
        }

        .pagination li.active {
            background-color: #007bff;
            color: #fff;
        }
    </style>
</head>
<body>

<div id="content"></div>
<ul class="pagination" id="pagination"></ul>

<script>
    const itemsPerPage = 5; // Numero di elementi per pagina
    const data = [...]; // I tuoi dati, ad esempio un array di elementi

    function displayItems(page) {
        const startIndex = (page - 1) * itemsPerPage;
        const endIndex = startIndex + itemsPerPage;
        const itemsToDisplay = data.slice(startIndex, endIndex);

        // Logica per visualizzare gli elementi nella pagina
        const contentContainer = document.getElementById('content');
        contentContainer.innerHTML = ''; // Pulisci il contenitore

        itemsToDisplay.forEach(item => {
            const itemElement = document.createElement('div');
            itemElement.textContent = item; // Usa la tua logica per visualizzare l'elemento
            contentContainer.appendChild(itemElement);
        });
    }

    function updatePagination(page) {
        const totalPages = Math.ceil(data.length / itemsPerPage);
        const paginationContainer = document.getElementById('pagination');
        paginationContainer.innerHTML = '';

        for (let i = 1; i <= totalPages; i++) {
            const pageItem = document.createElement('li');
            pageItem.textContent = i;
            pageItem.addEventListener('click', () => {
                displayItems(i);
                updatePagination(i);
            });

            if (i === page) {
                pageItem.classList.add('active');
            }

            paginationContainer.appendChild(pageItem);
        }
    }

    // Visualizza la prima pagina all'avvio
    document.addEventListener('DOMContentLoaded', () => {
        displayItems(1);
        updatePagination(1);
    });
</script>

</body>
</html>

In questo esempio:

  • itemsPerPage definisce quanti elementi sono visualizzati per pagina.
  • data rappresenta l’insieme completo di dati che vuoi paginare.
  • La funzione displayItems mostra gli elementi corrispondenti alla pagina corrente.
  • La funzione updatePagination aggiorna la barra di paginazione in base alla pagina corrente e aggiunge un evento di click a ciascun elemento della paginazione.
  • La paginazione viene visualizzata come una lista di numeri, e l’utente può fare clic su un numero per visualizzare la pagina corrispondente.

Cache dei dati

La cache dei dati è una tecnica utilizzata per archiviare temporaneamente le risorse (come file, immagini, script o dati) in un’area di memoria, in modo che possano essere recuperate rapidamente senza dover essere recuperate nuovamente dalla sorgente originale. Ci sono diversi tipi di cache che possono essere implementati su un sito web:

  1. Cache del Browser: i browser mantengono una cache locale per le risorse scaricate durante la navigazione. Quando una pagina web viene caricata, il browser controlla prima se le risorse richieste sono già presenti nella cache locale. Se sì, il browser può utilizzare le versioni in cache anziché scaricarle nuovamente.
    <!-- Esempio di direttiva di controllo della cache nel tag head -->
    <head>
        <meta http-equiv="Cache-Control" content="max-age=3600">
    </head>
  2. Cache del Server: le cache del server archiviano risorse lato server, riducendo così il carico sulla sorgente originale quando una richiesta viene effettuata. Ciò può essere implementato attraverso un server proxy, come ad esempio l’uso di cache di HTTP reverse proxy come Varnish o Nginx.
  3. Cache su Database: se il sito web utilizza un database per archiviare dati dinamici, è possibile implementare una cache sul livello database. I risultati delle query possono essere memorizzati temporaneamente nella cache per evitare l’esecuzione di query costose più volte e più volte.
  4. Cache delle API: se il sito web dipende da API esterne, possiamo implementare una cache delle risposte API. Questo riduce la dipendenza dalle risorse esterne e migliora le prestazioni del sito.
  5. Cache dei Contenuti Statici: cache specifiche per risorse statiche come file CSS, JavaScript, e immagini. Questo può essere implementato attraverso un Content Delivery Network (CDN) che archivia le risorse statiche in server distribuiti in tutto il mondo.

Per implementare la cache dei dati, è essenziale gestire correttamente i tempi di scadenza (expire time) e i controlli di aggiornamento (validation checks) per garantire che i dati siano sempre aggiornati quando necessario. Possiamo utilizzare le intestazioni HTTP come “Cache-Control” e “Expires” per controllare il comportamento della cache:

Cache-Control: max-age=3600, public

Questa intestazione indica che la risorsa può essere memorizzata nella cache per un massimo di 3600 secondi (1 ora) e può essere memorizzata nella cache pubblica (cioè condivisa tra gli utenti).

Come implementare una cache delle API utilizzando JavaScript? In questo esempio, utilizzeremo la cache del browser per memorizzare temporaneamente i risultati di una chiamata API.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>API Cache Example</title>
</head>
<body>

<div id="output"></div>

<script>
    // Funzione per ottenere i dati dall'API, utilizzando una cache
    async function fetchData() {
        const apiUrl = 'https://api.example.com/data';
        const cacheKey = 'apiData';

        // Controlla se i dati sono già presenti nella cache del browser
        const cachedData = sessionStorage.getItem(cacheKey);

        if (cachedData) {
            // Se i dati sono presenti nella cache, utilizzali direttamente
            displayData(JSON.parse(cachedData));
        } else {
            // Se i dati non sono nella cache, effettua la chiamata API
            try {
                const response = await fetch(apiUrl);
                const data = await response.json();

                // Memorizza i dati nella cache del browser per la prossima volta
                sessionStorage.setItem(cacheKey, JSON.stringify(data));

                // Visualizza i dati ottenuti dalla chiamata API
                displayData(data);
            } catch (error) {
                console.error('Errore durante il recupero dei dati:', error);
            }
        }
    }

    // Funzione per visualizzare i dati nella pagina
    function displayData(data) {
        const outputContainer = document.getElementById('output');
        outputContainer.innerHTML = '';

        data.forEach(item => {
            const itemElement = document.createElement('div');
            itemElement.textContent = item.name; // Usa la tua logica per visualizzare i dati
            outputContainer.appendChild(itemElement);
        });
    }

    // Chiamata alla funzione fetchData all'avvio della pagina
    document.addEventListener('DOMContentLoaded', fetchData);
</script>

</body>
</html>

In questo esempio:

  • La funzione fetchData controlla se i dati sono già presenti nella cache del browser utilizzando sessionStorage. Se i dati sono presenti, vengono visualizzati direttamente. In caso contrario, viene effettuata una chiamata API per ottenere i dati.
  • I dati ottenuti dalla chiamata API vengono memorizzati nella cache del browser utilizzando sessionStorage per una sessione di navigazione.
  • La funzione displayData è responsabile per visualizzare i dati nella pagina.

Nella pratica, la gestione della cache è molto più complessa, con considerazioni sul controllo delle scadenze, la gestione degli stati di caricamento e la gestione degli errori.

Compressione dei dati

La compressione dei dati è un processo che riduce le dimensioni dei file trasmettendo solo le informazioni essenziali. Questo è particolarmente utile per migliorare le prestazioni di un sito web riducendo i tempi di caricamento delle risorse. Solitamente, la compressione dei dati viene effettuata lato server prima che le risorse vengano inviate al browser del cliente.

Ecco un esempio semplice di come potresti implementare la compressione dei dati lato server utilizzando Node.js e Express con il middleware compression. E’ richiesto Node.js e npm installati sul pc. In una nuova cartella, eseguiamo:

npm init -y
npm install express compression

Creiamo un file chiamato server.js con il seguente codice:

const express = require('express');
const compression = require('compression');

const app = express();
const PORT = process.env.PORT || 3000;

// Utilizza il middleware di compressione
app.use(compression());

// Rotta di esempio
app.get('/', (req, res) => {
    const data = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';

    // Invia una risposta JSON compressa
    res.json({ data });
});

// Avvia il server
app.listen(PORT, () => {
    console.log(`Server in ascolto sulla porta ${PORT}`);
});

Eseguiamo il server con il comando node server.js.

Il server Node.js utilizza il middleware compression per comprimere tutte le risposte JSON prima di inviarle al client. La compressione dei dati può essere applicata a diversi tipi di risorse, non solo JSON, ma anche a HTML, CSS, JavaScript e altre risorse per ridurre le dimensioni dei file trasmessi attraverso la rete, migliorando così le prestazioni complessive del sito.

Lazy Loading delle immagini

Implementare il lazy loading delle immagini può essere fatto utilizzando l’attributo loading in HTML e JavaScript. L’attributo loading è una caratteristica HTML che permette di ritardare il caricamento delle immagini fino a quando non sono vicine al viewport dell’utente. Questo può migliorare significativamente i tempi di caricamento iniziali e risparmiare larghezza di banda.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Lazy Loading Images Example</title>
</head>
<body>

<!-- Immagine con Lazy Loading -->
<img src="placeholder.jpg" data-src="path/to/your/image.jpg" alt="Lazy Loaded Image" width="300" height="200" loading="lazy">

<!-- Altre parti della tua pagina -->

<script>
    // Funzione per gestire il lazy loading delle immagini
    document.addEventListener("DOMContentLoaded", function() {
        const lazyImages = document.querySelectorAll("img[data-src]");

        // Funzione per controllare se un'immagine è nel viewport
        function isElementInViewport(el) {
            const rect = el.getBoundingClientRect();
            return (
                rect.top >= 0 &&
                rect.left >= 0 &&
                rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
                rect.right <= (window.innerWidth || document.documentElement.clientWidth)
            );
        }

        // Funzione per caricare un'immagine quando è nel viewport
        function lazyLoadImage() {
            lazyImages.forEach((img) => {
                if (isElementInViewport(img)) {
                    img.src = img.getAttribute("data-src");
                    img.removeAttribute("data-src");
                }
            });
        }

        // Aggiungi un listener per l'evento di scroll per chiamare lazyLoadImage
        window.addEventListener("scroll", lazyLoadImage);

        // Carica le immagini visibili all'avvio della pagina
        lazyLoadImage();
    });
</script>

</body>
</html>

Caricamento asincrono

Il caricamento asincrono è una tecnica che consente di caricare risorse, come script o immagini, in modo asincrono rispetto al resto del contenuto della pagina. Solitamente in JavaScript si utilizza XMLHttpRequest o l’API Fetch. Ecco un esempio di caricamento asincrono di uno script utilizzando l’attributo async in HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Asynchronous Loading Example</title>
</head>
<body>

<!-- Caricamento asincrono di uno script -->
<script async src="path/to/your/script.js"></script>

<!-- Altre parti della tua pagina -->

</body>
</html>

In questo esempio:

  • Lo script è caricato utilizzando l’attributo async nell’elemento <script>. Questo indica al browser di continuare a caricare il resto della pagina senza attendere il completamento del caricamento dello script.
  • Gli script asincroni non bloccano il rendering della pagina e possono essere eseguiti in parallelo con il caricamento delle altre risorse.

Possiamo anche utilizzare l’attributo defer in alternativa all’attributo async. L’attributo defer indica al browser di eseguire lo script solo dopo che la pagina è stata completamente analizzata.

<!-- Caricamento differito di uno script -->
<script defer src="path/to/your/script.js"></script>

Esempio di caricamento asincrono di un’immagine utilizzando JavaScript:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Asynchronous Image Loading Example</title>
</head>
<body>

<!-- Immagine con caricamento asincrono utilizzando JavaScript -->
<img id="asyncImage" alt="Async Loaded Image" width="300" height="200">

<!-- Altre parti della tua pagina -->

<script>
    // Carica un'immagine in modo asincrono utilizzando JavaScript
    const asyncImage = document.getElementById('asyncImage');
    asyncImage.src = 'path/to/your/image.jpg';
</script>

</body>
</html>

In questo esempio:

  • L’elemento <img> ha un id che viene utilizzato per ottenere un riferimento all’elemento tramite JavaScript.
  • Utilizzando JavaScript, viene impostato l’attributo src dell’immagine con il percorso dell’immagine. Poiché questa operazione avviene dopo il caricamento della pagina, è asincrona rispetto al caricamento iniziale della pagina.

È importante notare che, sebbene il caricamento asincrono possa migliorare le prestazioni, è necessario utilizzarlo con attenzione, soprattutto quando si tratta di script che potrebbero dipendere dall’ordine di esecuzione. Inoltre, l’attributo async può causare problemi se lo script dipende dall’ordine di caricamento. In tal caso, potrebbe essere più appropriato utilizzare defer o metodi più avanzati come la gestione delle dipendenze degli script.

Pre-fetching

Il pre-fetching è una tecnica che consiste di recuperare in modo proattivo risorse, come file CSS, immagini o script, che potrebbero essere necessarie in futuro durante la navigazione dell’utente. Questo processo avviene in background, anticipando le richieste dell’utente e riducendo i tempi di caricamento quando tali risorse sono effettivamente richieste.

Possiamo utilizzare l’elemento HTML <link> con l’attributo rel=”prefetch” per indicare al browser di pre-fetchare determinate risorse. Ecco come si potrebbe fare:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pre-fetching Example</title>

    <!-- Pre-fetching di uno stile CSS -->
    <link rel="prefetch" href="path/to/your/style.css" as="style">

    <!-- Pre-fetching di un'immagine -->
    <link rel="prefetch" href="path/to/your/image.jpg" as="image">
</head>
<body>

<!-- Contenuto della pagina -->

</body>
</html>

In questo esempio:

  • Utilizziamo l’elemento <link> nel tag <head> per indicare al browser di pre-fetchare uno stile CSS e un’immagine.
  • L’attributo rel=”prefetch” specifica il tipo di relazione tra la pagina corrente e la risorsa da pre-fetchare.
  • L’attributo as specifica il tipo di risorsa che si sta pre-fetchando, ad esempio, “style” per uno stile CSS o “image” per un’immagine.

Il pre-fetching potrebbe comportare un utilizzo più intenso della larghezza di banda, poiché il browser scarica risorse aggiuntive in background. Pertanto, è necessario prestare attenzione a quali risorse pre-fetchare per ottimizzare la user experience.

Possiamo inoltre sfruttare il pre-fetching in modo avanzato utilizzando la programmazione del prefetching. Ad esempio, con JavaScript possiamo pre-fetchare risorse in base alle azioni dell’utente:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pre-fetching Example with JavaScript</title>
</head>
<body>

<!-- Contenuto della pagina -->

<script>
    // Funzione per pre-fetchare una risorsa in base a un'azione dell'utente
    function prefetchResource(resourceUrl) {
        const link = document.createElement('link');
        link.rel = 'prefetch';
        link.href = resourceUrl;
        document.head.appendChild(link);
    }

    // Esempio di utilizzo: pre-fetchare un'immagine quando l'utente fa clic su un elemento
    const clickableElement = document.getElementById('clickableElement');
    clickableElement.addEventListener('click', () => {
        prefetchResource('path/to/your/image.jpg');
        // Altre azioni quando l'utente fa clic sull'elemento
    });
</script>

</body>
</html>

In questo esempio, la funzione prefetchResource viene chiamata quando l’utente fa clic su un elemento specifico, e la risorsa specificata viene pre-fetchata. Questo approccio consente di pre-fetchare risorse in modo dinamico in base al comportamento dell’utente.

Minificazione e Ottimizzazione del Codice

La minificazione e l’ottimizzazione del codice sono pratiche assai comuni utilizzate per migliorare le prestazioni di un sito web riducendo le dimensioni dei file CSS, JavaScript e HTML. Queste tecniche contribuiscono a ridurre i tempi di caricamento delle pagine, migliorando l’esperienza dell’utente.

Minificazione: è il processo di eliminazione di spazi bianchi, commenti e altri caratteri non essenziali dal codice sorgente. Ciò riduce le dimensioni del file senza alterarne il comportamento. Gli strumenti di minificazione rimuovono spazi, a capo, indentazioni e commenti, producendo un codice più compatto.

// Codice originale
function addNumbers(a, b) {
    // Somma due numeri
    return a + b;
}

// Codice minificato
function addNumbers(a,b){return a+b;}

Esistono molte librerie e strumenti online che offrono servizi di minificazione per il codice JavaScript, CSS e HTML.

Ottimizzazione del Codice: sono pratiche che migliorano l’efficienza e le prestazioni del codice senza ridurne necessariamente le dimensioni. Questo può includere l’utilizzo di algoritmi più efficienti, la semplificazione di operazioni complesse e la riduzione del numero di richieste al server.

// Codice non ottimizzato
for (let i = 0; i < array.length; i++) {
    console.log(array[i]);
}

// Codice ottimizzato (utilizzo di forEach)
array.forEach(item => {
    console.log(item);
});

L’ottimizzazione del codice richiede una comprensione approfondita del linguaggio di programmazione e delle caratteristiche specifiche del progetto.

Compressione dei File: consiste nel ridurre le dimensioni dei file attraverso algoritmi di compressione. Ciò è particolarmente utile per file CSS, JavaScript e immagini. La compressione può essere implementata lato server, come Gzip o Brotli.

Riduzione delle Richieste al Server: è una pratica importante per migliorare le prestazioni del sito. Ciò può essere fatto combinando e riducendo il numero di file esterni richiesti (script, fogli di stile), utilizzando immagini sprite e riducendo al minimo le richieste HTTP.

Utilizzo di CDN (Content Delivery Network): tale tecnica può migliorare le prestazioni del sito distribuendo le risorse statiche su server geograficamente distribuiti. Ciò consente agli utenti di accedere alle risorse da server più vicini, riducendo i tempi di risposta.

L’implementazione di queste pratiche richiede attenzione e test accurati per assicurarsi che non vi siano effetti collaterali sul comportamento del sito. E’ sicuramente consigliato utilizzare strumenti di analisi delle prestazioni e misurazioni per valutare l’impatto delle ottimizzazioni effettuate.