Reconstrucción de un gran sitio web de comercio electrónico con Next.js (estudio de caso)

Publicado: 2022-03-10
Resumen rápido ↬ Pasamos de una plataforma de comercio electrónico integrada más tradicional a una plataforma autónoma con Next.js. Estas son las lecciones más importantes aprendidas al reconstruir un gran sitio de comercio electrónico con Next.js.

En nuestra empresa, Unplatform, hemos estado construyendo sitios de comercio electrónico durante décadas. A lo largo de esos años, hemos visto evolucionar la pila de tecnología desde páginas renderizadas por servidor con algunos JavaScript y CSS menores hasta aplicaciones JavaScript completas.

La plataforma que usamos para nuestros sitios de comercio electrónico se basó en ASP.NET y cuando los visitantes comenzaron a esperar más interacción, agregamos React para el front-end. Aunque mezclar los conceptos de un marco web de servidor como ASP.NET con un marco web del lado del cliente como React hizo las cosas más complicadas, estábamos muy contentos con la solución. Eso fue hasta que entramos en producción con nuestro cliente de mayor tráfico. Desde el momento en que lanzamos, experimentamos problemas de rendimiento . Los Core Web Vitals son importantes, más aún en el comercio electrónico. En este estudio de Deloitte: Milliseconds Make Millions, los investigadores analizaron datos de sitios móviles de 37 marcas diferentes. Como resultado, descubrieron que una mejora del rendimiento de 0,1 s puede generar un aumento del 10 % en la conversión.

Para mitigar los problemas de rendimiento, tuvimos que agregar una gran cantidad de servidores adicionales (no presupuestados) y tuvimos que almacenar en caché agresivamente las páginas en un proxy inverso. Esto incluso nos obligó a deshabilitar partes de la funcionalidad del sitio. Terminamos teniendo una solución realmente complicada y costosa que, en algunos casos, solo sirvió estáticamente algunas páginas.

Obviamente, esto no se sentía bien, hasta que nos enteramos de Next.js. Next.js es un marco web basado en React que le permite generar páginas estáticamente, pero también puede usar la representación del lado del servidor, lo que lo hace ideal para el comercio electrónico. Se puede alojar en un CDN como Vercel o Netlify, lo que da como resultado una latencia más baja . Vercel y Netlify también usan funciones sin servidor para la representación del lado del servidor, que es la forma más eficiente de escalar.

Desafíos

Desarrollar con Next.js es asombroso, pero definitivamente hay algunos desafíos. La experiencia del desarrollador con Next.js es algo que solo necesita experimentar. El código que escribes se visualiza instantáneamente en tu navegador y la productividad se dispara. Esto también es un riesgo porque fácilmente puede concentrarse demasiado en la productividad y descuidar la capacidad de mantenimiento de su código. Con el tiempo, esto y la naturaleza sin tipo de JavaScript pueden provocar la degradación de su base de código. El número de errores aumenta y la productividad comienza a disminuir.

También puede ser un reto en el lado del tiempo de ejecución de las cosas . Los cambios más pequeños en su código pueden provocar una caída en el rendimiento y otros Core Web Vitals. Además, el uso descuidado de la representación del lado del servidor puede generar costos de servicio inesperados.

Echemos un vistazo más de cerca a nuestras lecciones aprendidas para superar estos desafíos.

  1. Modularice su base de código
  2. Lint y formatee su código
  3. Usar mecanografiado
  4. Planificar el rendimiento y medir el rendimiento
  5. Agregue comprobaciones de rendimiento a su puerta de calidad
  6. Agregar pruebas automatizadas
  7. Administre agresivamente sus dependencias
  8. Usar un servicio de agregación de registros
  9. La funcionalidad de reescritura de Next.js permite la adopción incremental
¡Más después del salto! Continúe leyendo a continuación ↓

Lección aprendida: modularice su base de código

