Abres tu aplicación de producción y notas que se está deteniendo. La interfaz no responde. Las API de backend están agotadas. Las consultas de MongoDB parecen ejecutarse indefinidamente. Tu bandeja de entrada está inundada de quejas de usuarios. Tu equipo se reúne para tratar de clasificar la situación.
¿Has estado allí? Sí, yo también.
Soy desarrollador senior Full Stack y estoy harto de las aplicaciones que funcionan bien cuando solo las usas como un solo usuario o cuando el espacio del problema es simple pero luego simplemente se marchita y colapsa bajo el tráfico real o un tarea un poco más exigente.
Quédate conmigo y te explicaré cómo solucioné estas inquietudes usando React, Node.js y MongoDB.
No solo les daré otro simple tutorial, les compartiré una historia. Una historia sobre cómo abordar problemas del mundo real y cómo crear una aplicación rápida y altamente escalable que pueda pasar la prueba del tiempo y cualquier cosa que se le presente.
1: Cuando React se convirtió en el cuello de botella
Acabábamos de implementar una actualización para nuestra aplicación web, desarrollada con React, en mi trabajo. Estábamos llenos de confianza y creíamos que los usuarios apreciarían las nuevas funciones.
Sin embargo, no pasó mucho tiempo antes de que empezáramos a recibir quejas: la aplicación se cargaba muy lentamente, las transiciones tartamudeaban y los usuarios se sentían cada vez más frustrados. A pesar de saber que las nuevas funciones eran beneficiosas, sin darse cuenta provocaron problemas de rendimiento. Nuestra investigación reveló un problema: la aplicación agrupaba todos sus componentes en un solo paquete, lo que obligaba a los usuarios a descargar todo cada vez que accedían a la aplicación.
La solución: implementamos un concepto muy útil llamado Lazy Loading. Me había cruzado con esta idea antes, pero era exactamente lo que necesitábamos. Renovamos completamente la estructura de la aplicación, asegurándonos de que solo cargue los componentes necesarios cuando sea necesario.
A continuación se muestra cómo implementamos esta solución:
const Dashboard = React.lazy(() => import('./Dashboard')); const Profile = React.lazy(() => import('./Profile'));Loading...}>
El resultado: El impacto de este cambio fue nada menos que notable. Vimos una enorme reducción del 30 % en nuestro paquete y los usuarios experimentaron una carga inicial mucho más rápida. Lo mejor fue que los usuarios no tenían idea de que ciertas partes de la aplicación todavía se estaban cargando, usamos Suspense sabiamente y mostramos un mensaje de carga simple y no intrusivo.
2: Domar a la bestia de la gestión estatal en React
A medida que avanzamos unos meses, nuestro equipo de desarrollo estaba avanzando y lanzando muchas funciones nuevas. Pero junto con el crecimiento, sin darnos cuenta, comenzamos a crear lo que yo llamo una aplicación más compleja. Redux rápidamente se convirtió en una carga en lugar de una ayuda para facilitar interacciones simples.
Entonces, dediqué algún tiempo a crear una prueba de concepto para encontrar una mejor alternativa. Lo documenté muchísimo y facilité múltiples reuniones para compartir conocimientos sobre cómo sería ese enfoque. Finalmente decidimos como grupo probar React Hooks (y en particular useReducer) como nuestra solución propuesta para administrar el estado porque, en última instancia, queríamos un código más simple y menos espacio de tiempo de ejecución masivo que las versiones más nuevas de Redux tenían, con muchos recursos autónomos más pequeños. estados.
La transformación que siguió fue nada menos que revolucionaria. Nos encontramos reemplazando docenas de líneas de código repetitivo con una lógica de enlace concisa y fácil de entender. A continuación se muestra un ejemplo ilustrativo de cómo implementamos este nuevo enfoque:
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} ); }
El resultado: El impacto de esta transición fue profundo y de gran alcance. Nuestra aplicación se volvió significativamente más predecible y más fácil de razonar. El código base, ahora más ágil e intuitivo, permitió a nuestro equipo iterar a un ritmo mucho más rápido. Quizás lo más importante es que nuestros desarrolladores junior informaron una marcada mejora en su capacidad para navegar y comprender el código base. El resultado final fue una situación en la que todos salían ganando: menos código que mantener, menos errores que corregir y un equipo de desarrollo notablemente más feliz y productivo.
3: Conquistando el campo de batalla backend: optimización de las API de Node.js para obtener el máximo rendimiento
Si bien pudimos introducir muchas mejoras en nuestro frontend, poco después tuvimos varios problemas en el backend. El rendimiento de nuestra API se volvió horrible y hubo pocos puntos finales en particular que comenzaron a funcionar abismalmente. Esos puntos finales realizan una secuencia de llamadas a diferentes servicios de terceros y, con una base de usuarios cada vez mayor, el sistema no pudo manejar esta carga.
Lo que estaba mal era bastante de sentido común: ¡NO éramos paralelos! es decir, las solicitudes en cada punto final se manejaron de manera secuencial, es decir, cada siguiente llamada esperaría a que se completara la llamada anterior. En este sistema de gran escala (cien mil solicitudes), resultó desastroso.
La solución: para solucionar este problema, decidimos reescribir gran parte de nuestro código y usar el poder de Promise.all() para realizar la solicitud de API de forma simultánea. Eso significa que usted inicia múltiples solicitudes y no tiene que esperar hasta que finalice cada llamada para iniciar la siguiente.
Para hacerlo, no lanzamos una llamada API, no esperamos hasta que finalice, hacemos otra y así sucesivamente…
En lugar de simplemente usar Promise.all(), todo se inició de una vez y mucho más rápido.
A continuación se muestra cómo implementamos esta solución:
const getUserData = async () => { const [profile, posts, comments] = await Promise.all([ fetch('/api/profile'), fetch('/api/posts'), fetch('/api/comments') ]); return { profile, posts, comments }; };
El resultado: El impacto de esta optimización fue inmediato y sustancial. Observamos una notable reducción del 50 % en los tiempos de respuesta y nuestro backend demostró una resiliencia significativamente mejorada bajo cargas pesadas. Los usuarios ya no experimentaron retrasos frustrantes y vimos una disminución dramática en la cantidad de tiempos de espera del servidor. Esta mejora no solo mejoró la experiencia del usuario sino que también permitió que nuestro sistema manejara un volumen mucho mayor de solicitudes sin comprometer el rendimiento.
4: La búsqueda de MongoDB: Domar a la bestia de los datos
A medida que nuestra aplicación ganó terreno y nuestra base de usuarios creció en órdenes de magnitud, tuvimos que enfrentar un nuevo obstáculo: ¿cómo se escalan sus datos? Nuestra instancia de MongoDB, que alguna vez fue receptiva, comenzó a ahogarse al tener que lidiar con millones de documentos. Las consultas que solían ejecutarse en milisegundos tardaron segundos en completarse o expiraron.
Pasamos unos días investigando las herramientas de análisis de rendimiento de MongoDB e identificamos al gran malo: las consultas no indexadas. Algunas de nuestras consultas más comunes (por ejemplo, solicitudes de perfiles de usuario) eran escanear colecciones enteras para poder utilizar índices sólidos.
La solución: Con la información que teníamos a mano, sabíamos que todo lo que teníamos que hacer era crear índices compuestos en los campos más solicitados y que esto arreglaría el tiempo de búsqueda del cuerpo de nuestra base de datos para siempre. Así es como lo hicimos con los campos "nombre de usuario" y "correo electrónico".
db.users.createIndex({ "username": 1, "email": 1 });
El resultado: El impacto de esta optimización fue nada menos que notable. Las consultas que antes tardaban hasta 2 segundos en ejecutarse ahora se completaban en menos de 200 milisegundos, una mejora diez veces mayor en el rendimiento. Nuestra base de datos recuperó su ágil capacidad de respuesta, lo que nos permite manejar un volumen de tráfico significativamente mayor sin ninguna desaceleración notable.
Sin embargo, no nos detuvimos ahí. Al reconocer que nuestra trayectoria de rápido crecimiento probablemente continuaría, tomamos medidas proactivas para garantizar la escalabilidad a largo plazo. Implementamos fragmentación para distribuir nuestros datos en múltiples servidores. Esta decisión estratégica nos permitió escalar horizontalmente, asegurando que nuestra capacidad para manejar datos creciera junto con nuestra base de usuarios en expansión.
5. Adoptar microservicios: resolver el rompecabezas de la escalabilidad
A medida que nuestra base de usuarios continuaba multiplicándose, se hacía cada vez más evidente que no solo necesitábamos escalar nuestra infraestructura, sino que teníamos que evolucionar nuestra aplicación para poder escalar con confianza. La arquitectura monolítica nos venía bien cuando éramos un equipo más pequeño, pero con el tiempo se volvió bastante engorrosa. Sabíamos que necesitábamos dar el salto y comenzar a construir hacia una arquitectura de microservicios: una tarea intimidante para cualquier equipo de ingeniería, pero con una gran ventaja de escalabilidad y confiabilidad.
Uno de los mayores problemas fue la comunicación entre servicios. Las solicitudes HTTP realmente no funcionan para nuestro caso y nos han dejado con un cuello de botella más en el sistema, ya que una gran cantidad de operaciones esperaban inquietamente una respuesta y eliminaban el programa si era necesario, uno tenía demasiado que hacer. En este punto nos dimos cuenta de que usar RabbitMQ es la respuesta obvia, así que lo aplicamos sin pensar demasiado.
A continuación se muestra cómo implementamos esta solución:
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}`); }); });
El resultado: La transición en sí junto con la comunicación realizada a través de RabbitMQ parecía mágica desde nuestro punto de vista… ¡¡¡y los números lo confirmaron!!! Nos convertimos en afortunados propietarios de microservicios poco acoplados donde cada servicio podía escalarse por sí solo. De repente, los picos de tráfico reales en la zona dns concreta no implicaron el temor de que el sistema se estuviera cayendo (ya que no importa qué operación de servicio solicite lo mismo porque siempre son en cascada), sino que funcionó muy bien, ya que las partes/operaciones restantes simplemente levantaron la mano con calma diciendo " Puedo dormir querida'. El mantenimiento también se volvió más fácil y menos problemático, mientras que agregar nuevas funciones o actualizaciones hizo que la operación fuera más rápida y segura.
Conclusión: Trazando un rumbo para la innovación futura
Cada paso en este emocionante viaje fue una lección que nos recordó que el desarrollo completo es más que escribir código. Se trata de comprender y luego resolver problemas complicados interrelacionados, desde hacer que nuestras interfaces sean más rápidas y construir backends para resistir fallas, hasta lidiar con bases de datos que escalan mientras su base de usuarios explota.
Si miramos hacia la segunda mitad de 2024 y más allá, la creciente demanda de aplicaciones web no se desacelerará. Si nos mantenemos enfocados en crear aplicaciones escalables, con rendimiento optimizado y bien diseñadas, entonces estaremos posicionados para resolver cualquier problema hoy y aprovechar esos otros desafíos en nuestro futuro. Estas experiencias de la vida real han tenido un gran impacto en la forma en que abordo el desarrollo completo, ¡y no puedo esperar a ver dónde estas influencias seguirán impulsando nuestra industria!
¿Pero y tú? ¿Se ha enfrentado a obstáculos similares o ha tenido suerte con otras formas creativas de superar estos problemas? Me encantaría escuchar tus historias o ideas. ¡Déjamelo saber en los comentarios o conéctate conmigo!
Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.
Copyright© 2022 湘ICP备2022001581号-3