Você abre seu aplicativo de produção e percebe que ele está paralisado. O front-end não responde. As APIs de back-end estão expirando. As consultas do MongoDB parecem estar em execução indefinidamente. Sua caixa de entrada está inundada com reclamações de usuários. Sua equipe se reúne tentando fazer a triagem da situação.
Já esteve lá? Sim, eu também.
Sou um desenvolvedor Full Stack sênior e estou farto de aplicativos que funcionam bem enquanto você os usa apenas como um único usuário ou quando o espaço do problema é simples, mas simplesmente murcha e entra em colapso sob tráfego real ou um tarefa um pouco mais exigente.
Fique comigo e explicarei como abordei essas questões usando React, Node.js e MongoDB.
Não vou apenas dar a vocês outro tutorial simples, vou compartilhar uma história. Uma história sobre como resolver problemas do mundo real e como construir um aplicativo rápido e altamente escalável que possa passar no teste do tempo e de qualquer coisa que seja lançada nele.
1: Quando o React se tornou o gargalo
Acabamos de lançar uma atualização para nosso aplicativo web, desenvolvido com React, no meu trabalho. Estávamos cheios de confiança, acreditando que os usuários apreciariam os novos recursos.
No entanto, não demorou muito para começarmos a receber reclamações: o aplicativo carregava extremamente lentamente, as transições falhavam e os usuários estavam cada vez mais frustrados. Apesar de sabermos que os novos recursos eram benéficos, eles inadvertidamente levaram a problemas de desempenho. Nossa investigação revelou um problema: o aplicativo agrupava todos os seus componentes em um único pacote, o que forçava os usuários a baixar tudo cada vez que acessavam o aplicativo.
A solução: implementamos um conceito muito útil chamado Lazy Load. Eu já tinha me deparado com essa ideia antes, mas era exatamente o que precisávamos. Renovamos completamente a estrutura do aplicativo, garantindo que ele carregue apenas os componentes necessários quando necessário.
Veja como implementamos esta solução:
const Dashboard = React.lazy(() => import('./Dashboard')); const Profile = React.lazy(() => import('./Profile'));Loading...}>
O resultado: O impacto dessa mudança foi simplesmente notável. Vimos uma redução colossal de 30% em nosso pacote e os usuários experimentaram um carregamento inicial muito mais rápido. A melhor parte é que os usuários não tinham ideia de que certas partes do aplicativo ainda estavam carregando. Usamos o Suspense com sabedoria e mostramos uma mensagem de carregamento simples e não intrusiva.
2: Domando a fera da gestão estatal em reação
À medida que avançamos alguns meses, nossa equipe de desenvolvimento estava avançando e lançando muitas novas funcionalidades. Mas junto com o crescimento, começamos inadvertidamente a construir o que chamo de um aplicativo mais complexo. Redux rapidamente se tornou um passivo, em vez de um auxiliar, na facilitação de interações simples.
Então, passei algum tempo criando um POC para uma alternativa melhor. Documentei tudo isso e facilitei várias reuniões de compartilhamento de conhecimento sobre como seria essa abordagem. Finalmente decidimos como um grupo experimentar React Hooks (e em particular useReducer) como nossa solução proposta para gerenciamento de estado porque, em última análise, queríamos um código mais simples e menos espaço de tempo de execução massivo que as versões mais recentes do Redux tinham sobrecarga crescente com muitos recursos independentes menores. estados.
A transformação que se seguiu foi nada menos que revolucionária. Acabamos substituindo dezenas de linhas de código clichê por uma lógica de gancho concisa e fácil de entender. Aqui está um exemplo ilustrativo de como implementamos essa nova abordagem:
const initialState = { count: 0 }; function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count 1 }; case 'decrement': return { count: state.count - 1 }; default: throw new Error(); } } const CounterContext = React.createContext(); function CounterProvider({ children }) { const [state, dispatch] = useReducer(reducer, initialState); return ({children} ); }
O Resultado: O impacto desta transição foi profundo e de longo alcance. Nossa aplicação tornou-se significativamente mais previsível e mais fácil de raciocinar. A base de código, agora mais enxuta e intuitiva, permitiu que nossa equipe iterasse em um ritmo muito mais rápido. Talvez o mais importante seja que nossos desenvolvedores juniores relataram uma melhoria acentuada em sua capacidade de navegar e compreender a base de código. O resultado final foi uma situação ganha-ganha: menos código para manter, menos bugs para eliminar e uma equipe de desenvolvimento visivelmente mais feliz e produtiva.
3: Conquistando o campo de batalha de back-end – Otimizando APIs Node.js para desempenho máximo
Embora tenhamos conseguido introduzir muitas melhorias em nosso front-end, logo depois tivemos vários problemas no back-end. Nosso desempenho de API tornou-se horrível e houve poucos endpoints em particular que começaram a funcionar de maneira péssima. Esses endpoints fazem uma sequência de chamadas para diferentes serviços de terceiros e, com a crescente base de usuários, o sistema não foi capaz de lidar com essa carga.
Era bastante sensato o que estava errado: NÃO éramos paralelos! ou seja, as solicitações em cada terminal foram tratadas de maneira sequencial, ou seja, cada chamada seguinte aguardaria a conclusão da chamada anterior. Neste sistema de alta escala (cem mil solicitações), foi desastroso.
A solução: para corrigir isso, decidimos reescrever grande parte do nosso código e usar o poder de Promise.all() para fazer a solicitação da API de forma simultânea. Isso significa que você inicia várias solicitações e não precisa esperar até que cada chamada termine para iniciar a próxima.
Para isso, não estamos lançando uma chamada de API, esperando até que ela termine, fazendo outra e assim por diante…
Em vez de simplesmente usar Promise.all(), tudo foi iniciado de uma vez e muito mais rápido.
Veja como implementamos esta solução:
const getUserData = async () => { const [profile, posts, comments] = await Promise.all([ fetch('/api/profile'), fetch('/api/posts'), fetch('/api/comments') ]); return { profile, posts, comments }; };
O resultado: O impacto dessa otimização foi imediato e substancial. Observamos uma redução notável de 50% nos tempos de resposta e nosso back-end demonstrou resiliência significativamente melhorada sob carga pesada. Os usuários não sofreram mais atrasos frustrantes e vimos uma redução drástica no número de tempos limite do servidor. Essa melhoria não apenas melhorou a experiência do usuário, mas também permitiu que nosso sistema lidasse com um volume muito maior de solicitações sem comprometer o desempenho.
4: A missão do MongoDB – Domando a fera dos dados
À medida que nosso aplicativo ganhou força e nossa base de usuários cresceu em ordens de magnitude, tivemos que enfrentar um novo obstáculo: como dimensionar seus dados? Nossa instância do MongoDB, antes responsiva, começou a engasgar ao ter que lidar com milhões de documentos. Consultas que costumavam ser executadas em milissegundos levavam segundos para serem concluídas — ou expiravam.
Passamos alguns dias examinando as ferramentas de análise de desempenho do MongoDB e identificamos o grande vilão: consultas não indexadas. Algumas de nossas consultas mais comuns (por exemplo, solicitações de perfis de usuário) estavam verificando coleções inteiras nas quais poderiam usar índices sólidos.
A solução: Com as informações que tínhamos em mãos, sabíamos que tudo o que precisávamos fazer era criar índices compostos nos campos mais solicitados e que isso corrigiria o tempo de pesquisa do corpo do banco de dados para sempre. Veja como fizemos isso no que diz respeito aos campos “nome de usuário” e “e-mail”.
db.users.createIndex({ "username": 1, "email": 1 });
O resultado: O impacto dessa otimização foi simplesmente notável. Consultas que antes levavam até 2 segundos para serem executadas agora eram concluídas em menos de 200 milissegundos — uma melhoria de dez vezes no desempenho. Nosso banco de dados recuperou sua capacidade de resposta rápida, permitindo-nos lidar com um volume de tráfego significativamente maior sem qualquer lentidão perceptível.
No entanto, não paramos por aí. Reconhecendo que a nossa trajetória de rápido crescimento provavelmente continuaria, tomámos medidas proativas para garantir a escalabilidade a longo prazo. Implementamos sharding para distribuir nossos dados em vários servidores. Esta decisão estratégica permitiu-nos escalar horizontalmente, garantindo que a nossa capacidade de lidar com dados crescesse em conjunto com a expansão da nossa base de utilizadores.
5. Adotando microsserviços — Resolvendo o quebra-cabeça da escalabilidade
À medida que nossa base de usuários continuava a se multiplicar, ficava cada vez mais evidente que não apenas precisávamos dimensionar nossa infraestrutura, mas também evoluir nosso aplicativo para poder escalar com confiança. A arquitetura monolítica nos adequou bem quando éramos uma equipe menor, mas com o tempo tornou-se bastante complicada. Sabíamos que precisávamos dar o salto e começar a construir uma arquitetura de microsserviços — uma tarefa intimidante para qualquer equipe de engenharia, mas com muitas vantagens em escalabilidade e confiabilidade.
Um dos maiores problemas foi a comunicação entre os serviços. As solicitações HTTP realmente não funcionam para o nosso caso e nos deixaram com mais um gargalo no sistema, já que uma grande quantidade de operações aguardava resposta, todas incansáveis, e matava o programa se necessário, havia muito o que fazer. Neste ponto percebemos que usar RabbitMQ é a resposta óbvia aqui, então aplicamos sem pensar muito.
Veja como implementamos esta solução:
const amqp = require('amqplib/callback_api'); amqp.connect('amqp://localhost', (err, conn) => { conn.createChannel((err, ch) => { const queue = 'task_queue'; const msg = 'Hello World'; ch.assertQueue(queue, { durable: true }); ch.sendToQueue(queue, Buffer.from(msg), { persistent: true }); console.log(`Sent ${msg}`); }); });
O Resultado: A transição em si junto com a comunicação feita através do RabbitMQ pareceram mágica do nosso ponto de vista… e os números confirmaram isso!!! Tornamo-nos sortudos proprietários de microsserviços fracamente acoplados, onde cada serviço poderia ser dimensionado por conta própria. De repente, picos reais de tráfego na zona DNS concreta não envolveram medo de que o sistema estivesse inoperante (já que não importa qual operação de serviço pergunte o mesmo porque eles estão sempre em cascata), mas funcionaram bem, já que as partes/operações restantes apenas levantaram as mãos calmamente dizendo ' Eu posso dormir, minha querida'. A manutenção também se tornou mais fácil e menos problemática, enquanto a adição de novos recursos ou atualizações proporcionou uma operação mais rápida e mais confiável.
Conclusão: traçando um rumo para inovação futura
Cada etapa dessa jornada emocionante foi uma lição, nos lembrando que o desenvolvimento full-stack é mais do que escrever código. É entender e, em seguida, resolver problemas complicados e inter-relacionados – desde tornar nossos front-ends mais rápidos e construir back-ends para resistir a falhas, até lidar com bancos de dados que aumentam enquanto sua base de usuários explode.
Ao olharmos para o segundo semestre de 2024 e além, a crescente demanda por aplicativos da web não diminuirá. Se continuarmos focados na criação de aplicativos escaláveis, com desempenho otimizado e bem arquitetados, estaremos posicionados para resolver qualquer problema hoje — e aproveitar esses outros desafios em nosso futuro. Essas experiências da vida real tiveram um grande impacto na forma como abordo o desenvolvimento full-stack – e mal posso esperar para ver onde essas influências continuarão a impulsionar nossa indústria!
Mas e você? Você já enfrentou obstáculos semelhantes ou teve sorte com outras formas criativas de superar esses problemas? Eu adoraria ouvir suas histórias ou ideias. Deixe-me saber nos comentários ou entre em contato comigo!
Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.
Copyright© 2022 湘ICP备2022001581号-3