Los marcos front-end como Next.js hacen que sea muy fácil comenzar en estos días. Simplemente ejecute npx create-next-app y puede comenzar a codificar. Pero si no tiene cuidado y comienza a generar código sin pensar en el diseño, podría terminar con una gran bola de barro.

Cuando ejecute npx create-next-app , tendrá una estructura de carpetas como la siguiente (así es como se estructuran la mayoría de los ejemplos):

 /public logo.gif /src /lib /hooks useForm.js /api content.js /components Header.js Layout.js /pages Index.js

Empezamos usando la misma estructura. Teníamos algunas subcarpetas en la carpeta de componentes para componentes más grandes, pero la mayoría de los componentes estaban en la carpeta de componentes raíz. No hay nada de malo con este enfoque y está bien para proyectos más pequeños. Sin embargo, a medida que nuestro proyecto creció, se volvió más difícil razonar sobre los componentes y dónde se usan. ¡Incluso encontramos componentes que ya no se usaban! También promueve una gran bola de barro, porque no hay una guía clara sobre qué código debe depender de qué otro código.

Para resolver esto, decidimos refactorizar el código base y agrupar el código por módulos funcionales (algo así como módulos NPM) en lugar de conceptos técnicos:

 /src /modules /catalog /components productblock.js /checkout /api cartservice.js /components cart.js

En este pequeño ejemplo, hay un módulo de pago y un módulo de catálogo. Agrupar el código de esta manera conduce a una mejor capacidad de descubrimiento: con solo mirar la estructura de carpetas, sabe exactamente qué tipo de funcionalidad hay en la base de código y dónde encontrarla. También hace que sea mucho más fácil razonar acerca de las dependencias . En la situación anterior, había muchas dependencias entre los componentes. Tuvimos solicitudes de incorporación de cambios en el proceso de pago que también afectaron a los componentes del catálogo. Esto aumentó el número de conflictos de combinación y dificultó la realización de cambios.

La solución que funcionó mejor para nosotros fue mantener las dependencias entre los módulos al mínimo absoluto (si realmente necesita una dependencia, asegúrese de que sea unidireccional) e introducir un nivel de "proyecto" que une todo:

 /src /modules /common /atoms /lib /catalog /components productblock.js /checkout /api cartservice.js /components cart.js /search /project /layout /components /templates productdetail.js cart.js /pages cart.js

Una descripción visual de esta solución:

Una descripción general de un ejemplo de proyecto modularizado
Una descripción general de un ejemplo de proyecto modularizado (vista previa grande)

El nivel de proyecto contiene el código para el diseño del sitio de comercio electrónico y las plantillas de página. En Next.js, un componente de página es una convención y da como resultado una página física. En nuestra experiencia, estas páginas a menudo necesitan reutilizar la misma implementación y es por eso que hemos introducido el concepto de "plantillas de página". Las plantillas de página usan los componentes de los diferentes módulos, por ejemplo, la plantilla de la página de detalles del producto usará componentes del catálogo para mostrar información del producto, pero también un componente para agregar al carrito desde el módulo de pago.

También tenemos un módulo común, porque todavía hay código que necesita ser reutilizado por los módulos funcionales. Contiene átomos simples que son componentes de React que se usan para brindar una apariencia y una sensación consistentes. También contiene código de infraestructura, piense en ciertos ganchos de reacción genéricos o código de cliente GraphQL.

Advertencia : asegúrese de que el código en el módulo común sea estable y siempre piénselo dos veces antes de agregar código aquí, para evitar que se enrede el código.

Micro frontales

En soluciones aún más grandes o cuando se trabaja con diferentes equipos, puede tener sentido dividir la aplicación aún más en las llamadas micro-frontends. En resumen, esto significa dividir aún más la aplicación en varias aplicaciones físicas que se alojan de forma independiente en diferentes URL. Por ejemplo: checkout.mydomain.com y catalog.mydomain.com. Luego, estos son integrados por una aplicación diferente que actúa como un proxy.

