Rails — Melhorando a performance das suas views com cache
Quando pensamos em melhorar a performance de uma aplicação, o uso de cache é uma das alternativas mais usadas na prática. Podemos pensar no uso de cache como a abordagem dos 3 Rs da sustentabilidade: Reduzimos a carga de processamento e requests da nossa aplicação ao reutilizarmos algo já calculado, já processado e salvo anteriormente, ao mesmo tempo que precisamos reciclar essa informação de tempos em tempos ou sempre que houver uma novidade relevante que altere ela, para manter ela relevante para a aplicação e não mostrarmos informações erradas.
Quando falamos de aplicações rails, cache não é algo novo. Pelo contrário, é algo que já existe no framework há muitos anos. Mesmo assim, é fácil cometermos erros se não entendermos bem como usar e, principalmente, como reciclar o cache.
Uma das formas que eu acho mais interessante de usar cache no rails é usando ele diretamente nas views, cacheando o html gerado pelo erb, utilizando o método conhecido como russian doll caching.
O que é o russian doll caching?
Nas guides do rails sobre cache tem um tópico exclusivo para esse método de cache, explicando em mais detalhes esse tópico. De forma resumida, essa abordagem é a prática de usar o cache de forma encadeada nas views, ou seja, usarmos o cache dentro de partes já cacheadas. O nome russian doll vem das bonecas russas que tem bonecas menores dentro delas, conforme a imagem acima.
Para todos os exemplos citado abaixo nos próximos tópicos abaixo foi usado o projeto https://github.com/jplethier/russian-doll-cache-examples. O projeto foi criado somente com o intuito de ser usado como exemplo nesse post. Os cenários criados no projeto foram feitos especificamente para facilitarem o entendimento do uso do russian doll caching, não sendo necessariamente reais.
Utilizando o cache nas views
Para usar o cache nas views do rails, podemos chamar direto o método helper cache . Esse método, conforme podemos ver na documentação, recebe um objeto como primeiro parâmetro obrigatório, um hash de opções como segundo parâmetro opcional, e um bloco com o html que vai ser renderizado. É possível também usar esse método nas variações cache_if (documentação)) e cache_unless (documentação). A diferença é que esses dois últimos métodos recebem um parâmetro a mais obrigatório que é a condição a ser avaliada. Por exemplo, no cache_if , o bloco passado só é cacheado se a condição for avaliada como verdadeira.
Como exemplo, vamos pensar numa página que lista autores contendo o nome de cada autor, a quantidade de livros e a data da publicação do último livro. Para facilitar o exemplo, a página não possui nenhuma paginação.
No código acima, ao iniciar a renderização dessa página, para transformar o erb em html, o rails vai verificar primeiro se todo o bloco passado para o cache(linhas 4 a 22) já foi cacheado. Se já estiver no cache, o rails não irá executar o bloco novamente, e usar o valor do cache. Caso contrário, ele executará o bloco e salvará o resultado dele no cache.
Invalidando o cache
Uma das maiores dificuldades de trabalhar com cache é conseguir invalidar ele corretamente. É muito comum não invalidarmos alguma chave corretamente, e vermos informações antigas, informações que não são mais válidas. A forma mais simples de invalidar o cache é setando uma data de expiração.
No exemplo acima, podemos passarm uma opção de expires_in ao utilizarmos o método cache, conforme o exemplo abaixo:
Usando dessa forma, garantimos que a informação nessa página estará sempre no máximo 1 hora atrasada. Mas e se eu quiser garantir que a informação está sempre atualizada? Se for importante que assim que eu fizer uma alteração no nome de um autor, a página já exiba essa informação mais atualizada corretamente?
Para fazer isso, precisamos entender como é montada a chave do cache ao usarmos o método cache na view. Quando passamos um objeto para o método cache, o rails sempre monta a chave do cache usando algo que o ajude a identificar o que é o objeto e também a identificar se há uma versão mais recente desse objeto. No nossa página de lista de autores, ao passarmos uma lista de autores para o cache, o rails faz uma query para pegar o tamanho da lista e o maior valor para a coluna updated_at dos elementos dessa lista, e usa essas duas informações para compor a chave do cache e salva o resultado do bloco nessa chave. Na imagem, além da query feita para pegar o count e o updated_at, é possível perceber o uso desses valores no final da linha que tem o output do Read fragment, onde mostra os valores 4219–20210131190705856638, sendo o primeiro valor o resultado do count, e o último a data e hora da última atualização de um registro de autor na tabela.
Um outro caso onde pode ser necessário invalidarmos o cache é quando temos uma alteração no erb que foi passado como bloco para o cache. O rails também já resolve isso para gente de forma automática, pois ele gera um hash para identificar o código que está dentro do bloco, e ao ter qualquer alteração nesse bloco, o hash do novo bloco passa a ser diferente, gerando uma nova chave para o cache e não utilizando mais o valor cacheado anteriormente. Na imagem abaixo é possível ver o output de escrita no cache com um valor diferente na parte do meio da chave, onde aparece _index_with_cache:HASH, em comparação com a imagem acima, como consequência de uma pequena alteração feita no erb.
Utilizando o russian doll cache nas views
Usando ainda o exemplo da lista de autores, pensando em um cenário extremo de milhares de autores(e de novo ignorando paginação para facilitar o entendimento), sempre que um único autor tiver uma simples alteração de nome, o cache da lista vai ser inteiro invalidado e toda a página contendo os milhares de autores terá que ser reconstruída. É nesse caso que o russian doll caching nos pode ser útil.
Para resolver esse problema, podemos não só cachear a lista, como também cachear cada fragmento de autor na página, conforme abaixo:
Nesse caso, na primeira vez que o rails executar esse bloco, ele vai salvar cada fragmento menor de cada autor no cache primeiro, e depois vai salvar o fragmento maior, que contém todos os fragmentos de autores, também no cache. Ao recarregarmos a página após um ou alguns autores terem sido alterados, o rails não irá ler o cache da lista inteira(que foi invalidada pelo max updated_at da lista de autores), mas irá decidir caso a caso em cada fragmento de autor na página se irá ler do cache ou ter que recarregar as informações.
Na imagem acima é possível como fica o output do servidor rails no terminal, ao ler fragmentos de vários autores do cache. Também é possível observar novamente o padrão para a montagem da chave do cache, com um hash que identifica o pedaço de código do fragmento, o bloco passado para o método cache, assim como o updated_at de cada autor no final da chave. A diferença fica por conta do uso do id de cada registro de autor na chave, ao invés do hash da query e do count usados ao passarmos uma lista para o cache.
Caches com informações de múltiplos models
Como podemos ver, o rails parece cuidar de tudo para nos facilitar a vida em relação a invalidar o cache. Tudo que precisamos é garantir que sempre que eu atualizar uma informação no banco, o updated_at daquele registro também seja atualizado, ou seja, basta evitar os métodos que fazem as chamadas direto no banco(como update_column, por exemplo).
De fato o rails cuida de muita coisa, facilitando muito nosso trabalho ao usar o cache. Mas tem uma coisa que ele não consegue cuidar sozinho: informações de models diferentes no mesmo cache. No exemplo acima, temos duas informações sobre os livros dos autores em cada cache de fragmento de autor que fazemos: a quantidade de livros escritos e a data de publicação do último livro publicado. Da forma que escrevemos o código da listagem acima, ao cadastrar um novo livro para algum autor, esse cache não será atualizado, pois a ação de cadastrar um novo livro não impacta em nenhuma das informações compostas na chave do cache; não muda o bloco de código erb e consequentemente não gera um novo hash para ele; e também não muda o updated_at de nenhum autor.
Bom, uma forma de resolvermos isso, é fazermos com que sempre que um livro for atualizado, criado ou removido, o autor daquele livre seja "atualizado". Ou melhor, que o autor daquele livro pareça ter sido atualizado, pois só precisamos atualizar a própria coluna updated_at daquele autor, e não atualizar nenhum outro atributo. Podemos fazer isso adicionando o parametro touch: true no relacionamento do model book, conforme abaixo:
Ao fazermos isso, sempre que um livro for alterado, o rails automaticamente irá atualizar o valor do atributo updated_at do autor correspondente àquele livro, e consequentemente invalidar qualquer cache que tenha sido feito considerando esse atributo.
Uma outra forma de tratar esses cenários de cache de fragmentos com informações de mais de um registro, é passar para o método cache uma lista contendo cada objeto que é usado naquele cache.
No exemplo acima, a alteração da linha 13, onde o método cache passa a receber tanto o author, como a query que pega todos os livros daquele autor, faz com que o rails inclua na chave do cache também as informações da quantidade de livro do autor e a última atualização ocorrida em algum livro desse autor. Dessa forma, ao atualizar qualquer livro, o cache do fragmento específico dele irá ser invalidado, garantindo que a informação nova seja exibida.
Um cuidado que precisamos ter é que cada objeto a mais que passamos para a chave do cache, significa também mais queries na hora de montar e verificar o cache, assim como chaves mais complexas para cada fragmento. Isso precisa ser levado em consideração na hora de escolher a abordagem correta para invalidar o cache e garantir que a informação exibida seja sempre a mais nova. No output da imagem abaixo, podemos ver a chave que o rails montou para o mesmo fragmento de um autor, ao incluirmos a lista de livros.
Múltiplas páginas com caches do mesmo objeto
Nos exemplos acima, usamos o objeto autor para cachear o fragmento referente as informações específicas desse autor na listagem. Mas e se quisermos também usar o objeto do autor para cachear informações dele na página de perfil do autor, conforme o código abaixo:
Olhando e pensando de forma rápida, parece que vai dar algum conflito nas chaves entre o cache usado nessa página de show e o cache do fragmento da página de lista de autores, certo? Sim, olhando rápido de fato nos dá esse receio, mas podemos novamente recorrer ao output do servidor no terminal para entender se ele usa a mesma chave ou se já é gerada uma chave separada para essa página de show.
Como podemos ver, a chave gerada nesse caso é diferente da chave gerada para o fragmento da página de lista de autores. O rails já escopa o cache de acordo com o arquivo erb onde ele está sendo chamado, permitindo que a gente possa simplesmente passar o mesmo objeto para o método cache em diferentes arquivos sem precisarmos nos preocupar em garantir que serão geradas chaves diferentes para cada um deles.
Outros exemplos
No projeto criado no github para esse post, além da página de index de autores, inclui mais exemplos de uso na página de show de um autor e nas páginas de index e show de livros. Todos eles possuem exemplos de como ficaria a página sem usar o cache, e como ficaria usando o cache. Para rodar o projeto e carregar as páginas que usam o cache, basta setar a variável de ambiente CACHE_ON para true. Importante também lembrar que a partir do rails 5, para ligar ou desligar o cache no ambiente de development, é preciso rodar rails dev:cache.
Outros links sobre cache
- Caching with Rails: An Overview - Ruby on Rails Guides
- Russian doll caching in Rails
- Russian Doll Caching with Rails 5 (Example) | GoRails - GoRails
Top comments (0)