Caching is essentieel voor het verbeteren van de prestaties en schaalbaarheid van webapplicaties – en caching in Ruby on Rails is geen uitzondering. Door de resultaten van belastende berekeningen of databasequeries op te slaan en te hergebruiken, vermindert caching de tijd en resources die nodig zijn om gebruikersverzoeken uit te voeren aanzienlijk.

In dit artikel laten we zien hoe je verschillende soorten caching in Rails kunt implementeren, zoals fragment caching en Russian doll caching. We laten ook zien hoe je cache dependencie sbeheert en hoe je cache stores kiest en geven we je best practices voor effectief gebruik van caching in een Rails applicatie.

Dit artikel gaat ervan uit dat je bekend bent met Ruby on Rails, Rails versie 6 of hoger gebruikt en vertrouwd bent met het gebruik van Rails views. De codevoorbeelden laten zien hoe je caching kunt gebruiken in nieuwe of bestaande view templates.

Soorten Ruby on Rails caching

Er zijn verschillende soorten caching beschikbaar in Ruby on Rails applicaties, afhankelijk van het niveau en de granulariteit van de inhoud die je wilt cachen. De primaire typen die worden gebruikt in moderne Rails apps zijn:

  • Fragment caching: Caching van delen van een webpagina die niet vaak veranderen, zoals headers, footers, sidebars of statische content. Fragment caching vermindert het aantal onderdelen of componenten die bij elk verzoek worden gerenderd.
  • Russian doll caching: Caching van nested fragmenten van een webpagina die van elkaar afhankelijk zijn, zoals collections en assocations. Russian doll caching voorkomt onnodige database queries en maakt het gemakkelijk om ongewijzigde gecachede fragmenten opnieuw te gebruiken.

Twee extra soorten caching maakten eerder deel uit van Ruby on Rails, maar zijn nu beschikbaar als aparte gems:

  • Page caching: Slaat hele webpagina’s op als statische bestanden op de server, waarbij de hele cyclus van het renderen van pagina’s wordt omzeild
  • Action caching: Slaat de uitvoer van volledige controlleracties op. Het is vergelijkbaar met pagina caching maar staat je toe om filters toe te passen zoals authenticatie.

Page en action caching worden niet vaak gebruikt en niet langer aanbevolen voor de meeste gebruikssituaties in moderne Rails apps.

Fragment caching in Ruby on Rails

Met fragment caching kun je delen van een pagina cachen die niet vaak veranderen. Een pagina met een lijst van producten met hun bijbehorende prijzen en beoordelingen kan details cachen die waarschijnlijk niet zullen veranderen.

Ondertussen kan Rails dynamische delen van de pagina – zoals opmerkingen of beoordelingen – bij elke paginalading opnieuw weergeven. Fragment caching is minder nuttig als de onderliggende gegevens van een view vaak veranderen vanwege de overhead van het regelmatig bijwerken van de cache.

Als de eenvoudigste vorm van caching in Rails, zou fragment caching je eerste keuze moeten zijn bij het toevoegen van caching aan je app om de prestaties te verbeteren.

Om fragment caching te gebruiken in Rails, gebruik je de cache helper methode in je views. Schrijf bijvoorbeeld de volgende code om een deel van een product in je view te cachen:

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

De cache helper genereert een cache key gebaseerd op de class name van elk element, id, en updated_at timestamp (bijvoorbeeld products/1-20230501000000). De volgende keer dat een gebruiker hetzelfde product opvraagt, haalt de cache helper het fragment uit de cache op en geeft het weer zonder het product uit de database te lezen.

Je kunt de cache sleutel ook aanpassen door opties door te geven aan de cache helper. Om bijvoorbeeld een versienummer of een tijdstempel op te nemen in je cache key, schrijf je iets als dit:

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

Je kunt ook een verlooptijd instellen:

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

Het eerste voorbeeld voegt v1 toe aan de cache key (bijvoorbeeld products/1-v1). Dit is handig om de cache ongeldig te maken als je het gedeeltelijke template of de layout wijzigt. Het tweede voorbeeld stelt een verlooptijd in voor de cache invoer (1 uur), wat helpt om verouderde gegevens te laten verlopen.

Russian doll caching in Ruby on Rails

