Il caching è essenziale per migliorare le prestazioni e la scalabilità delle applicazioni web e il caching in Ruby on Rails non fa eccezione. Memorizzando e riutilizzando i risultati di calcoli o di query onerosi per il database, il caching riduce significativamente il tempo e le risorse necessarie per servire le richieste degli utenti.

Qui esaminiamo come implementare diversi tipi di caching in Rails, come il fragment caching, ovvero caching dei frammenti, e il Russian doll caching, o caching matrioska. Vi mostreremo anche come gestire le dipendenze della cache e come scegliere gli archivi della cache, e vi illustreremo le best practice per utilizzare la cache in modo efficace in un’applicazione Rails.

Questo articolo presuppone che abbiate familiarità con Ruby on Rails, che utilizziate Rails versione 6 o superiore e che vi sentiate a vostro agio nell’utilizzo delle viste di Rails. Gli esempi di codice dimostrano come utilizzare il caching all’interno di modelli di vista nuovi o già esistenti.

Tipi di caching in Ruby on Rails

Nelle applicazioni Ruby on Rails sono disponibili diversi tipi di caching, a seconda del livello e della granularità dei contenuti da memorizzare nella cache. I principali tipi utilizzati nelle moderne applicazioni Rails sono:

  • Caching dei frammenti: memorizza nella cache le parti di una pagina web che non cambiano frequentemente, come le intestazioni, i piè di pagina, le barre laterali o i contenuti statici. Il caching dei frammenti riduce il numero di parti o componenti renderizzati ad ogni richiesta.
  • Caching matrioska: memorizza nella cache i frammenti annidati di una pagina web che dipendono l’uno dall’altro, come le raccolte e le associazioni. Il caching matrioska evita le query inutili al database e facilita il riutilizzo dei frammenti invariati nella cache.

Facevano parte di Ruby on Rails anche altri due tipi di caching, ma adesso sono disponibili solo come gemme separate:

  • Caching delle pagine: memorizza nella cache intere pagine web come file statici sul server, bypassando l’intero ciclo di vita del rendering della pagina
  • Caching delle azioni: memorizza nella cache l’output di intere azioni del controller. È simile al caching delle pagine ma permette di applicare filtri come l’autenticazione.

Il caching delle pagine e delle azioni sono poco utilizzati e non sono più consigliati per la maggior parte dei casi d’uso nelle moderne applicazioni Rails.

Cache dei frammenti in Ruby on Rails

La cache dei frammenti permette di memorizzare nella cache parti di una pagina che cambiano di rado. Ad esempio, una pagina che mostra un elenco di prodotti con i relativi prezzi e valutazioni può memorizzare nella cache i dettagli che difficilmente cambieranno.

Nel frattempo, potrebbe permettere a Rails di renderizzare nuovamente le parti dinamiche della pagina, come i commenti o le recensioni, a ogni caricamento della pagina. La cache dei frammenti è meno utile quando i dati sottostanti a una vista cambiano frequentemente, a causa dell’overhead che comporta l’aggiornamento frequente della cache.

Essendo il tipo più semplice di cache incluso in Rails, il fragment caching dovrebbe essere la prima scelta quando si vuole aggiungere la cache alla propria applicazione per migliorarne le prestazioni.

Per utilizzare la cache dei frammenti in Rails, usate il metodo helper cache nelle viste. Ad esempio, scrivete il seguente codice per mettere in cache un prodotto parziale nella vostra vista:

<% @products.each do |product| %>
  <% cache product do %>
    <%= render partial: "product", locals: { product: product } %>
  <% end %>
<% end %>

L’helper cache genera una chiave di cache basata sul nome della classe di ogni elemento, id, e sul timestamp updated_at (ad esempio, products/1-20230501000000). La volta successiva che un utente richiede lo stesso prodotto, l’helper cache recupera il frammento in cache dall’archivio della cache e lo visualizza senza leggere il prodotto dal database.

Potete anche personalizzare la chiave della cache passando delle opzioni all’helper cache. Ad esempio, per includere un numero di versione o un timestamp nella chiave della cache, scrivete qualcosa di simile:

<% @products.each do |product| %>
  <% cache [product, "v1"] do %>
    <%= render partial: "product", locals: { product: product } %>
  <% end %>
<% end %>

In alternativa, potete impostare un tempo di scadenza:

<% @products.each do |product| %>
  <% cache product, expires_in: 1.hour do %>
    <%= render partial: "product", locals: { product: product } %>
  <% end %>
<% end %>

Il primo esempio aggiunge v1 alla chiave della cache (ad esempio, products/1-v1). Questo è utile per invalidare la cache quando cambiate il modello parziale o il layout. Il secondo esempio imposta un tempo di scadenza per la voce della cache (1 ora), che aiuta a far scadere i dati obsoleti.

Caching matrioska in Ruby on Rails

