Desduplicação de Recursos com React Suspense: Prevenindo Requisições Duplicadas | MLOG | MLOG
Português
Explore como prevenir requisições duplicadas de busca de dados em aplicações React usando Suspense e técnicas de desduplicação de recursos para melhor desempenho e eficiência.
Desduplicação de Recursos com React Suspense: Prevenindo Requisições Duplicadas
O React Suspense revolucionou a forma como lidamos com a busca de dados assíncrona em aplicações React. Ao permitir que os componentes "suspendam" a renderização até que seus dados estejam disponíveis, ele fornece uma abordagem mais limpa e declarativa em comparação com o gerenciamento tradicional de estados de carregamento. No entanto, um desafio comum surge quando vários componentes tentam buscar o mesmo recurso simultaneamente, levando a requisições duplicadas e potenciais gargalos de desempenho. Este artigo explora o problema de requisições duplicadas no React Suspense e fornece soluções práticas usando técnicas de desduplicação de recursos.
Entendendo o Problema: O Cenário de Requisição Duplicada
Imagine um cenário onde vários componentes em uma página precisam exibir os mesmos dados de perfil de usuário. Sem um gerenciamento adequado, cada componente pode iniciar sua própria requisição para buscar o perfil do usuário, resultando em chamadas de rede redundantes. Isso desperdiça largura de banda, aumenta a carga no servidor e, por fim, degrada a experiência do usuário.
Aqui está um exemplo de código simplificado para ilustrar o problema:
import React, { Suspense } from 'react';
const fetchUser = (userId) => {
console.log(`Buscando usuário com ID: ${userId}`); // Simula uma requisição de rede
return new Promise(resolve => {
setTimeout(() => {
resolve({ id: userId, name: `Usuário ${userId}`, email: `usuario${userId}@example.com` });
}, 1000); // Simula a latência da rede
});
};
const UserResource = (userId) => {
let promise = null;
let status = 'pending'; // pendente, sucesso, erro
let result;
const suspender = fetchUser(userId).then(
(r) => {
status = 'success';
result = r;
},
(e) => {
status = 'error';
result = e;
}
);
return {
read() {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
}
return result;
},
};
};
const UserProfile = ({ userId }) => {
const user = UserResource(userId).read();
return (
Neste exemplo, tanto os componentes UserProfile quanto UserDetails tentam buscar os mesmos dados de usuário usando UserResource. Se você executar este código, verá que Buscando usuário com ID: 1 é registrado duas vezes, indicando duas requisições separadas.
Técnicas de Desduplicação de Recursos
Para prevenir requisições duplicadas, podemos implementar a desduplicação de recursos. Isso envolve garantir que apenas uma requisição seja feita para um recurso específico, e o resultado seja compartilhado entre todos os componentes que precisam dele. Várias técnicas podem ser usadas para alcançar isso.
1. Armazenando a Promise em Cache
A abordagem mais direta é armazenar em cache a promise retornada pela função de busca de dados. Isso garante que, se o mesmo recurso for solicitado novamente enquanto a requisição original ainda estiver em andamento, a promise existente seja retornada em vez de criar uma nova.
Veja como você pode modificar o UserResource para implementar o cache de promises:
import React, { Suspense } from 'react';
const fetchUser = (userId) => {
console.log(`Buscando usuário com ID: ${userId}`); // Simula uma requisição de rede
return new Promise(resolve => {
setTimeout(() => {
resolve({ id: userId, name: `Usuário ${userId}`, email: `usuario${userId}@example.com` });
}, 1000); // Simula a latência da rede
});
};
const cache = {}; // Cache simples
const UserResource = (userId) => {
if (!cache[userId]) {
let promise = null;
let status = 'pending'; // pendente, sucesso, erro
let result;
const suspender = fetchUser(userId).then(
(r) => {
status = 'success';
result = r;
},
(e) => {
status = 'error';
result = e;
}
);
cache[userId] = {
read() {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
}
return result;
},
};
}
return cache[userId];
};
const UserProfile = ({ userId }) => {
const user = UserResource(userId).read();
return (
Agora, o UserResource verifica se um recurso já existe no cache. Se existir, o recurso em cache é retornado. Caso contrário, uma nova requisição é iniciada e a promise resultante é armazenada no cache. Isso garante que apenas uma requisição seja feita para cada userId único.
2. Usando uma Biblioteca de Cache Dedicada (ex: `lru-cache`)
Para cenários de cache mais complexos, considere usar uma biblioteca de cache dedicada como lru-cache ou similar. Essas bibliotecas fornecem recursos como remoção de itens do cache com base no Menos Recentemente Usado (LRU) ou outras políticas, o que pode ser crucial para gerenciar o uso de memória, especialmente ao lidar com um grande número de recursos.
Primeiro, instale a biblioteca:
npm install lru-cache
Em seguida, integre-a ao seu UserResource:
import React, { Suspense } from 'react';
import LRUCache from 'lru-cache';
const fetchUser = (userId) => {
console.log(`Buscando usuário com ID: ${userId}`); // Simula uma requisição de rede
return new Promise(resolve => {
setTimeout(() => {
resolve({ id: userId, name: `Usuário ${userId}`, email: `usuario${userId}@example.com` });
}, 1000); // Simula a latência da rede
});
};
const cache = new LRUCache({
max: 100, // Número máximo de itens no cache
ttl: 60000, // Tempo de vida em milissegundos (1 minuto)
});
const UserResource = (userId) => {
if (!cache.has(userId)) {
let promise = null;
let status = 'pending'; // pendente, sucesso, erro
let result;
const suspender = fetchUser(userId).then(
(r) => {
status = 'success';
result = r;
cache.set(userId, {
read() {
return result;
},
});
},
(e) => {
status = 'error';
result = e;
cache.set(userId, {
read() {
throw result;
},
});
}
);
cache.set(userId, {
read() {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
}
return result;
}
});
}
return cache.get(userId);
};
const UserProfile = ({ userId }) => {
const user = UserResource(userId).read();
return (
Essa abordagem fornece mais controle sobre o tamanho e a política de expiração do cache.
3. Coalescência de Requisições com Bibliotecas como `axios-extensions`
Bibliotecas como axios-extensions oferecem recursos mais avançados, como a coalescência de requisições. A coalescência de requisições combina várias requisições idênticas em uma única requisição, otimizando ainda mais o uso da rede. Isso é particularmente útil em cenários onde as requisições são iniciadas muito próximas umas das outras no tempo.
Primeiro, instale a biblioteca:
npm install axios axios-extensions
Em seguida, configure o Axios com o adaptador de cache fornecido pelo axios-extensions.
Exemplo usando `axios-extensions` e criando um recurso:
import React, { Suspense } from 'react';
import axios from 'axios';
import { cacheAdapterEnhancer, throttleAdapterEnhancer } from 'axios-extensions';
const instance = axios.create({
baseURL: 'https://api.example.com', // Substitua pelo seu endpoint de API
adapter: cacheAdapterEnhancer(axios.defaults.adapter, { enabledByDefault: true }),
});
const fetchUser = async (userId) => {
console.log(`Buscando usuário com ID: ${userId}`); // Simula uma requisição de rede
const response = await instance.get(`/users/${userId}`);
return response.data;
};
const UserResource = (userId) => {
let promise = null;
let status = 'pending'; // pendente, sucesso, erro
let result;
const suspender = fetchUser(userId).then(
(r) => {
status = 'success';
result = r;
},
(e) => {
status = 'error';
result = e;
}
);
return {
read() {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
}
return result;
},
};
};
const UserProfile = ({ userId }) => {
const user = UserResource(userId).read();
return (
Isso configura o Axios para usar um adaptador de cache, armazenando respostas em cache automaticamente com base na configuração da requisição. A função cacheAdapterEnhancer oferece opções para configurar o cache, como definir um tamanho máximo de cache ou tempo de expiração. O throttleAdapterEnhancer também pode ser usado para limitar o número de requisições feitas ao servidor dentro de um certo período, otimizando ainda mais o desempenho.
Melhores Práticas para Desduplicação de Recursos
Centralize o Gerenciamento de Recursos: Crie módulos ou serviços dedicados para gerenciar recursos. Isso promove a reutilização de código e facilita a implementação de estratégias de desduplicação.
Use Chaves Únicas: Garanta que suas chaves de cache sejam únicas e representem com precisão o recurso que está sendo buscado. Isso é crucial para evitar colisões no cache.
Considere a Invalidação do Cache: Implemente um mecanismo para invalidar o cache quando os dados mudam. Isso garante que seus componentes sempre exibam as informações mais atualizadas. Técnicas comuns incluem o uso de webhooks ou a invalidação manual do cache quando ocorrem atualizações.
Monitore o Desempenho do Cache: Acompanhe as taxas de acerto do cache (hit rates) e os tempos de resposta para identificar potenciais gargalos de desempenho. Ajuste sua estratégia de cache conforme necessário para otimizar o desempenho.
Implemente o Tratamento de Erros: Garanta que sua lógica de cache inclua um tratamento de erros robusto. Isso evita que erros se propaguem para seus componentes e proporciona uma melhor experiência ao usuário. Considere estratégias para tentar novamente requisições falhas ou exibir conteúdo de fallback.
Use o AbortController: Se um componente for desmontado antes que os dados sejam buscados, use o `AbortController` para cancelar a requisição, evitando trabalho desnecessário e possíveis vazamentos de memória.
Considerações Globais para Busca de Dados e Desduplicação
Ao projetar estratégias de busca de dados para um público global, vários fatores entram em jogo:
Redes de Distribuição de Conteúdo (CDNs): Utilize CDNs para distribuir seus ativos estáticos e respostas de API em locais geograficamente diversos. Isso reduz a latência para usuários que acessam sua aplicação de diferentes partes do mundo.
Dados Localizados: Implemente estratégias para servir dados localizados com base na localização ou nas preferências de idioma do usuário. Isso pode envolver o uso de diferentes endpoints de API ou a aplicação de transformações nos dados no lado do servidor ou do cliente. Por exemplo, um site de e-commerce europeu pode mostrar preços em Euros, enquanto o mesmo site visto dos Estados Unidos pode mostrar preços em Dólares Americanos.
Fusos Horários: Esteja atento aos fusos horários ao exibir datas e horas. Use bibliotecas de formatação e conversão apropriadas para garantir que os horários sejam exibidos corretamente para cada usuário.
Conversão de Moeda: Ao lidar com dados financeiros, use uma API de conversão de moeda confiável para exibir preços na moeda local do usuário. Considere fornecer opções para que os usuários alternem entre diferentes moedas.
Acessibilidade: Garanta que suas estratégias de busca de dados sejam acessíveis a usuários com deficiências. Isso inclui fornecer atributos ARIA apropriados para indicadores de carregamento e mensagens de erro.
Privacidade de Dados: Cumpra os regulamentos de privacidade de dados, como GDPR e CCPA, ao coletar e processar dados do usuário. Implemente medidas de segurança apropriadas para proteger as informações do usuário.
Por exemplo, um site de reservas de viagens com foco em um público global pode usar uma CDN para servir dados de disponibilidade de voos e hotéis de servidores localizados em diferentes regiões. O site também usaria uma API de conversão de moeda para exibir preços na moeda local do usuário e forneceria opções para filtrar os resultados da pesquisa com base nas preferências de idioma.
Conclusão
A desduplicação de recursos é uma técnica de otimização essencial para aplicações React que usam Suspense. Ao prevenir requisições duplicadas de busca de dados, você pode melhorar significativamente o desempenho, reduzir a carga no servidor e aprimorar a experiência do usuário. Seja escolhendo implementar um cache simples de promises ou aproveitando bibliotecas mais avançadas como lru-cache ou axios-extensions, o fundamental é entender os princípios subjacentes e escolher a solução que melhor se adapta às suas necessidades específicas. Lembre-se de considerar fatores globais como CDNs, localização e acessibilidade ao projetar suas estratégias de busca de dados para um público diversificado. Ao implementar essas melhores práticas, você pode construir aplicações React mais rápidas, eficientes e amigáveis ao usuário.