Russian doll caching is een krachtige cachingstrategie in Ruby on Rails die de prestaties van je applicatie optimaliseert door caches in elkaar te nesten. Het gebruikt de Rails fragment caching en cachet dependencies om overbodig werk te minimaliseren en laadtijden te verbeteren.

In een typische Rails applicatie render je vaak een verzameling items, elk met meerdere child-componenten. Wanneer je een enkel item bijwerkt, moet je voorkomen dat je de hele verzameling of alle niet-beïnvloede items opnieuw rendert. Gebruik Russian Doll caching als je te maken hebt met hiërarchische of nested gegevensstructuren, vooral als de nested componenten hun eigen bijbehorende gegevens hebben die onafhankelijk van elkaar kunnen veranderen.

Het nadeel van Russian Doll caching is dat het complexiteit toevoegt. Je moet de relaties begrijpen tussen de nested niveaus van items die je cachet om ervoor te zorgen dat je de juiste items cachet. In sommige gevallen moet je associaties toevoegen aan je Active Record modellen, zodat Rails de relaties kan afleiden tussen gegevensitems in de cache.

Net als bij gewone fragment caching, gebruikt Russian doll caching de cache helper methode. Om bijvoorbeeld een categorie met zijn subcategorieën en producten in je view te cachen, schrijf je iets als dit:

<% @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 %>

De cache helper slaat elk nested niveau apart op in de cache store. De volgende keer dat dezelfde categorie wordt opgevraagd, wordt het fragment uit de cache gehaald en weergegeven zonder opnieuw te renderen.

Als echter de details van een subcategorie of product veranderen – zoals de naam of beschrijving – wordt het fragment uit de cache ongeldig en wordt het opnieuw weergegeven met bijgewerkte gegevens. Russian doll caching zorgt ervoor dat je niet een hele categorie ongeldig hoeft te maken als een enkele subcategorie of product verandert.

Cache dependency beheer in Ruby on Rails

Cache dependencies zijn relaties tussen gegevens in de cache en de onderliggende bronnen, en het beheren ervan kan lastig zijn. Als de brongegevens veranderen, moeten alle bijbehorende cachegegevens verlopen.

Rails kan tijdstempels gebruiken om de meeste cache dependencies automatisch te beheren. Elk Active Record model heeft created_at en updated_at attributen die aangeven wanneer de cache het record heeft aangemaakt of voor het laatst heeft bijgewerkt. Om ervoor te zorgen dat Rails caching automatisch kan beheren, definieer je de relaties van je Active Record modellen als volgt:

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

In dit voorbeeld:

  • Als je een product record bijwerkt (bijvoorbeeld door de prijs te veranderen), verandert zijn updated_at timestamp automatisch.
  • Als je deze tijdstempel gebruikt als onderdeel van je cache key (zoals products/1-20230504000000), wordt ook automatisch je cache fragment ongeldig gemaakt.
  • Om het cachefragment van je categorie ongeldig te maken als je een product record bijwerkt – misschien omdat het geaggregeerde gegevens zoals de gemiddelde prijs toont – gebruik je de methode touch in je controller (@product.category.touch) of voeg je een optie touch toe in je modelassociatie (belongs_to :category touch: true).

Een ander mechanisme voor het beheren van cache dependencies is het gebruik van low-level cachingmethoden – zoals fetch en write – rechtstreeks in je modellen of controllers. Met deze methoden kun je willekeurige gegevens of inhoud opslaan in je cache store met aangepaste keys en opties. Bijvoorbeeld:

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

Dit voorbeeld laat zien hoe je berekende gegevens – zoals de gemiddelde prijs van alle producten – een uur lang kunt cachen met behulp van de methode fetch met een custom key(products/average_price) en een verloopoptie (expires_in: 1.hour).

De methode fetch probeert eerst de gegevens uit de cache store te lezen. Als het de gegevens niet kan vinden of als de gegevens verlopen zijn, voert het het blok uit en slaat het resultaat op in de cache store.

Om handmatig een cache entry ongeldig te maken voor de verlooptijd, gebruik je de methode write met de optie force:

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

Cache stores en backends in Ruby on Rails