La funcionalidad de reescritura de Next.js es excelente para esto y su uso es compatible con las llamadas zonas múltiples.

Un ejemplo de una configuración multizona
Un ejemplo de una configuración multizona (Vista previa grande)

La ventaja de las zonas múltiples es que cada zona administra sus propias dependencias. También facilita la evolución gradual de la base de código: si sale una nueva versión de Next.js o React, puede actualizar las zonas una por una en lugar de tener que actualizar toda la base de código a la vez. En una organización de varios equipos, esto puede reducir en gran medida las dependencias entre equipos.

Otras lecturas

  • “Estructura del proyecto Next.js”, Yannick Wittwer, Medium
  • “Una guía 2021 sobre cómo estructurar su proyecto Next.js de manera flexible y eficiente”, Vadorequest, Dev.to.
  • "Microfrontends", Michael Geers

Lección aprendida: lint y formatee su código

Esto es algo que aprendimos en un proyecto anterior: si trabaja en el mismo código base con varias personas y no usa un formateador, su código pronto se volverá muy inconsistente. Incluso si está utilizando convenciones de codificación y está haciendo revisiones, pronto comenzará a notar los diferentes estilos de codificación, dando una impresión desordenada del código.

Un linter verificará su código en busca de posibles problemas y un formateador se asegurará de que el código esté formateado de manera consistente. Usamos ESLint y más bonitos y creemos que son geniales. No tiene que pensar en el estilo de codificación, lo que reduce la carga cognitiva durante el desarrollo.