Il caching matrioska è una potente strategia di caching in Ruby on Rails che ottimizza le prestazioni dell’applicazione annidando le cache una dentro l’altra. Utilizza la cache dei frammenti di Rails e le dipendenze della cache per ridurre al minimo il lavoro ridondante e migliorare i tempi di caricamento.

In una tipica applicazione Rails, spesso si esegue il rendering di una raccolta di elementi, ciascuno con più componenti figli. Quando aggiornate un singolo elemento, evita di renderizzare l’intera collezione o gli elementi non interessati. Usate il Russian Doll caching quando avete a che fare con strutture di dati gerarchiche o annidate, soprattutto quando i componenti annidati hanno i loro dati associati che potrebbero cambiare in modo indipendente.

L’aspetto negativo del caching matrioska è che aggiunge complessità. Sarà necessario capire le relazioni tra i livelli annidati degli elementi che state mettendo in cache per assicurarvi di mettere in cache gli elementi giusti. In alcuni casi, dovrete aggiungere delle associazioni ai vostri modelli Active Record in modo che Rails possa dedurre le relazioni tra i dati memorizzati nella cache.

Come per la normale cache dei frammenti, la cache matrioska utilizza il metodo helper cache. Ad esempio, per mettere in cache una categoria con le sue sottocategorie e i suoi prodotti nella vostra vista, scrivete qualcosa di simile:

<% @categories.each do |category| %>
  <% cache category do %>
    <h2><%= category.name %></h2>
    <% category.subcategories.each do |subcategory| %>
    <% cache subcategory do %>
    <h3><%= subcategory.name %></h3>
    <% subcategory.products.each do |product| %>
    <% cache product do %>
        <%= render partial: "product", locals: { product: product } %>
        <% end %>
    <% end %>
    <% end %>
    <% end %>
  <% end %>
<% end %>

L’helper cache memorizzerà ogni livello annidato separatamente nell’archivio della cache. La volta successiva che verrà richiesta la stessa categoria, verrà recuperato il suo frammento in cache dall’archivio della cache e verrà visualizzato senza doverlo renderizzare nuovamente.

Tuttavia, se i dettagli di una sottocategoria o di un prodotto cambiano (ad esempio il nome o la descrizione), il frammento memorizzato nella cache viene invalidato e quindi riproposto con i dati aggiornati. La cache a matrioska vi permette di non dover invalidare un’intera categoria se una singola sottocategoria o un prodotto cambia.

Gestione delle dipendenze della cache in Ruby on Rails

Le dipendenze della cache sono relazioni tra i dati memorizzati nella cache e le loro fonti sottostanti e la loro gestione può essere complicata. Se i dati sorgente cambiano, i dati in cache associati devono scadere.

Rails può utilizzare i timestamp per gestire automaticamente la maggior parte delle dipendenze della cache. Ogni modello Active Record ha gli attributi created_at e updated_at che indicano quando la cache ha creato o aggiornato l’ultimo record. Per fare in modo che Rails possa gestire automaticamente la cache, definite le relazioni dei vostri modelli Active Record come segue:

class Product < ApplicationRecord
  belongs_to :category
end
class Category < ApplicationRecord
  has_many :products
end

In questo esempio:

  • Se aggiornate un record di prodotto (ad esempio, cambiando il suo prezzo), il vostro timestamp updated_at cambia automaticamente.
  • Se utilizzate questo timestamp come parte della vostra chiave di cache (come products/1-20230504000000), invalida automaticamente anche il frammento in cache.
  • Per invalidare il frammento memorizzato nella cache della vostra categoria quando aggiornate un record di prodotto, magari perché mostra alcuni dati aggregati come il prezzo medio, usate il metodo touch nel vostro controllore (@product.category.touch) o aggiungete un’opzione touch nel vostro modello di associazione (belongs_to :category touch: true).

Un altro meccanismo per gestire le dipendenze dalla cache è l’utilizzo di metodi di caching di basso livello, come fetch e write, direttamente nei modelli o nei controllori. Questi metodi permettono di memorizzare dati o contenuti arbitrari nella cache con chiavi e opzioni personalizzate. Ad esempio:

class Product < ApplicationRecord
  def self.average_price
    Rails.cache.fetch("products/average_price", expires_in: 1.hour) do
    average(:price)
    end
  end
end

Questo esempio mostra come memorizzare nella cache dati calcolati – come il prezzo medio di tutti i prodotti – per un’ora utilizzando il metodo fetch con una chiave personalizzata (products/average_price) e un’opzione di scadenza (expires_in: 1.hour).

Il metodo fetch cercherà prima di tutto di leggere i dati dall’archivio della cache. Se non riesce a trovare i dati o se questi sono scaduti, esegue il blocco e memorizza il risultato nella cache.

Per invalidare manualmente una voce della cache prima della sua scadenza, usate il metodo write con l’opzione force:

Rails.cache.write("products/average_price", Product.average(:price), force: true))

Cache store e backend in Ruby on Rails