Rails laat je verschillende cache stores of backends kiezen om je cachegegevens en inhoud op te slaan. De Rails cache store is een abstractielaag die een gemeenschappelijke interface biedt voor interactie met verschillende opslagsystemen. Een cache backend implementeert de cache store interface voor een specifiek opslagsysteem.

Rails ondersteunt verschillende soorten cache stores of backends, die hieronder worden beschreven.

Geheugenopslag

Memory store gebruikt een hash in het geheugen als cacheopslag. Het is snel en eenvoudig, maar heeft een beperkte capaciteit en persistentie. Deze cache store is geschikt voor ontwikkel- en testomgevingen of kleine, eenvoudige toepassingen.

Schijf opslag

Disk store gebruikt bestanden op de schijf als cache-opslag. Het is de traagste cache-optie in Rails, maar heeft een grote capaciteit en persistentie. Disk store is geschikt voor applicaties die grote hoeveelheden gegevens moeten cachen en geen maximale prestaties nodig hebben.

Redis

De Redis store gebruikt een Redis instantie voor cacheopslag. Redis is een in-memory data store die verschillende datatypes ondersteunt. Hoewel het snel en flexibel is, vereist het een aparte server en configuratie. Het is geschikt voor applicaties die complexe of dynamische gegevens moeten cachen die vaak veranderen. Redis is een ideale keuze als je Rails apps in de cloud draait, omdat sommige hostingproviders, waaronder Kinsta, Redis aanbieden als een persistent object cache.

Memcached

De Memcached store gebruikt een Memcached instance voor cacheopslag. Memcached is een in-memory key-value store die eenvoudige gegevenstypen en functies ondersteunt. Het is snel en schaalbaar, maar vereist net als Redis een aparte server en configuratie. Deze opslag is geschikt voor applicaties die eenvoudige of statische gegevens moeten cachen die regelmatig worden bijgewerkt.

Je kunt je cache store configureren in je Rails environments bestanden (bijvoorbeeld config/environments/development.rb) met de optie config.cache_store. Hier lees je hoe je elk van de ingebouwde cachingmethodes van Rails gebruikt:

# 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"

Je mag maar één config.cache_store aanroep per omgevingsbestand hebben. Als je er meer dan één hebt, gebruikt de cache store alleen de laatste.

Elke cache store heeft unieke voor- en nadelen, afhankelijk van de behoeften en voorkeuren van je applicatie. Kies degene die het beste past bij jouw toepassing en ervaringsniveau.

Best Practices voor Ruby on Rails Caching

Het gebruik van caching in je Rails applicatie kan de prestaties en schaalbaarheid aanzienlijk verbeteren, vooral als je de volgende best practices implementeert:

  • Cache selectief: Cache alleen vaak opgevraagde, lastig te genereren of zelden bijgewerkte gegevens. Vermijd overmatig cachen om overmatig geheugengebruik, het risico op verouderde gegevens en prestatievermindering te voorkomen.
  • Laat cache-items verlopen: Voorkom verouderde gegevens door ongeldige of irrelevante vermeldingen te laten verlopen. Gebruik tijdstempels, vervalopties of handmatig ongeldig maken.
  • Optimaliseer de cacheprestaties: Kies de cache store die past bij de behoeften van je applicatie en stel de parameters fijn af – zoals grootte, compressie of serialisatie – voor optimale prestaties.
  • Monitor en test de impact van de cache: Evalueer het gedrag van de cache – zoals hit rate, miss rate en latency – en beoordeel hun respectievelijke impact op de prestaties (responstijd, doorvoer, resourcegebruik). Gebruik tools zoals New Relic, Rails logs, ActiveSupport meldingen of Rack mini profiler.

Samenvatting

Ruby on Rails caching verbetert de prestaties en schaalbaarheid van applicaties door het efficiënt opslaan en hergebruiken van veelgebruikte gegevens of inhoud. Met een beter begrip van cachingtechnieken, ben je beter uitgerust om snellere Rails apps te leveren aan je gebruikers.

Bij het deployen van je geoptimaliseerde Rails applicatie, kun je terecht bij Kinsta’s Applicatie Hosting platform. Begin gratis met een Hobby Tier account en verken het platform met dit Ruby on Rails Quickstart voorbeelden.

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.