Afortunadamente, Next.js 11 ahora es compatible con ESLint listo para usar (https://nextjs.org/blog/next-11), lo que hace que sea muy fácil de configurar ejecutando npx next lint. Esto le ahorra mucho tiempo porque viene con una configuración predeterminada para Next.js. Por ejemplo, ya está configurado con una extensión ESLint para React. Aún mejor, viene con una nueva extensión específica de Next.js que incluso detectará problemas con su código que podrían afectar potencialmente a Core Web Vitals de su aplicación. En un párrafo posterior, hablaremos sobre las puertas de calidad que pueden ayudarlo a evitar enviar código a un producto que daña accidentalmente su Core Web Vitals. Esta extensión le brinda comentarios mucho más rápido, lo que la convierte en una gran adición.

Otras lecturas

  • “ESLint,” Documentos de Next.js
  • "ESLint", sitio web oficial

Lección aprendida: usar TypeScript

A medida que los componentes se modificaron y refactorizaron, notamos que algunos de los accesorios de los componentes ya no se usaban. Además, en algunos casos, experimentamos errores debido a que faltaban o se pasaban tipos incorrectos de accesorios a los componentes.

TypeScript es un superconjunto de JavaScript y agrega tipos, lo que permite que un compilador verifique estáticamente su código, como un linter con esteroides.

Al comienzo del proyecto, realmente no vimos el valor de agregar TypeScript. Sentimos que era solo una abstracción innecesaria. Sin embargo, uno de nuestros colegas tuvo buenas experiencias con TypeScript y nos convenció de probarlo. Afortunadamente, Next.js tiene una excelente compatibilidad con TypeScript lista para usar y TypeScript le permite agregarlo a su solución de manera incremental. Esto significa que no tiene que reescribir o convertir todo su código base de una sola vez, pero puede comenzar a usarlo de inmediato y convertir lentamente el resto del código base.

Una vez que comenzamos a migrar componentes a TypeScript, inmediatamente encontramos problemas con valores incorrectos que se pasaban a componentes y funciones. Además, el ciclo de comentarios del desarrollador se acortó y se le notifican los problemas antes de ejecutar la aplicación en el navegador. Otro gran beneficio que encontramos es que hace que sea mucho más fácil refactorizar el código: es más fácil ver dónde se está utilizando el código e inmediatamente detecta los accesorios y el código de los componentes no utilizados. En resumen, los beneficios de TypeScript:

  1. Reduce el número de errores.
  2. Hace que sea más fácil refactorizar su código
  3. El código se vuelve más fácil de leer

Otras lecturas

  • "TypeScript", Documentos de Next.js
  • Mecanografiado, sitio web oficial

Lección aprendida: planificar para el desempeño y medir el desempeño

Next.js admite diferentes tipos de representación previa: generación estática y representación del lado del servidor. Para obtener el mejor rendimiento, se recomienda utilizar la generación estática, que ocurre durante el tiempo de compilación, pero esto no siempre es posible. Piense en las páginas de detalles del producto que contienen información sobre acciones. Este tipo de información cambia con frecuencia y ejecutar una compilación cada vez que no escala bien. Afortunadamente, Next.js también admite un modo llamado Regeneración estática incremental (ISR), que sigue generando la página de forma estática, pero genera una nueva en segundo plano cada x segundos. Hemos aprendido que este modelo funciona muy bien para aplicaciones más grandes. El rendimiento sigue siendo excelente, requiere menos tiempo de CPU que la representación del lado del servidor y reduce los tiempos de compilación: las páginas solo se generan en la primera solicitud. Para cada página que agregue, debe pensar en el tipo de representación necesaria. Primero, vea si puede usar la generación estática; si no, vaya a la regeneración estática incremental, y si eso tampoco es posible, aún puede usar la representación del lado del servidor.

Next.js determina automáticamente el tipo de representación en función de la ausencia de los métodos getServerSideProps y getInitialProps en la página. Es fácil cometer un error, lo que podría hacer que la página se renderice en el servidor en lugar de generarse estáticamente. El resultado de una compilación de Next.js muestra exactamente qué página usa qué tipo de representación, así que asegúrese de verificar esto. También ayuda a monitorear la producción y rastrear el rendimiento de las páginas y el tiempo de CPU involucrado. La mayoría de los proveedores de alojamiento le cobran en función del tiempo de CPU y esto ayuda a evitar sorpresas desagradables. Describiré cómo monitoreamos esto en el párrafo Lección aprendida: Usar un servicio de agregación de registros.

Tamaño del paquete

Para tener un buen rendimiento es crucial minimizar el tamaño del paquete. Next.js tiene muchas funciones listas para usar que ayudan, por ejemplo, división automática de código. Esto asegurará que solo se carguen el JavaScript y el CSS necesarios para cada página. También genera diferentes paquetes para el cliente y para el servidor. Sin embargo, es importante mantener un ojo en estos. Por ejemplo, si importa módulos de JavaScript de manera incorrecta, el JavaScript del servidor puede terminar en el paquete del cliente, aumentando considerablemente el tamaño del paquete del cliente y perjudicando el rendimiento. Agregar dependencias de NPM también puede tener un gran impacto en el tamaño del paquete.

Afortunadamente, Next.js viene con un analizador de paquetes que le brinda información sobre qué código ocupa qué parte de los paquetes.

El analizador de paquetes de paquetes web le muestra el tamaño de los paquetes en su paquete
El analizador de paquetes de paquetes web le muestra el tamaño de los paquetes en su paquete (vista previa grande)

Otras lecturas

  • “Next.js + Analizador de paquete Webpack,” Vercel, GitHub
  • "Obtención de datos", Documentos de Next.js

Lección aprendida: agregue comprobaciones de rendimiento a su control de calidad

Uno de los grandes beneficios de usar Next.js es la capacidad de generar páginas estáticamente y poder implementar la aplicación en el perímetro (CDN), lo que debería resultar en un gran rendimiento y Web Vitals. Aprendimos que, incluso con una gran tecnología como Next.js, obtener y mantener una excelente puntuación de faro es realmente difícil. Ocurrió varias veces que, después de implementar algunos cambios en la producción, la puntuación del faro se redujo significativamente. Para recuperar el control, hemos agregado pruebas automáticas de faros a nuestra puerta de calidad. Con esta acción de Github, puede agregar automáticamente pruebas de faro a sus solicitudes de incorporación de cambios. Usamos Vercel y cada vez que se crea una solicitud de extracción, Vercel la implementa en una URL de vista previa y usamos la acción de Github para ejecutar pruebas de faro en esta implementación.

Un ejemplo de los resultados del faro en una solicitud de extracción de Github
Un ejemplo de los resultados del faro en una solicitud de extracción de Github (vista previa grande)

Si no desea configurar la acción de GitHub usted mismo, o si desea llevar esto aún más lejos, también podría considerar un servicio de monitoreo de rendimiento de terceros como DebugBear. Vercel también ofrece una función de análisis, que mide los Web Vitals centrales de su implementación de producción. Vercel Analytics en realidad recopila las medidas de los dispositivos de sus visitantes, por lo que estos puntajes son realmente lo que experimentan sus visitantes. En el momento de escribir este artículo, Vercel Analytics solo funciona en implementaciones de producción.

Lección aprendida: agregar pruebas automatizadas

Cuando el código base se hace más grande, se vuelve más difícil determinar si los cambios en el código podrían haber roto la funcionalidad existente. Según nuestra experiencia, es fundamental contar con un buen conjunto de pruebas de extremo a extremo como red de seguridad. Incluso si tiene un proyecto pequeño, puede hacer su vida mucho más fácil cuando tiene al menos algunas pruebas básicas de humo. Hemos estado usando Cypress para esto y nos encanta. La combinación de usar Netlify o Vercel para implementar automáticamente su solicitud de extracción en un entorno temporal y ejecutar sus pruebas E2E no tiene precio.

Usamos cypress-io/GitHub-action para ejecutar automáticamente las pruebas de cypress contra nuestras solicitudes de incorporación de cambios. Dependiendo del tipo de software que esté creando, puede ser valioso tener pruebas más granulares usando Enzyme o JEST. La contrapartida es que estos están más estrechamente acoplados a su código y requieren más mantenimiento.

Un ejemplo de comprobaciones automatizadas en una solicitud de extracción de Github
Un ejemplo de comprobaciones automatizadas en una solicitud de extracción de Github (vista previa grande)

Lección aprendida: administre agresivamente sus dependencias

Administrar dependencias se convierte en una actividad que requiere mucho tiempo, pero que es muy importante cuando se mantiene una gran base de código de Next.js. NPM facilitó la adición de paquetes y parece haber un paquete para todo en estos días. Mirando hacia atrás, muchas veces, cuando introdujimos un nuevo error o tuvimos una caída en el rendimiento, tuvo algo que ver con un paquete NPM nuevo o actualizado.

Entonces, antes de instalar un paquete, siempre debe preguntarse lo siguiente:

  • ¿Cuál es la calidad del paquete?
  • ¿Qué significará agregar este paquete para el tamaño de mi paquete?
  • ¿Es este paquete realmente necesario o hay alternativas?
  • ¿Se sigue manteniendo activamente el paquete?

Para mantener el tamaño del paquete pequeño y minimizar el esfuerzo necesario para mantener estas dependencias, es importante mantener el número de dependencias lo más pequeño posible. Tu futuro yo te lo agradecerá cuando estés manteniendo el software.

Sugerencia : la extensión Import Cost VSCode muestra automáticamente el tamaño de los paquetes importados.

Manténgase al día con las versiones de Next.js

Mantenerse al día con Next.js y React es importante. No solo le dará acceso a nuevas funciones, sino que las nuevas versiones también incluirán correcciones de errores y correcciones para posibles problemas de seguridad. Afortunadamente, Next.js hace que la actualización sea increíblemente fácil al proporcionar Codemods (https://nextjs.org/docs/advanced-features/codemods). Estas son transformaciones de código automáticas que actualizan automáticamente su código.

Actualizar dependencias

Por la misma razón, es importante mantener actualizadas las versiones de Next.js y React; también es importante actualizar otras dependencias. Dependabot de Github (https://github.com/dependabot) realmente puede ayudar aquí. Automáticamente creará Pull Requests con dependencias actualizadas. Sin embargo, actualizar las dependencias puede potencialmente romper las cosas, por lo que tener pruebas automatizadas de extremo a extremo aquí puede ser realmente un salvavidas.

Lección aprendida: usar un servicio de agregación de registros

Para asegurarnos de que la aplicación se comporta correctamente y para encontrar problemas de forma preventiva, hemos descubierto que es absolutamente necesario configurar un servicio de agregación de registros. Vercel le permite iniciar sesión y ver los registros, pero estos se transmiten en tiempo real y no se conservan. Tampoco admite la configuración de alertas y notificaciones.

Algunas excepciones pueden tardar mucho en aparecer. Por ejemplo, habíamos configurado Stale-While-Revalidate para una página en particular. En algún momento, notamos que las páginas no se actualizaban y que se servían datos antiguos. Después de verificar el registro de Vercel, encontramos que estaba ocurriendo una excepción durante la representación en segundo plano de la página. Al usar un servicio de agregación de registros y configurar una alerta para excepciones, hubiéramos podido detectar esto mucho antes.

Los servicios de agregación de registros también pueden ser útiles para monitorear los límites de los planes de precios de Vercel. La página de uso de Vercel también le brinda información sobre esto, pero el uso de un servicio de agregación de registros le permite agregar notificaciones cuando alcanza un cierto umbral. Más vale prevenir que curar, especialmente cuando se trata de facturación.

Vercel ofrece una serie de integraciones listas para usar con servicios de agregación de registros, que incluyen Datadog, Logtail, Logalert, Sentry y más.

Ver el registro de solicitud de Next.js en Datadog
Ver el registro de solicitud de Next.js en Datadog (vista previa grande)

Otras lecturas

  • “Integraciones”, Vercel

Lección aprendida: la funcionalidad de reescritura de Next.js permite la adopción incremental

A menos que haya algunos problemas serios con el sitio web actual, no muchos clientes estarán emocionados de reescribir todo el sitio web. Pero, ¿qué pasaría si pudiera comenzar con la reconstrucción solo de las páginas que más importan en términos de Web Vitals? Eso es exactamente lo que hicimos para otro cliente. En lugar de reconstruir todo el sitio, solo reconstruimos las páginas que más importan para el SEO y la conversión. En este caso, las páginas de detalles y categorías del producto. Al reconstruirlos con Next.js, el rendimiento aumentó considerablemente.

La funcionalidad de reescritura de Next.js es excelente para esto. Creamos un nuevo front-end de Next.js que contiene las páginas del catálogo y lo implementamos en la CDN. Next.js vuelve a escribir todas las demás páginas existentes en el sitio web existente. De esta manera, puede comenzar a disfrutar de los beneficios de un sitio Next.js con poco esfuerzo o bajo riesgo.

Otras lecturas

  • "Reescrituras", Documentos de Next.js

¿Que sigue?

Cuando lanzamos la primera versión del proyecto y comenzamos a realizar pruebas de rendimiento serias, nos entusiasmaron los resultados. No solo los tiempos de respuesta de la página y Web Vitals fueron mucho mejores que antes, sino que los costos operativos también fueron una fracción de lo que eran antes. Next.js y JAMStack generalmente le permiten escalar horizontalmente de la manera más rentable.

Cambiar de una arquitectura más orientada al back-end a algo como Next.js es un gran paso. La curva de aprendizaje puede ser bastante empinada e, inicialmente, algunos miembros del equipo realmente se sentían fuera de su zona de confort. Los pequeños ajustes que hicimos, las lecciones aprendidas de este artículo, realmente ayudaron con esto. Además, la experiencia de desarrollo con Next.js brinda un increíble aumento de la productividad. ¡El ciclo de comentarios de los desarrolladores es increíblemente corto!

Otras lecturas

  • "Ir a producción", Documentos de Next.js