Rails permette di scegliere diversi cache store o backend per memorizzare dati e contenuti nella cache. Il cache store di Rails è un livello di astrazione che fornisce un’interfaccia comune per interagire con diversi sistemi di archiviazione. Un backend della cache implementa l’interfaccia del cache store per uno specifico sistema di archiviazione.

Rails supporta diversi tipi di cache store o backend, illustrati di seguito.

Memory store

Il memory store utilizza un hash in-memory come memoria cache. È veloce e semplice, ma ha una capacità e una persistenza limitate. Questo archivio di cache è adatto ad ambienti di sviluppo e di test o ad applicazioni piccole e semplici.

Disk store

Il Disk Store utilizza i file sul disco come memoria cache. È l’opzione di cache più lenta di Rails ma ha una grande capacità e persistenza. Il Disk Store è adatto alle applicazioni che devono memorizzare nella cache grandi quantità di dati e non hanno bisogno di prestazioni massime.

Redis

Lo store Redis utilizza un’istanza Redis per la memorizzazione della cache. Redis è un archivio di dati in-memory che supporta diversi tipi di dati. Sebbene sia veloce e flessibile, richiede un server e una configurazione separati. È adatto alle applicazioni che devono memorizzare nella cache dati complessi o dinamici che cambiano frequentemente. Redis è la scelta ideale quando si eseguono applicazioni Rails nel cloud perché alcuni provider di hosting, tra cui Kinsta, offrono Redis come cache persistente di oggetti.

Memcached

Lo store Memcached utilizza un’istanza Memcached per la memorizzazione della cache. Memcached è un archivio di valori-chiave in memoria che supporta tipi di dati e funzioni semplici. È veloce e scalabile ma, come Redis, richiede un server e una configurazione separati. Questo archivio è adatto alle applicazioni che hanno bisogno di memorizzare nella cache dati semplici o statici che subiscono aggiornamenti frequenti.

Potete configurare il vostro archivio di cache nei file degli ambienti di Rails (ad esempio, config/environments/development.rb) utilizzando l’opzione config.cache_store. Ecco come utilizzare ciascuno dei metodi di cache integrati in Rails:

# Use memory store
config.cache_store = :memory_store
# Use disk store
config.cache_store = :file_store, "tmp/cache"
# Use Redis
config.cache_store = :redis_cache_store, { url: "redis://localhost:6379/0" }
# Use Memcached
config.cache_store = :mem_cache_store, "localhost"

Dovreste eseguire una sola chiamata a config.cache_store per ogni file di ambiente. Se ne avete più di una, il cache store utilizza solo l’ultima.

Ogni archivio di cache presenta vantaggi e svantaggi unici a seconda delle esigenze e delle preferenze dell’applicazione. Scegliete quello più adatto al vostro caso d’uso e al vostro livello di esperienza.

Le best practice per il caching di Ruby on Rails

L’uso della cache nella vostra applicazione Rails può aumentare in modo significativo le sue prestazioni e la sua scalabilità, soprattutto se si implementano le seguenti best practice:

  • Mettere in cache in modo selettivo: Mettete in cache solo i dati con accesso frequente, costosi da generare o aggiornati di rado. Evitate l’uso eccessivo della cache per evitare l’utilizzo eccessivo della memoria, il rischio di dati stantii e il degrado delle prestazioni.
  • Far scadere le voci della cache: Evitate i dati obsoleti facendo scadere le voci non valide o irrilevanti. Usate i timestamp, le opzioni di scadenza o l’invalidazione manuale.
  • Ottimizzare le prestazioni della cache: Scegliete l’archivio della cache che si adatta alle esigenze della vostra applicazione e mettete a punto i suoi parametri, come le dimensioni, la compressione o la serializzazione, per ottenere prestazioni ottimali.
  • Monitorare e testare l’impatto della cache: Valutate il comportamento della cache – come la percentuale di hit, miss e latenza – e valutate il rispettivo impatto sulle prestazioni (tempi di risposta, throughput, utilizzo delle risorse). Utilizzate strumenti come New Relic, i log di Rails, le notifiche di ActiveSupport o il mini profiler di Rack.

Riepilogo

Il caching di Ruby on Rails migliora le prestazioni e la scalabilità delle applicazioni memorizzando e riutilizzando in modo efficiente i dati o i contenuti a cui si accede di frequente. Con una conoscenza più approfondita delle tecniche di caching, sarete meglio equipaggiati per offrire applicazioni Rails più veloci ai vostri utenti.

Per distribuire la vostra applicazione Rails ottimizzata, potete rivolgerti alla piattaforma di Hosting di Applicazioni di Kinsta. Iniziate gratuitamente con un account Piano Hobby ed esplorate la piattaforma con questo esempio di avvio rapido di Ruby on Rails.

Steve Bonisteel Kinsta

Steve Bonisteel is a Technical Editor at Kinsta who began his writing career as a print journalist, chasing ambulances and fire trucks. He has been covering Internet-related technology since the late 1990s.